diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 616388d37..0f633d294 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -27,6 +27,7 @@ jobs: run: | mkdir -p ~/.gradle echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties + echo "cc.tweaked.clientTests=true" >> ~/.gradle/gradle.properties - name: Build with Gradle run: | diff --git a/README.md b/README.md index 4573d1d98..37714bd10 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,12 @@ on is present. ```groovy repositories { - maven { url 'https://squiddev.cc/maven/' } + maven { + url 'https://squiddev.cc/maven/' + content { + includeGroup 'org.squiddev' + } + } } dependencies { diff --git a/build.gradle b/build.gradle index 0d2c26cca..858a1501c 100644 --- a/build.gradle +++ b/build.gradle @@ -436,7 +436,11 @@ task setupServer(type: Copy) { } } - check.dependsOn("jacocoTest${name}Report") + if (name != "Client" || project.findProperty('cc.tweaked.clientTests') == 'true') { + // Don't run client tests unless explicitly opted into them. They're a bit of a faff + // to run and pretty flakey. + check.dependsOn("jacocoTest${name}Report") + } } diff --git a/doc/stub/fs.lua b/doc/stub/fs.lua index 40b9107db..aa40a30f3 100644 --- a/doc/stub/fs.lua +++ b/doc/stub/fs.lua @@ -12,6 +12,7 @@ -- @treturn boolean If the path is mounted, rather than a normal file/folder. -- @throws If the path does not exist. -- @see getDrive +-- @since 1.87.0 function isDriveRoot(path) end --[[- Provides completion for a file or directory name, suitable for use with @@ -30,5 +31,6 @@ included in the returned list. @tparam[opt] boolean include_dirs When @{false}, "raw" directories will not be included in the returned list. @treturn { string... } A list of possible completion candidates. +@since 1.74 ]] function complete(path, location, include_files, include_dirs) end diff --git a/doc/stub/http.lua b/doc/stub/http.lua index 28d04d648..81ba63753 100644 --- a/doc/stub/http.lua +++ b/doc/stub/http.lua @@ -2,6 +2,7 @@ -- receiving data from them. -- -- @module http +-- @since 1.1 --- Asynchronously make a HTTP request to the given url. -- @@ -35,6 +36,11 @@ -- -- @see http.get For a synchronous way to make GET requests. -- @see http.post For a synchronous way to make POST requests. +-- +-- @changed 1.63 Added argument for headers. +-- @changed 1.80pr1 Added argument for binary handles. +-- @changed 1.80pr1.6 Added support for table argument. +-- @changed 1.86.0 Added PATCH and TRACE methods. function request(...) end --- Make a HTTP GET request to the given url. @@ -58,6 +64,12 @@ function request(...) end -- @treturn string A message detailing why the request failed. -- @treturn Response|nil The failing http response, if available. -- +-- @changed 1.63 Added argument for headers. +-- @changed 1.80pr1 Response handles are now returned on error if available. +-- @changed 1.80pr1 Added argument for binary handles. +-- @changed 1.80pr1.6 Added support for table argument. +-- @changed 1.86.0 Added PATCH and TRACE methods. +-- -- @usage Make a request to [example.tweaked.cc](https://example.tweaked.cc), -- and print the returned page. -- ```lua @@ -89,6 +101,13 @@ function get(...) end -- error or connection timeout. -- @treturn string A message detailing why the request failed. -- @treturn Response|nil The failing http response, if available. +-- +-- @since 1.31 +-- @changed 1.63 Added argument for headers. +-- @changed 1.80pr1 Response handles are now returned on error if available. +-- @changed 1.80pr1 Added argument for binary handles. +-- @changed 1.80pr1.6 Added support for table argument. +-- @changed 1.86.0 Added PATCH and TRACE methods. function post(...) end --- Asynchronously determine whether a URL can be requested. @@ -142,6 +161,9 @@ function checkURL(url) end -- @treturn Websocket The websocket connection. -- @treturn[2] false If the websocket connection failed. -- @treturn string An error message describing why the connection failed. +-- @since 1.80pr1.1 +-- @changed 1.80pr1.3 No longer asynchronous. +-- @changed 1.95.3 Added User-Agent to default headers. function websocket(url, headers) end --- Asynchronously open a websocket. @@ -154,4 +176,6 @@ function websocket(url, headers) end -- `ws://` or `wss://` protocol. -- @tparam[opt] { [string] = string } headers Additional headers to send as part -- of the initial websocket connection. +-- @since 1.80pr1.3 +-- @changed 1.95.3 Added User-Agent to default headers. function websocketAsync(url, headers) end diff --git a/doc/stub/os.lua b/doc/stub/os.lua index da03257c7..39c051ceb 100644 --- a/doc/stub/os.lua +++ b/doc/stub/os.lua @@ -8,6 +8,7 @@ variables and functions exported by it will by available through the use of @tparam string path The path of the API to load. @treturn boolean Whether or not the API was successfully loaded. +@since 1.2 @deprecated When possible it's best to avoid using this function. It pollutes the global table and can mask errors. @@ -21,6 +22,7 @@ function loadAPI(path) end -- This effectively removes the specified table from `_G`. -- -- @tparam string name The name of the API to unload. +-- @since 1.2 -- @deprecated See @{os.loadAPI} for why. function unloadAPI(name) end @@ -58,6 +60,7 @@ event, printing the error "Terminated". end @see os.pullEventRaw To pull the terminate event. +@changed 1.3 Added filter argument. ]] function pullEvent(filter) end diff --git a/doc/stub/turtle.lua b/doc/stub/turtle.lua index 5f032b5c7..10ae68df7 100644 --- a/doc/stub/turtle.lua +++ b/doc/stub/turtle.lua @@ -9,5 +9,6 @@ empty, including those outside the crafting "grid". @treturn[1] true If crafting succeeds. @treturn[2] false If crafting fails. @treturn string A string describing why crafting failed. +@since 1.4 ]] function craft(limit) end diff --git a/src/main/java/dan200/computercraft/client/ClientRegistry.java b/src/main/java/dan200/computercraft/client/ClientRegistry.java index cb9fba444..e1be11a2e 100644 --- a/src/main/java/dan200/computercraft/client/ClientRegistry.java +++ b/src/main/java/dan200/computercraft/client/ClientRegistry.java @@ -13,11 +13,10 @@ import dan200.computercraft.client.render.TurtleModelLoader; import dan200.computercraft.client.render.TurtlePlayerRenderer; import dan200.computercraft.shared.Registry; import dan200.computercraft.shared.common.IColouredItem; -import dan200.computercraft.shared.computer.inventory.ContainerComputer; +import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; import dan200.computercraft.shared.computer.inventory.ContainerViewComputer; import dan200.computercraft.shared.media.items.ItemDisk; import dan200.computercraft.shared.media.items.ItemTreasureDisk; -import dan200.computercraft.shared.pocket.inventory.ContainerPocketComputer; import dan200.computercraft.shared.pocket.items.ItemPocketComputer; import dan200.computercraft.shared.util.Colour; import net.minecraft.client.gui.screens.MenuScreens; @@ -173,8 +172,9 @@ public final class ClientRegistry { // My IDE doesn't think so, but we do actually need these generics. - MenuScreens.>register( Registry.ModContainers.COMPUTER.get(), GuiComputer::create ); - MenuScreens.>register( Registry.ModContainers.POCKET_COMPUTER.get(), GuiComputer::createPocket ); + MenuScreens.>register( Registry.ModContainers.COMPUTER.get(), GuiComputer::create ); + MenuScreens.>register( Registry.ModContainers.POCKET_COMPUTER.get(), GuiComputer::createPocket ); + MenuScreens.>register( Registry.ModContainers.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new ); MenuScreens.register( Registry.ModContainers.TURTLE.get(), GuiTurtle::new ); MenuScreens.register( Registry.ModContainers.PRINTER.get(), GuiPrinter::new ); diff --git a/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java b/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java index 2ee73f5f2..2c98dc5da 100644 --- a/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java +++ b/src/main/java/dan200/computercraft/client/gui/ComputerScreenBase.java @@ -144,22 +144,43 @@ public abstract class ComputerScreenBase extend return; } + String name = file.getFileName().toString(); + if( name.length() > UploadFileMessage.MAX_FILE_NAME ) + { + alert( UploadResult.FAILED_TITLE, new TranslatableComponent( "gui.computercraft.upload.failed.name_too_long" ) ); + return; + } + ByteBuffer buffer = ByteBuffer.allocateDirect( (int) fileSize ); sbc.read( buffer ); buffer.flip(); - toUpload.add( new FileUpload( file.getFileName().toString(), buffer ) ); + byte[] digest = FileUpload.getDigest( buffer ); + if( digest == null ) + { + alert( UploadResult.FAILED_TITLE, new TranslatableComponent( "gui.computercraft.upload.failed.corrupted" ) ); + return; + } + + buffer.rewind(); + toUpload.add( new FileUpload( name, buffer, digest ) ); } catch( IOException e ) { ComputerCraft.log.error( "Failed uploading files", e ); - alert( UploadResult.FAILED_TITLE, new TranslatableComponent( "computercraft.gui.upload.failed.generic", e.getMessage() ) ); + alert( UploadResult.FAILED_TITLE, new TranslatableComponent( "gui.computercraft.upload.failed.generic", "Cannot compute checksum" ) ); } } + if( toUpload.size() > UploadFileMessage.MAX_FILES ) + { + alert( UploadResult.FAILED_TITLE, new TranslatableComponent( "gui.computercraft.upload.failed.too_many_files" ) ); + return; + } + if( toUpload.size() > 0 ) { - NetworkHandler.sendToServer( new UploadFileMessage( computer.getInstanceID(), toUpload ) ); + UploadFileMessage.send( computer.getInstanceID(), toUpload ); } } diff --git a/src/main/java/dan200/computercraft/client/gui/GuiComputer.java b/src/main/java/dan200/computercraft/client/gui/GuiComputer.java index 0acad3cc3..30379860e 100644 --- a/src/main/java/dan200/computercraft/client/gui/GuiComputer.java +++ b/src/main/java/dan200/computercraft/client/gui/GuiComputer.java @@ -11,10 +11,8 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.client.gui.widgets.ComputerSidebar; import dan200.computercraft.client.gui.widgets.WidgetTerminal; import dan200.computercraft.client.render.ComputerBorderRenderer; -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.network.chat.Component; import net.minecraft.world.entity.player.Inventory; @@ -40,7 +38,7 @@ public final class GuiComputer extends Computer } @Nonnull - public static GuiComputer create( ContainerComputer container, Inventory inventory, Component component ) + public static GuiComputer create( ContainerComputerBase container, Inventory inventory, Component component ) { return new GuiComputer<>( container, inventory, component, @@ -49,7 +47,7 @@ public final class GuiComputer extends Computer } @Nonnull - public static GuiComputer createPocket( ContainerPocketComputer container, Inventory inventory, Component component ) + public static GuiComputer createPocket( ContainerComputerBase container, Inventory inventory, Component component ) { return new GuiComputer<>( container, inventory, component, diff --git a/src/main/java/dan200/computercraft/client/gui/NoTermComputerScreen.java b/src/main/java/dan200/computercraft/client/gui/NoTermComputerScreen.java new file mode 100644 index 000000000..288e1dc02 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/gui/NoTermComputerScreen.java @@ -0,0 +1,108 @@ +/* + * This file is part of 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.vertex.PoseStack; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.gui.widgets.WidgetTerminal; +import dan200.computercraft.shared.computer.core.ClientComputer; +import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.world.entity.player.Inventory; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nonnull; +import java.util.List; + +public class NoTermComputerScreen extends Screen implements MenuAccess +{ + private final T menu; + private WidgetTerminal terminal; + + public NoTermComputerScreen( T menu, Inventory player, Component title ) + { + super( title ); + this.menu = menu; + } + + @Nonnull + @Override + public T getMenu() + { + return menu; + } + + @Override + protected void init() + { + super.init(); + minecraft.keyboardHandler.setSendRepeatsToGui( true ); + + terminal = addWidget( new WidgetTerminal( (ClientComputer) menu.getComputer(), 0, 0, ComputerCraft.pocketTermWidth, ComputerCraft.pocketTermHeight ) ); + terminal.visible = false; + terminal.active = false; + setFocused( terminal ); + } + + @Override + public final void removed() + { + super.removed(); + minecraft.keyboardHandler.setSendRepeatsToGui( false ); + } + + @Override + public final void tick() + { + super.tick(); + terminal.update(); + } + + @Override + public void onClose() + { + minecraft.player.closeContainer(); + super.onClose(); + } + + @Override + public boolean isPauseScreen() + { + return false; + } + + @Override + public final 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() == terminal ) + { + return getFocused().keyPressed( key, scancode, modifiers ); + } + + return super.keyPressed( key, scancode, modifiers ); + } + + @Override + public void render( PoseStack transform, int mouseX, int mouseY, float partialTicks ) + { + super.render( transform, mouseX, mouseY, partialTicks ); + + Font font = minecraft.font; + List lines = font.split( new TranslatableComponent( "gui.computercraft.pocket_computer_overlay" ), (int) (width * 0.8) ); + float y = 10.0f; + for( FormattedCharSequence line : lines ) + { + font.drawShadow( transform, line, (float) ((width / 2) - (minecraft.font.width( line ) / 2)), y, 0xFFFFFF ); + y += 9.0f; + } + } +} diff --git a/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java index b7d5a6e5a..8ecbb90c0 100644 --- a/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java +++ b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java @@ -312,6 +312,7 @@ public class WidgetTerminal extends AbstractWidget @Override public void render( @Nonnull PoseStack transform, int mouseX, int mouseY, float partialTicks ) { + if( !visible ) return; Matrix4f matrix = transform.last().pose(); Terminal terminal = computer.getTerminal(); if( terminal != null ) diff --git a/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/src/main/java/dan200/computercraft/core/apis/FSAPI.java index 35ee648c7..9e1059545 100644 --- a/src/main/java/dan200/computercraft/core/apis/FSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -99,6 +99,7 @@ public class FSAPI implements ILuaAPI * @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. + * @cc.changed 1.95.0 Now supports multiple arguments. * @cc.usage Combine several file paths together *
{@code
      * fs.combine("/rom/programs", "../apis", "parallel.lua")
@@ -126,6 +127,7 @@ public class FSAPI implements ILuaAPI
      *
      * @param path The path to get the name from.
      * @return The final part of the path (the file name).
+     * @cc.since 1.2
      * @cc.usage Get the file name of {@code rom/startup.lua}
      * 
{@code
      * fs.getName("rom/startup.lua")
@@ -143,6 +145,7 @@ public class FSAPI implements ILuaAPI
      *
      * @param path The path to get the directory from.
      * @return The path with the final part removed (the parent directory).
+     * @cc.since 1.63
      * @cc.usage Get the directory name of {@code rom/startup.lua}
      * 
{@code
      * fs.getDir("rom/startup.lua")
@@ -161,6 +164,7 @@ public class FSAPI implements ILuaAPI
      * @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.
+     * @cc.since 1.3
      */
     @LuaFunction
     public final long getSize( String path ) throws LuaException
@@ -458,6 +462,7 @@ public class FSAPI implements ILuaAPI
      * @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".
+     * @cc.since 1.4
      * @see #getCapacity To get the capacity of this drive.
      */
     @LuaFunction
@@ -485,6 +490,7 @@ public class FSAPI implements ILuaAPI
      * @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.
+     * @cc.since 1.6
      */
     @LuaFunction
     public final String[] find( String path ) throws LuaException
@@ -508,6 +514,7 @@ public class FSAPI implements ILuaAPI
      * @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.
+     * @cc.since 1.87.0
      * @see #getFreeSpace To get the free space available on this drive.
      */
     @LuaFunction
@@ -537,6 +544,9 @@ public class FSAPI implements ILuaAPI
      * @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.
+     * @cc.since 1.87.0
+     * @cc.changed 1.91.0 Renamed `modification` field to `modified`.
+     * @cc.changed 1.95.2 Added `isReadOnly` to 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.
      */
diff --git a/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/src/main/java/dan200/computercraft/core/apis/OSAPI.java
index 95f3a8ac3..3140326d7 100644
--- a/src/main/java/dan200/computercraft/core/apis/OSAPI.java
+++ b/src/main/java/dan200/computercraft/core/apis/OSAPI.java
@@ -204,14 +204,15 @@ public class OSAPI implements ILuaAPI
     }
 
     /**
-     * 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.
+     * Sets an alarm that will fire at the specified in-game 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.
+     * @cc.since 1.2
      * @see #cancelAlarm To cancel an alarm.
      */
     @LuaFunction
@@ -232,6 +233,7 @@ public class OSAPI implements ILuaAPI
      * alarm from firing.
      *
      * @param token The ID of the alarm to cancel.
+     * @cc.since 1.2
      * @see #setAlarm To set an alarm.
      */
     @LuaFunction
@@ -277,6 +279,7 @@ public class OSAPI implements ILuaAPI
      *
      * @return The label of the computer.
      * @cc.treturn string The label of the computer.
+     * @cc.since 1.3
      */
     @LuaFunction( { "getComputerLabel", "computerLabel" } )
     public final Object[] getComputerLabel()
@@ -289,6 +292,7 @@ public class OSAPI implements ILuaAPI
      * Set the label of this computer.
      *
      * @param label The new label. May be {@code nil} in order to clear it.
+     * @cc.since 1.3
      */
     @LuaFunction
     public final void setComputerLabel( Optional label )
@@ -300,6 +304,7 @@ public class OSAPI implements ILuaAPI
      * Returns the number of seconds that the computer has been running.
      *
      * @return The computer's uptime.
+     * @cc.since 1.2
      */
     @LuaFunction
     public final double clock()
@@ -325,6 +330,15 @@ public class OSAPI implements ILuaAPI
      * @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 dan200.computercraft.ingame} locale if not specified.
+     * @cc.see textutils.formatTime To convert times into a user-readable string.
+     * @cc.usage Print the current in-game time.
+     * 
{@code
+     * textutils.formatTime(os.time())
+     * }
+ * @cc.since 1.2 + * @cc.changed 1.80pr1 Add support for getting the local local and UTC time. + * @cc.changed 1.82.0 Arguments are now case insensitive. + * @cc.changed 1.83.0 {@link #time(IArguments)} now accepts table arguments and converts them to UNIX timestamps. * @see #date To get a date table that can be converted with this function. */ @LuaFunction @@ -360,6 +374,8 @@ public class OSAPI implements ILuaAPI * @param args The locale to get the day for. Defaults to {@code dan200.computercraft.ingame} if not set. * @return The day depending on the selected locale. * @throws LuaException If an invalid locale is passed. + * @cc.since 1.48 + * @cc.changed 1.82.0 Arguments are now case insensitive. */ @LuaFunction public final int day( Optional args ) throws LuaException @@ -390,6 +406,14 @@ public class OSAPI implements ILuaAPI * @param args The locale to get the milliseconds for. Defaults to {@code dan200.computercraft.ingame} if not set. * @return The milliseconds since the epoch depending on the selected locale. * @throws LuaException If an invalid locale is passed. + * @cc.since 1.80pr1 + * @cc.usage Get the current time and use {@link #date} to convert it to a table. + *
{@code
+     * -- Dividing by 1000 converts it from milliseconds to seconds.
+     * local time = os.epoch("local") / 1000
+     * local time_table = os.date("*t", time)
+     * print(textutils.serialize(time_table))
+     * }
*/ @LuaFunction public final long epoch( Optional args ) throws LuaException @@ -438,6 +462,11 @@ public class OSAPI implements ILuaAPI * @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. + * @cc.since 1.83.0 + * @cc.usage Print the current date in a user-friendly string. + *
{@code
+     * os.date("%A %d %B %Y") -- See the reference above!
+     * }
*/ @LuaFunction public final Object date( Optional formatA, Optional timeA ) throws LuaException diff --git a/src/main/java/dan200/computercraft/core/apis/TermAPI.java b/src/main/java/dan200/computercraft/core/apis/TermAPI.java index 6b1816096..d0fa42ff1 100644 --- a/src/main/java/dan200/computercraft/core/apis/TermAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/TermAPI.java @@ -46,6 +46,7 @@ public class TermAPI extends TermMethods implements ILuaAPI * @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. + * @cc.since 1.81.0 * @see TermMethods#setPaletteColour(IArguments) To change the palette colour. */ @LuaFunction( { "nativePaletteColour", "nativePaletteColor" } ) diff --git a/src/main/java/dan200/computercraft/core/apis/TermMethods.java b/src/main/java/dan200/computercraft/core/apis/TermMethods.java index ce5545124..420c8fc95 100644 --- a/src/main/java/dan200/computercraft/core/apis/TermMethods.java +++ b/src/main/java/dan200/computercraft/core/apis/TermMethods.java @@ -112,6 +112,7 @@ public abstract class TermMethods * * @return If the cursor is blinking. * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.since 1.80pr1.9 */ @LuaFunction public final boolean getCursorBlink() throws LuaException @@ -179,6 +180,7 @@ public abstract class TermMethods * @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. + * @cc.since 1.74 */ @LuaFunction( { "getTextColour", "getTextColor" } ) public final int getTextColour() throws LuaException @@ -192,6 +194,8 @@ public abstract class TermMethods * @param colourArg The new text colour. * @throws LuaException (hidden) If the terminal cannot be found. * @cc.see colors For a list of colour constants. + * @cc.since 1.45 + * @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen. */ @LuaFunction( { "setTextColour", "setTextColor" } ) public final void setTextColour( int colourArg ) throws LuaException @@ -211,6 +215,7 @@ public abstract class TermMethods * @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. + * @cc.since 1.74 */ @LuaFunction( { "getBackgroundColour", "getBackgroundColor" } ) public final int getBackgroundColour() throws LuaException @@ -225,6 +230,8 @@ public abstract class TermMethods * @param colourArg The new background colour. * @throws LuaException (hidden) If the terminal cannot be found. * @cc.see colors For a list of colour constants. + * @cc.since 1.45 + * @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen. */ @LuaFunction( { "setBackgroundColour", "setBackgroundColor" } ) public final void setBackgroundColour( int colourArg ) throws LuaException @@ -245,6 +252,7 @@ public abstract class TermMethods * * @return Whether this terminal supports colour. * @throws LuaException (hidden) If the terminal cannot be found. + * @cc.since 1.45 */ @LuaFunction( { "isColour", "isColor" } ) public final boolean getIsColour() throws LuaException @@ -267,6 +275,8 @@ public abstract class TermMethods * @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.since 1.74 + * @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen. * @cc.usage Prints "Hello, world!" in rainbow text. *
{@code
      * term.blit("Hello, world!","01234456789ab","0000000000000")
@@ -319,6 +329,7 @@ public abstract class TermMethods
      * }
* @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. + * @cc.since 1.80pr1 */ @LuaFunction( { "setPaletteColour", "setPaletteColor" } ) public final void setPaletteColour( IArguments args ) throws LuaException @@ -348,6 +359,7 @@ public abstract class TermMethods * @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. + * @cc.since 1.80pr1 */ @LuaFunction( { "getPaletteColour", "getPaletteColor" } ) public final Object[] getPaletteColour( int colourArg ) throws LuaException diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java index 4477dde92..b026aa750 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java @@ -61,6 +61,7 @@ public class BinaryReadableHandle extends HandleGeneric * @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. + * @cc.changed 1.80pr1 Now accepts an integer argument to read multiple bytes, returning a string instead of a number. */ @LuaFunction public final Object[] read( Optional countArg ) throws LuaException @@ -145,6 +146,7 @@ public class BinaryReadableHandle extends HandleGeneric * @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. + * @cc.since 1.80pr1 */ @LuaFunction public final Object[] readAll() throws LuaException @@ -182,6 +184,8 @@ public class BinaryReadableHandle extends HandleGeneric * @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. + * @cc.since 1.80pr1.9 + * @cc.changed 1.81.0 `\r` is now stripped. */ @LuaFunction public final Object[] readLine( Optional withTrailingArg ) throws LuaException @@ -259,6 +263,7 @@ public class BinaryReadableHandle extends HandleGeneric * @cc.treturn [1] number The new position. * @cc.treturn [2] nil If seeking failed. * @cc.treturn string The reason seeking failed. + * @cc.since 1.80pr1.9 */ @LuaFunction public final Object[] seek( Optional whence, Optional offset ) throws LuaException diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java index 796582855..39e234648 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java @@ -55,6 +55,7 @@ public class BinaryWritableHandle extends HandleGeneric * @throws LuaException If the file has been closed. * @cc.tparam [1] number The byte to write. * @cc.tparam [2] string The string to write. + * @cc.changed 1.80pr1 Now accepts a string to write multiple bytes. */ @LuaFunction public final void write( IArguments arguments ) throws LuaException @@ -130,6 +131,7 @@ public class BinaryWritableHandle extends HandleGeneric * @cc.treturn [1] number The new position. * @cc.treturn [2] nil If seeking failed. * @cc.treturn string The reason seeking failed. + * @cc.since 1.80pr1.9 */ @LuaFunction public final Object[] seek( Optional whence, Optional offset ) throws LuaException diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java index 28576f70d..27e1b7083 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java @@ -50,6 +50,7 @@ public class EncodedReadableHandle extends HandleGeneric * @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. + * @cc.changed 1.81.0 Added option to return trailing newline. */ @LuaFunction public final Object[] readLine( Optional withTrailingArg ) throws LuaException @@ -116,6 +117,7 @@ public class EncodedReadableHandle extends HandleGeneric * @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. + * @cc.since 1.80pr1.4 */ @LuaFunction public final Object[] read( Optional countA ) throws LuaException diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java index c5d72134b..4dcd82c9b 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java @@ -46,6 +46,7 @@ public class HttpResponseHandle implements ObjectSource * @return The response code and message. * @cc.treturn number The response code (i.e. 200) * @cc.treturn string The response message (i.e. "OK") + * @cc.changed 1.80pr1.13 Added response message return value. */ @LuaFunction public final Object[] getResponseCode() diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java index 0c74c0c9f..0901cc25e 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -55,6 +55,8 @@ public class WebsocketHandle implements Closeable * @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. + * @cc.changed 1.80pr1.13 Added return value indicating whether the message was binary. + * @cc.changed 1.87.0 Added timeout argument. */ @LuaFunction public final MethodResult receive( Optional timeout ) throws LuaException @@ -74,6 +76,7 @@ public class WebsocketHandle implements Closeable * @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. + * @cc.changed 1.81.0 Added argument for binary mode. */ @LuaFunction public final void send( Object message, Optional binary ) throws LuaException diff --git a/src/main/java/dan200/computercraft/shared/Registry.java b/src/main/java/dan200/computercraft/shared/Registry.java index 1ac78c40a..798f20c53 100644 --- a/src/main/java/dan200/computercraft/shared/Registry.java +++ b/src/main/java/dan200/computercraft/shared/Registry.java @@ -18,7 +18,8 @@ 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.ComputerMenuWithoutInventory; +import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; import dan200.computercraft.shared.computer.inventory.ContainerViewComputer; import dan200.computercraft.shared.computer.items.ItemComputer; import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe; @@ -52,7 +53,6 @@ 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; @@ -312,11 +312,14 @@ public final class Registry { static final DeferredRegister> CONTAINERS = DeferredRegister.create( ForgeRegistries.CONTAINERS, ComputerCraft.MOD_ID ); - public static final RegistryObject> COMPUTER = CONTAINERS.register( "computer", - () -> ContainerData.toType( ComputerContainerData::new, ContainerComputer::new ) ); + public static final RegistryObject> COMPUTER = CONTAINERS.register( "computer", + () -> ContainerData.toType( ComputerContainerData::new, ComputerMenuWithoutInventory::new ) ); - public static final RegistryObject> POCKET_COMPUTER = CONTAINERS.register( "pocket_computer", - () -> ContainerData.toType( ComputerContainerData::new, ContainerPocketComputer::new ) ); + public static final RegistryObject> POCKET_COMPUTER = CONTAINERS.register( "pocket_computer", + () -> ContainerData.toType( ComputerContainerData::new, ComputerMenuWithoutInventory::new ) ); + + public static final RegistryObject> POCKET_COMPUTER_NO_TERM = CONTAINERS.register( "pocket_computer_no_term", + () -> ContainerData.toType( ComputerContainerData::new, ComputerMenuWithoutInventory::new ) ); public static final RegistryObject> TURTLE = CONTAINERS.register( "turtle", () -> ContainerData.toType( ComputerContainerData::new, ContainerTurtle::new ) ); diff --git a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index 97042910e..10fde7489 100644 --- a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -236,7 +236,7 @@ public final class CommandComputerCraft @Override public AbstractContainerMenu createMenu( int id, @Nonnull Inventory player, @Nonnull Player entity ) { - return new ContainerViewComputer( id, computer ); + return new ContainerViewComputer( id, player, computer ); } } ); return 1; diff --git a/src/main/java/dan200/computercraft/shared/command/UserLevel.java b/src/main/java/dan200/computercraft/shared/command/UserLevel.java index 3ea882b2e..8f5715edb 100644 --- a/src/main/java/dan200/computercraft/shared/command/UserLevel.java +++ b/src/main/java/dan200/computercraft/shared/command/UserLevel.java @@ -56,18 +56,17 @@ public enum UserLevel implements Predicate public boolean test( CommandSourceStack 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 Player && - ((Player) sender).getGameProfile().getName().equalsIgnoreCase( server.getServerModName() ) ) - { - return true; - } - } - + if( this == OWNER ) return isOwner( source ); + if( this == OWNER_OP && isOwner( source ) ) return true; return source.hasPermission( toLevel() ); } + + private static boolean isOwner( CommandSourceStack source ) + { + MinecraftServer server = source.getServer(); + Entity sender = source.getEntity(); + return server.isDedicatedServer() + ? source.getEntity() == null && source.hasPermission( 4 ) && source.getTextName().equals( "Server" ) + : sender instanceof Player && ((Player) sender).getGameProfile().getName().equalsIgnoreCase( server.getServerModName() ); + } } diff --git a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java index d1f2d5860..4b8e2b612 100644 --- a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java +++ b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java @@ -25,6 +25,7 @@ import java.util.*; /** * @cc.module commands + * @cc.since 1.7 */ public class CommandAPI implements ILuaAPI { @@ -90,6 +91,8 @@ public class CommandAPI implements ILuaAPI * @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.changed 1.71 Added return value with command output. + * @cc.changed 1.85.0 Added return value with the number of affected objects. * @cc.usage Set the block above the command computer to stone. *
{@code
      * commands.exec("setblock ~ ~1 ~ minecraft:stone")
@@ -118,7 +121,7 @@ public class CommandAPI implements ILuaAPI
      * @throws LuaException (hidden) If the task cannot be created.
      * @cc.usage Asynchronously sets the block above the computer to stone.
      * 
{@code
-     * commands.execAsync("~ ~1 ~ minecraft:stone")
+     * commands.execAsync("setblock ~ ~1 ~ minecraft:stone")
      * }
* @cc.see parallel One may also use the parallel API to run multiple commands at once. */ @@ -193,6 +196,7 @@ public class CommandAPI implements ILuaAPI * @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. + * @cc.since 1.76 */ @LuaFunction( mainThread = true ) public final List> getBlockInfos( int minX, int minY, int minZ, int maxX, int maxY, int maxZ ) throws LuaException @@ -245,6 +249,7 @@ public class CommandAPI implements ILuaAPI * @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. + * @cc.changed 1.76 Added block state info to return value */ @LuaFunction( mainThread = true ) public final Map getBlockInfo( int x, int y, int z ) throws LuaException diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java index 842c5d80a..9ba5fcf36 100644 --- a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java @@ -8,10 +8,11 @@ package dan200.computercraft.shared.computer.blocks; import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.shared.Registry; import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.core.ServerComputer; -import dan200.computercraft.shared.computer.inventory.ContainerComputer; +import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory; import dan200.computercraft.shared.util.CapabilityUtil; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -51,7 +52,7 @@ public class TileComputer extends TileComputerBase return computer; } - public boolean isUsableByPlayer( Player player ) + protected boolean isUsableByPlayer( Player player ) { return isUsable( player, false ); } @@ -86,7 +87,7 @@ public class TileComputer extends TileComputerBase @Override public AbstractContainerMenu createMenu( int id, @Nonnull Inventory inventory, @Nonnull Player player ) { - return new ContainerComputer( id, this ); + return new ComputerMenuWithoutInventory( Registry.ModContainers.COMPUTER.get(), id, inventory, this::isUsableByPlayer, createServerComputer(), getFamily() ); } @Nonnull diff --git a/src/main/java/dan200/computercraft/shared/computer/core/IContainerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/IContainerComputer.java index 491d05e3b..31dc660a7 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/IContainerComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/IContainerComputer.java @@ -5,6 +5,7 @@ */ package dan200.computercraft.shared.computer.core; +import dan200.computercraft.shared.computer.upload.FileSlice; import dan200.computercraft.shared.computer.upload.FileUpload; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.inventory.AbstractContainerMenu; @@ -12,6 +13,7 @@ import net.minecraft.world.inventory.AbstractContainerMenu; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.List; +import java.util.UUID; /** * An instance of {@link AbstractContainerMenu} which provides a computer. You should implement this @@ -38,12 +40,28 @@ public interface IContainerComputer InputState getInput(); /** - * Attempt to upload a series of files to this computer. + * Start a file upload into this container. * - * @param uploader The player uploading files. + * @param uploadId The unique ID of this upload. * @param files The files to upload. */ - void upload( @Nonnull ServerPlayer uploader, @Nonnull List files ); + void startUpload( @Nonnull UUID uploadId, @Nonnull List files ); + + /** + * Append more data to partially uploaded files. + * + * @param uploadId The unique ID of this upload. + * @param slices Additional parts of file data to upload. + */ + void continueUpload( @Nonnull UUID uploadId, @Nonnull List slices ); + + /** + * Finish off an upload. This either writes the uploaded files or + * + * @param uploader The player uploading files. + * @param uploadId The unique ID of this upload. + */ + void finishUpload( @Nonnull ServerPlayer uploader, @Nonnull UUID uploadId ); /** * Continue an upload. @@ -51,5 +69,5 @@ public interface IContainerComputer * @param uploader The player uploading files. * @param overwrite Whether the files should be overwritten or not. */ - void continueUpload( @Nonnull ServerPlayer uploader, boolean overwrite ); + void confirmUpload( @Nonnull ServerPlayer uploader, boolean overwrite ); } diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ComputerMenuWithoutInventory.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ComputerMenuWithoutInventory.java new file mode 100644 index 000000000..8b7971397 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ComputerMenuWithoutInventory.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.inventory; + +import dan200.computercraft.shared.computer.core.ComputerFamily; +import dan200.computercraft.shared.computer.core.IComputer; +import dan200.computercraft.shared.network.container.ComputerContainerData; +import dan200.computercraft.shared.util.InvisibleSlot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.MenuType; + +import java.util.function.Predicate; + +/** + * A computer menu which does not have any visible inventory. + * + * This adds invisible versions of the player's hotbars slots, to ensure they're synced to the client when changed. + */ +public class ComputerMenuWithoutInventory extends ContainerComputerBase +{ + public ComputerMenuWithoutInventory( MenuType type, int id, Inventory player, Predicate canUse, IComputer computer, ComputerFamily family ) + { + super( type, id, canUse, computer, family ); + addSlots( player ); + } + + public ComputerMenuWithoutInventory( MenuType type, int id, Inventory player, ComputerContainerData data ) + { + super( type, id, player, data ); + addSlots( player ); + } + + private void addSlots( Inventory player ) + { + for( int i = 0; i < 9; i++ ) addSlot( new InvisibleSlot( player, i ) ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java deleted file mode 100644 index 146990892..000000000 --- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This file is part of 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.Registry; -import dan200.computercraft.shared.computer.blocks.TileComputer; -import dan200.computercraft.shared.network.container.ComputerContainerData; -import net.minecraft.world.entity.player.Inventory; - -public class ContainerComputer extends ContainerComputerBase -{ - public ContainerComputer( int id, TileComputer tile ) - { - super( Registry.ModContainers.COMPUTER.get(), id, tile::isUsableByPlayer, tile.createServerComputer(), tile.getFamily() ); - } - - public ContainerComputer( int id, Inventory player, ComputerContainerData data ) - { - super( Registry.ModContainers.COMPUTER.get(), id, player, data ); - } -} diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java index f75d68798..738f47e32 100644 --- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputerBase.java @@ -10,6 +10,7 @@ import dan200.computercraft.core.filesystem.FileSystem; import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.filesystem.FileSystemWrapper; import dan200.computercraft.shared.computer.core.*; +import dan200.computercraft.shared.computer.upload.FileSlice; import dan200.computercraft.shared.computer.upload.FileUpload; import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.network.NetworkHandler; @@ -29,10 +30,11 @@ import java.nio.channels.WritableByteChannel; import java.util.ArrayList; import java.util.List; import java.util.StringJoiner; +import java.util.UUID; import java.util.function.Function; import java.util.function.Predicate; -public class ContainerComputerBase extends AbstractContainerMenu implements IContainerComputer +public abstract class ContainerComputerBase extends AbstractContainerMenu implements IContainerComputer { private static final String LIST_PREFIX = "\n \u2022 "; @@ -40,9 +42,11 @@ public class ContainerComputerBase extends AbstractContainerMenu implements ICon private final IComputer computer; private final ComputerFamily family; private final InputState input = new InputState( this ); + + private UUID toUploadId; private List toUpload; - protected ContainerComputerBase( MenuType type, int id, Predicate canUse, IComputer computer, ComputerFamily family ) + public ContainerComputerBase( MenuType type, int id, Predicate canUse, IComputer computer, ComputerFamily family ) { super( type, id ); this.canUse = canUse; @@ -50,7 +54,7 @@ public class ContainerComputerBase extends AbstractContainerMenu implements ICon this.family = family; } - protected ContainerComputerBase( MenuType type, int id, Inventory player, ComputerContainerData data ) + public ContainerComputerBase( MenuType type, int id, Inventory player, ComputerContainerData data ) { this( type, id, x -> true, getComputer( player, data ), data.getFamily() ); } @@ -92,25 +96,52 @@ public class ContainerComputerBase extends AbstractContainerMenu implements ICon } @Override - public void upload( @Nonnull ServerPlayer uploader, @Nonnull List files ) + public void startUpload( @Nonnull UUID uuid, @Nonnull List files ) { - UploadResultMessage message = upload( files, false ); + toUploadId = uuid; + toUpload = files; + } + + @Override + public void continueUpload( @Nonnull UUID uploadId, @Nonnull List slices ) + { + if( toUploadId == null || toUpload == null || !toUploadId.equals( uploadId ) ) + { + ComputerCraft.log.warn( "Invalid continueUpload call, skipping." ); + return; + } + + for( FileSlice slice : slices ) slice.apply( toUpload ); + } + + @Override + public void finishUpload( @Nonnull ServerPlayer uploader, @Nonnull UUID uploadId ) + { + if( toUploadId == null || toUpload == null || toUpload.isEmpty() || !toUploadId.equals( uploadId ) ) + { + ComputerCraft.log.warn( "Invalid finishUpload call, skipping." ); + return; + } + + UploadResultMessage message = finishUpload( false ); NetworkHandler.sendToPlayer( uploader, message ); } @Override - public void continueUpload( @Nonnull ServerPlayer uploader, boolean overwrite ) + public void confirmUpload( @Nonnull ServerPlayer uploader, boolean overwrite ) { - List files = this.toUpload; - toUpload = null; - if( files == null || files.isEmpty() || !overwrite ) return; + if( toUploadId == null || toUpload == null || toUpload.isEmpty() ) + { + ComputerCraft.log.warn( "Invalid finishUpload call, skipping." ); + return; + } - UploadResultMessage message = upload( files, true ); + UploadResultMessage message = finishUpload( true ); NetworkHandler.sendToPlayer( uploader, message ); } @Nonnull - private UploadResultMessage upload( @Nonnull List files, boolean forceOverwrite ) + private UploadResultMessage finishUpload( boolean forceOverwrite ) { ServerComputer computer = (ServerComputer) getComputer(); if( computer == null ) return UploadResultMessage.COMPUTER_OFF; @@ -118,9 +149,20 @@ public class ContainerComputerBase extends AbstractContainerMenu implements ICon FileSystem fs = computer.getComputer().getEnvironment().getFileSystem(); if( fs == null ) return UploadResultMessage.COMPUTER_OFF; + for( FileUpload upload : toUpload ) + { + if( !upload.checksumMatches() ) + { + ComputerCraft.log.warn( "Checksum failed to match for {}.", upload.getName() ); + return new UploadResultMessage( UploadResult.ERROR, new TranslatableComponent( "gui.computercraft.upload.failed.corrupted" ) ); + } + } + try { List overwrite = new ArrayList<>(); + List files = toUpload; + toUpload = null; for( FileUpload upload : files ) { if( !fs.exists( upload.getName() ) ) continue; @@ -139,7 +181,6 @@ public class ContainerComputerBase extends AbstractContainerMenu implements ICon { StringJoiner joiner = new StringJoiner( LIST_PREFIX, LIST_PREFIX, "" ); for( String value : overwrite ) joiner.add( value ); - toUpload = files; return new UploadResultMessage( UploadResult.CONFIRM_OVERWRITE, @@ -167,7 +208,7 @@ public class ContainerComputerBase extends AbstractContainerMenu implements ICon catch( FileSystemException | IOException e ) { ComputerCraft.log.error( "Error uploading files", e ); - return new UploadResultMessage( UploadResult.ERROR, new TranslatableComponent( "computercraft.gui.upload.failed.generic", e.getMessage() ) ); + return new UploadResultMessage( UploadResult.ERROR, new TranslatableComponent( "gui.computercraft.upload.failed.generic", e.getMessage() ) ); } } diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java index 2a7e78450..6284efe42 100644 --- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java @@ -16,14 +16,14 @@ import net.minecraft.world.entity.player.Player; import javax.annotation.Nonnull; -public class ContainerViewComputer extends ContainerComputerBase +public class ContainerViewComputer extends ComputerMenuWithoutInventory { private final int width; private final int height; - public ContainerViewComputer( int id, ServerComputer computer ) + public ContainerViewComputer( int id, Inventory player, ServerComputer computer ) { - super( Registry.ModContainers.VIEW_COMPUTER.get(), id, player -> canInteractWith( computer, player ), computer, computer.getFamily() ); + super( Registry.ModContainers.VIEW_COMPUTER.get(), id, player, p -> canInteractWith( computer, p ), computer, computer.getFamily() ); width = height = 0; } diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/FileSlice.java b/src/main/java/dan200/computercraft/shared/computer/upload/FileSlice.java new file mode 100644 index 000000000..2f290e660 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/upload/FileSlice.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.computer.upload; + +import dan200.computercraft.ComputerCraft; + +import java.nio.ByteBuffer; +import java.util.List; + +public class FileSlice +{ + private final int fileId; + private final int offset; + private final ByteBuffer bytes; + + public FileSlice( int fileId, int offset, ByteBuffer bytes ) + { + this.fileId = fileId; + this.offset = offset; + this.bytes = bytes; + } + + public int getFileId() + { + return fileId; + } + + public int getOffset() + { + return offset; + } + + public ByteBuffer getBytes() + { + return bytes; + } + + public void apply( List files ) + { + if( fileId < 0 || fileId >= files.size() ) + { + ComputerCraft.log.warn( "File ID is out-of-bounds (0 <= {} < {})", fileId, files.size() ); + return; + } + + ByteBuffer file = files.get( fileId ).getBytes(); + if( offset < 0 || offset + bytes.remaining() > file.capacity() ) + { + ComputerCraft.log.warn( "File offset is out-of-bounds (0 <= {} <= {})", offset, file.capacity() - offset ); + return; + } + + bytes.rewind(); + file.position( offset ); + file.put( bytes ); + file.rewind(); + + if( bytes.remaining() != 0 ) throw new IllegalStateException( "Should have read the whole buffer" ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/upload/FileUpload.java b/src/main/java/dan200/computercraft/shared/computer/upload/FileUpload.java index 93757d6a7..6f70ad9e1 100644 --- a/src/main/java/dan200/computercraft/shared/computer/upload/FileUpload.java +++ b/src/main/java/dan200/computercraft/shared/computer/upload/FileUpload.java @@ -5,26 +5,76 @@ */ package dan200.computercraft.shared.computer.upload; +import dan200.computercraft.ComputerCraft; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; public class FileUpload { - private final String name; - private final ByteBuffer bytes; + public static final int CHECKSUM_LENGTH = 32; - public FileUpload( String name, ByteBuffer bytes ) + private final String name; + private final int length; + private final ByteBuffer bytes; + private final byte[] checksum; + + public FileUpload( String name, ByteBuffer bytes, byte[] checksum ) { this.name = name; this.bytes = bytes; + length = bytes.remaining(); + this.checksum = checksum; } + @Nonnull public String getName() { return name; } + @Nonnull public ByteBuffer getBytes() { return bytes; } + + public int getLength() + { + return length; + } + + @Nonnull + public byte[] getChecksum() + { + return checksum; + } + + public boolean checksumMatches() + { + // This is meant to be a checksum. Doesn't need to be cryptographically secure, hence non constant time. + byte[] digest = getDigest( bytes ); + return digest != null && Arrays.equals( checksum, digest ); + } + + @Nullable + public static byte[] getDigest( ByteBuffer bytes ) + { + try + { + bytes.rewind(); + MessageDigest digest = MessageDigest.getInstance( "SHA-256" ); + digest.update( bytes ); + return digest.digest(); + } + catch( NoSuchAlgorithmException e ) + { + ComputerCraft.log.warn( "Failed to compute digest ({})", e.toString() ); + return null; + } + } } diff --git a/src/main/java/dan200/computercraft/shared/network/container/ContainerData.java b/src/main/java/dan200/computercraft/shared/network/container/ContainerData.java index 872d76f4d..d45285ac7 100644 --- a/src/main/java/dan200/computercraft/shared/network/container/ContainerData.java +++ b/src/main/java/dan200/computercraft/shared/network/container/ContainerData.java @@ -13,6 +13,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.MenuType; import net.minecraftforge.common.extensions.IForgeContainerType; +import net.minecraftforge.fmllegacy.network.IContainerFactory; import net.minecraftforge.fmllegacy.network.NetworkHooks; import javax.annotation.Nonnull; @@ -37,8 +38,36 @@ public interface ContainerData return IForgeContainerType.create( ( id, player, data ) -> factory.create( id, player, reader.apply( data ) ) ); } + static MenuType toType( Function reader, FixedFactory factory ) + { + return new FixedPointContainerFactory<>( reader, factory ).type; + } + interface Factory { C create( int id, @Nonnull Inventory inventory, T data ); } + + interface FixedFactory + { + C create( MenuType type, int id, @Nonnull Inventory inventory, T data ); + } + + class FixedPointContainerFactory implements IContainerFactory + { + private final IContainerFactory impl; + private final MenuType type; + + private FixedPointContainerFactory( Function reader, FixedFactory factory ) + { + MenuType type = this.type = IForgeContainerType.create( this ); + impl = ( id, player, data ) -> factory.create( type, id, player, reader.apply( data ) ); + } + + @Override + public C create( int windowId, Inventory inv, FriendlyByteBuf data ) + { + return impl.create( windowId, inv, data ); + } + } } diff --git a/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java b/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java index 17421693c..c371af183 100644 --- a/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java +++ b/src/main/java/dan200/computercraft/shared/network/server/ContinueUploadMessage.java @@ -40,6 +40,6 @@ public class ContinueUploadMessage extends ComputerServerMessage protected void handle( NetworkEvent.Context context, @Nonnull ServerComputer computer, @Nonnull IContainerComputer container ) { ServerPlayer player = context.getSender(); - if( player != null ) container.continueUpload( player, overwrite ); + if( player != null ) container.confirmUpload( player, overwrite ); } } diff --git a/src/main/java/dan200/computercraft/shared/network/server/UploadFileMessage.java b/src/main/java/dan200/computercraft/shared/network/server/UploadFileMessage.java index 75fcc9f19..235d6dcd4 100644 --- a/src/main/java/dan200/computercraft/shared/network/server/UploadFileMessage.java +++ b/src/main/java/dan200/computercraft/shared/network/server/UploadFileMessage.java @@ -5,9 +5,13 @@ */ 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.computer.upload.FileSlice; import dan200.computercraft.shared.computer.upload.FileUpload; +import dan200.computercraft.shared.network.NetworkHandler; +import io.netty.handler.codec.DecoderException; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.fmllegacy.network.NetworkEvent; @@ -16,34 +20,81 @@ import javax.annotation.Nonnull; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class UploadFileMessage extends ComputerServerMessage { - public static final int MAX_SIZE = 30 * 1024; // Max packet size is 32767. TODO: Bump this in the future - private final List files; + public static final int MAX_SIZE = 512 * 1024; + static final int MAX_PACKET_SIZE = 30 * 1024; // Max packet size is 32767. - public UploadFileMessage( int instanceId, List files ) + public static final int MAX_FILES = 32; + public static final int MAX_FILE_NAME = 128; + + private static final int FLAG_FIRST = 1; + private static final int FLAG_LAST = 2; + + private final UUID uuid; + private final int flag; + private final List files; + private final List slices; + + UploadFileMessage( int instanceId, UUID uuid, int flag, List files, List slices ) { super( instanceId ); + this.uuid = uuid; + this.flag = flag; this.files = files; + this.slices = slices; } public UploadFileMessage( @Nonnull FriendlyByteBuf buf ) { super( buf ); - int nFiles = buf.readVarInt(); - List files = this.files = new ArrayList<>( nFiles ); - for( int i = 0; i < nFiles; i++ ) + uuid = buf.readUUID(); + int flag = this.flag = buf.readByte(); + + int totalSize = 0; + if( (flag & FLAG_FIRST) != 0 ) { - String name = buf.readUtf( 32767 ); - int size = buf.readVarInt(); - if( size > MAX_SIZE ) break; + int nFiles = buf.readVarInt(); + if( nFiles >= MAX_FILES ) throw new DecoderException( "Too many files" ); + + List files = this.files = new ArrayList<>( nFiles ); + for( int i = 0; i < nFiles; i++ ) + { + String name = buf.readUtf( MAX_FILE_NAME ); + int size = buf.readVarInt(); + if( size > MAX_SIZE || (totalSize += size) >= MAX_SIZE ) + { + throw new DecoderException( "Files are too large" ); + } + + byte[] digest = new byte[FileUpload.CHECKSUM_LENGTH]; + buf.readBytes( digest ); + + files.add( new FileUpload( name, ByteBuffer.allocateDirect( size ), digest ) ); + } + } + else + { + files = null; + } + + int nSlices = buf.readVarInt(); + List slices = this.slices = new ArrayList<>( nSlices ); + for( int i = 0; i < nSlices; i++ ) + { + int fileId = buf.readUnsignedByte(); + int offset = buf.readVarInt(); + + int size = buf.readUnsignedShort(); + if( size > MAX_PACKET_SIZE ) throw new DecoderException( "File is too large" ); ByteBuffer buffer = ByteBuffer.allocateDirect( size ); buf.readBytes( buffer ); buffer.flip(); - files.add( new FileUpload( name, buffer ) ); + slices.add( new FileSlice( fileId, offset, buffer ) ); } } @@ -51,19 +102,85 @@ public class UploadFileMessage extends ComputerServerMessage public void toBytes( @Nonnull FriendlyByteBuf buf ) { super.toBytes( buf ); - buf.writeVarInt( files.size() ); - for( FileUpload file : files ) + buf.writeUUID( uuid ); + buf.writeByte( flag ); + + if( (flag & FLAG_FIRST) != 0 ) { - buf.writeUtf( file.getName() ); - buf.writeVarInt( file.getBytes().remaining() ); - buf.writeBytes( file.getBytes() ); + buf.writeVarInt( files.size() ); + for( FileUpload file : files ) + { + buf.writeUtf( file.getName(), MAX_FILE_NAME ); + buf.writeVarInt( file.getLength() ); + buf.writeBytes( file.getChecksum() ); + } } + + buf.writeVarInt( slices.size() ); + for( FileSlice slice : slices ) + { + buf.writeByte( slice.getFileId() ); + buf.writeVarInt( slice.getOffset() ); + + slice.getBytes().rewind(); + buf.writeShort( slice.getBytes().remaining() ); + buf.writeBytes( slice.getBytes() ); + } + } + + public static void send( int instanceId, List files ) + { + UUID uuid = UUID.randomUUID(); + + int remaining = MAX_PACKET_SIZE; + for( FileUpload file : files ) remaining -= file.getName().length() * 4 + FileUpload.CHECKSUM_LENGTH; + + boolean first = true; + List slices = new ArrayList<>( files.size() ); + for( int fileId = 0; fileId < files.size(); fileId++ ) + { + FileUpload file = files.get( fileId ); + ByteBuffer contents = file.getBytes(); + int capacity = contents.limit(); + + int currentOffset = 0; + while( currentOffset < capacity ) + { + if( remaining <= 0 ) + { + NetworkHandler.sendToServer( first + ? new UploadFileMessage( instanceId, uuid, FLAG_FIRST, files, new ArrayList<>( slices ) ) + : new UploadFileMessage( instanceId, uuid, 0, null, new ArrayList<>( slices ) ) ); + slices.clear(); + remaining = MAX_PACKET_SIZE; + first = false; + } + + int canWrite = Math.min( remaining, capacity - currentOffset ); + + ComputerCraft.log.info( "Adding slice from {} to {}", currentOffset, currentOffset + canWrite - 1 ); + contents.position( currentOffset ).limit( currentOffset + canWrite ); + slices.add( new FileSlice( fileId, currentOffset, contents.slice() ) ); + currentOffset += canWrite; + } + + contents.position( 0 ).limit( capacity ); + } + + NetworkHandler.sendToServer( first + ? new UploadFileMessage( instanceId, uuid, FLAG_FIRST | FLAG_LAST, files, new ArrayList<>( slices ) ) + : new UploadFileMessage( instanceId, uuid, FLAG_LAST, null, new ArrayList<>( slices ) ) ); } @Override protected void handle( NetworkEvent.Context context, @Nonnull ServerComputer computer, @Nonnull IContainerComputer container ) { ServerPlayer player = context.getSender(); - if( player != null ) container.upload( player, files ); + if( player != null ) + { + if( (flag & FLAG_FIRST) != 0 ) container.startUpload( uuid, files ); + container.continueUpload( uuid, slices ); + if( (flag & FLAG_LAST) != 0 ) container.finishUpload( player, uuid ); + } } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java index 3eaec4188..17d370159 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java @@ -186,6 +186,7 @@ public class DiskDrivePeripheral implements IPeripheral * * @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. + * @cc.since 1.4 */ @LuaFunction public final Object[] getDiskID() diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java index 45d470132..12af0a546 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java @@ -187,6 +187,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements IW * * @return The current computer's name. * @cc.treturn string|nil The current computer's name on the wired network. + * @cc.since 1.80pr1.7 */ @LuaFunction public final Object[] getNameLocal() diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java index fb9da8dda..c1a1c7ce4 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java @@ -71,6 +71,7 @@ public class MonitorPeripheral extends TermMethods implements IPeripheral * * @return The monitor's current scale. * @throws LuaException If the monitor cannot be found. + * @cc.since 1.81.0 */ @LuaFunction public final double getTextScale() throws LuaException diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java index fd34914f2..4671e9b06 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java @@ -35,6 +35,7 @@ import static dan200.computercraft.api.lua.LuaValues.checkFinite; * Speakers allow playing notes and other sounds. * * @cc.module speaker + * @cc.since 1.80pr1 */ public abstract class SpeakerPeripheral implements IPeripheral { diff --git a/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java b/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java deleted file mode 100644 index 59ba6c860..000000000 --- a/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * This file is part of 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.Registry; -import dan200.computercraft.shared.computer.core.ServerComputer; -import dan200.computercraft.shared.computer.inventory.ContainerComputerBase; -import dan200.computercraft.shared.network.container.ComputerContainerData; -import dan200.computercraft.shared.pocket.items.ItemPocketComputer; -import net.minecraft.network.chat.Component; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.MenuProvider; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.item.ItemStack; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public final class ContainerPocketComputer extends ContainerComputerBase -{ - private ContainerPocketComputer( int id, ServerComputer computer, ItemPocketComputer item, InteractionHand hand ) - { - super( Registry.ModContainers.POCKET_COMPUTER.get(), id, p -> { - ItemStack stack = p.getItemInHand( hand ); - return stack.getItem() == item && ItemPocketComputer.getServerComputer( stack ) == computer; - }, computer, item.getFamily() ); - } - - public ContainerPocketComputer( int id, Inventory player, ComputerContainerData data ) - { - super( Registry.ModContainers.POCKET_COMPUTER.get(), id, player, data ); - } - - public static class Factory implements MenuProvider - { - private final ServerComputer computer; - private final Component name; - private final ItemPocketComputer item; - private final InteractionHand hand; - - public Factory( ServerComputer computer, ItemStack stack, ItemPocketComputer item, InteractionHand hand ) - { - this.computer = computer; - name = stack.getHoverName(); - this.item = item; - this.hand = hand; - } - - - @Nonnull - @Override - public Component getDisplayName() - { - return name; - } - - @Nullable - @Override - public AbstractContainerMenu createMenu( int id, @Nonnull Inventory inventory, @Nonnull Player entity ) - { - return new ContainerPocketComputer( id, computer, item, hand ); - } - } -} diff --git a/src/main/java/dan200/computercraft/shared/pocket/inventory/PocketComputerMenuProvider.java b/src/main/java/dan200/computercraft/shared/pocket/inventory/PocketComputerMenuProvider.java new file mode 100644 index 000000000..4ea9e8be3 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/pocket/inventory/PocketComputerMenuProvider.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.inventory; + +import dan200.computercraft.shared.Registry; +import dan200.computercraft.shared.computer.core.ServerComputer; +import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory; +import dan200.computercraft.shared.pocket.items.ItemPocketComputer; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class PocketComputerMenuProvider implements MenuProvider +{ + private final ServerComputer computer; + private final Component name; + private final ItemPocketComputer item; + private final InteractionHand hand; + private final boolean isTypingOnly; + + public PocketComputerMenuProvider( ServerComputer computer, ItemStack stack, ItemPocketComputer item, InteractionHand hand, boolean isTypingOnly ) + { + this.computer = computer; + name = stack.getHoverName(); + this.item = item; + this.hand = hand; + this.isTypingOnly = isTypingOnly; + } + + + @Nonnull + @Override + public Component getDisplayName() + { + return name; + } + + @Nullable + @Override + public AbstractContainerMenu createMenu( int id, @Nonnull Inventory inventory, @Nonnull Player entity ) + { + return new ComputerMenuWithoutInventory( + isTypingOnly ? Registry.ModContainers.POCKET_COMPUTER_NO_TERM.get() : Registry.ModContainers.POCKET_COMPUTER.get(), id, inventory, + p -> { + ItemStack stack = p.getItemInHand( hand ); + return stack.getItem() == item && ItemPocketComputer.getServerComputer( stack ) == computer; + }, + computer, item.getFamily() + ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java index 84f228f6c..0eb722770 100644 --- a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java +++ b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java @@ -22,7 +22,7 @@ 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 dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider; import net.minecraft.ChatFormatting; import net.minecraft.core.NonNullList; import net.minecraft.nbt.CompoundTag; @@ -154,7 +154,8 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I if( !stop && computer != null ) { - new ComputerContainerData( computer ).open( player, new ContainerPocketComputer.Factory( computer, stack, this, hand ) ); + boolean isTypingOnly = hand == InteractionHand.OFF_HAND; + new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) ); } } return new InteractionResultHolder<>( InteractionResult.SUCCESS, stack ); diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index aa547839d..3d6997141 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -26,6 +26,7 @@ import java.util.Optional; * The turtle API allows you to control your turtle. * * @cc.module turtle + * @cc.since 1.3 */ public class TurtleAPI implements ILuaAPI { @@ -139,6 +140,7 @@ public class TurtleAPI implements ILuaAPI * @return The turtle command result. * @cc.treturn boolean Whether a block was broken. * @cc.treturn string|nil The reason no block was broken. + * @cc.changed 1.6 Added optional side argument. */ @LuaFunction public final MethodResult dig( Optional side ) @@ -154,6 +156,7 @@ public class TurtleAPI implements ILuaAPI * @return The turtle command result. * @cc.treturn boolean Whether a block was broken. * @cc.treturn string|nil The reason no block was broken. + * @cc.changed 1.6 Added optional side argument. */ @LuaFunction public final MethodResult digUp( Optional side ) @@ -169,6 +172,7 @@ public class TurtleAPI implements ILuaAPI * @return The turtle command result. * @cc.treturn boolean Whether a block was broken. * @cc.treturn string|nil The reason no block was broken. + * @cc.changed 1.6 Added optional side argument. */ @LuaFunction public final MethodResult digDown( Optional side ) @@ -189,6 +193,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 */ @LuaFunction public final MethodResult place( IArguments args ) @@ -204,6 +209,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 * @see #place For more information about placing items. */ @LuaFunction @@ -220,6 +226,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 * @see #place For more information about placing items. */ @LuaFunction @@ -237,6 +244,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.31 * @see #select */ @LuaFunction @@ -254,6 +262,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 * @see #select */ @LuaFunction @@ -271,6 +280,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 * @see #select */ @LuaFunction @@ -374,6 +384,7 @@ public class TurtleAPI implements ILuaAPI * * @return If the block and item are equal. * @cc.treturn boolean If the block and item are equal. + * @cc.since 1.31 */ @LuaFunction public final MethodResult compare() @@ -386,6 +397,7 @@ public class TurtleAPI implements ILuaAPI * * @return If the block and item are equal. * @cc.treturn boolean If the block and item are equal. + * @cc.since 1.31 */ @LuaFunction public final MethodResult compareUp() @@ -398,6 +410,7 @@ public class TurtleAPI implements ILuaAPI * * @return If the block and item are equal. * @cc.treturn boolean If the block and item are equal. + * @cc.since 1.31 */ @LuaFunction public final MethodResult compareDown() @@ -412,6 +425,8 @@ public class TurtleAPI implements ILuaAPI * @return The turtle command result. * @cc.treturn boolean Whether an entity was attacked. * @cc.treturn string|nil The reason nothing was attacked. + * @cc.since 1.4 + * @cc.changed 1.6 Added optional side argument. */ @LuaFunction public final MethodResult attack( Optional side ) @@ -426,6 +441,8 @@ public class TurtleAPI implements ILuaAPI * @return The turtle command result. * @cc.treturn boolean Whether an entity was attacked. * @cc.treturn string|nil The reason nothing was attacked. + * @cc.since 1.4 + * @cc.changed 1.6 Added optional side argument. */ @LuaFunction public final MethodResult attackUp( Optional side ) @@ -440,6 +457,8 @@ public class TurtleAPI implements ILuaAPI * @return The turtle command result. * @cc.treturn boolean Whether an entity was attacked. * @cc.treturn string|nil The reason nothing was attacked. + * @cc.since 1.4 + * @cc.changed 1.6 Added optional side argument. */ @LuaFunction public final MethodResult attackDown( Optional side ) @@ -457,6 +476,8 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 + * @cc.changed 1.6 Added an optional limit argument. */ @LuaFunction public final MethodResult suck( Optional count ) throws LuaException @@ -472,6 +493,8 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 + * @cc.changed 1.6 Added an optional limit argument. */ @LuaFunction public final MethodResult suckUp( Optional count ) throws LuaException @@ -487,6 +510,8 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 + * @cc.changed 1.6 Added an optional limit argument. */ @LuaFunction public final MethodResult suckDown( Optional count ) throws LuaException @@ -500,6 +525,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 * @see #getFuelLimit() * @see #refuel(Optional) */ @@ -542,6 +568,7 @@ public class TurtleAPI implements ILuaAPI * local is_fuel, reason = turtle.refuel(0) * if not is_fuel then printError(reason) end * }
+ * @cc.since 1.4 * @see #getFuelLevel() * @see #getFuelLimit() */ @@ -560,6 +587,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.4 */ @LuaFunction public final MethodResult compareTo( int slot ) throws LuaException @@ -576,6 +604,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.45 */ @LuaFunction public final MethodResult transferTo( int slotArg, Optional countArg ) throws LuaException @@ -589,6 +618,7 @@ public class TurtleAPI implements ILuaAPI * Get the currently selected slot. * * @return The current slot. + * @cc.since 1.6 * @see #select */ @LuaFunction @@ -605,6 +635,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.6 * @see #getFuelLevel() * @see #refuel(Optional) */ @@ -625,6 +656,7 @@ public class TurtleAPI implements ILuaAPI * @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. + * @cc.since 1.6 * @see #equipRight() */ @LuaFunction @@ -644,7 +676,8 @@ public class TurtleAPI implements ILuaAPI * @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() + * @cc.since 1.6 + * @see #equipLeft() */ @LuaFunction public final MethodResult equipRight() @@ -658,6 +691,8 @@ public class TurtleAPI implements ILuaAPI * @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.since 1.64 + * @cc.changed 1.76 Added block state to return value. * @cc.usage
{@code
      * local has_block, data = turtle.inspect()
      * if has_block then
@@ -683,6 +718,7 @@ public class TurtleAPI implements ILuaAPI
      * @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.
+     * @cc.since 1.64
      */
     @LuaFunction
     public final MethodResult inspectUp()
@@ -696,6 +732,7 @@ public class TurtleAPI implements ILuaAPI
      * @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.
+     * @cc.since 1.64
      */
     @LuaFunction
     public final MethodResult inspectDown()
@@ -713,6 +750,7 @@ public class TurtleAPI implements ILuaAPI
      * @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.since 1.64
      * @cc.usage Print the current slot, assuming it contains 13 dirt.
      *
      * 
{@code
diff --git a/src/main/java/dan200/computercraft/shared/util/InvisibleSlot.java b/src/main/java/dan200/computercraft/shared/util/InvisibleSlot.java
new file mode 100644
index 000000000..cea2cb706
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/util/InvisibleSlot.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.util;
+
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+
+import javax.annotation.Nonnull;
+
+public class InvisibleSlot extends Slot
+{
+    public InvisibleSlot( Container container, int slot )
+    {
+        super( container, slot, 0, 0 );
+    }
+
+    @Override
+    public boolean mayPlace( @Nonnull ItemStack stack )
+    {
+        return false;
+    }
+
+    @Override
+    public boolean mayPickup( @Nonnull Player player )
+    {
+        return false;
+    }
+
+    @Override
+    public boolean isActive()
+    {
+        return false;
+    }
+}
diff --git a/src/main/resources/assets/computercraft/lang/en_us.json b/src/main/resources/assets/computercraft/lang/en_us.json
index 8fa9209b1..e19fd33e6 100644
--- a/src/main/resources/assets/computercraft/lang/en_us.json
+++ b/src/main/resources/assets/computercraft/lang/en_us.json
@@ -123,9 +123,13 @@
     "gui.computercraft.upload.failed.out_of_space": "Not enough space on the computer for these files.",
     "gui.computercraft.upload.failed.computer_off": "You must turn the computer on before uploading files.",
     "gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.",
+    "gui.computercraft.upload.failed.name_too_long": "File names are too long to be uploaded.",
+    "gui.computercraft.upload.failed.too_many_files": "Cannot upload this many files.",
     "gui.computercraft.upload.failed.overwrite_dir": "Cannot upload %s, as there is already a directory with the same name.",
-    "computercraft.gui.upload.failed.generic": "Uploading files failed (%s)",
+    "gui.computercraft.upload.failed.generic": "Uploading files failed (%s)",
+    "gui.computercraft.upload.failed.corrupted": "Files corrupted when uploading. Please try again.",
     "gui.computercraft.upload.overwrite": "Files would be overwritten",
     "gui.computercraft.upload.overwrite.detail": "The following files will be overwritten when uploading. Continue?%s",
-    "gui.computercraft.upload.overwrite_button": "Overwrite"
+    "gui.computercraft.upload.overwrite_button": "Overwrite",
+    "gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close."
 }
diff --git a/src/main/resources/assets/computercraft/lang/ja_jp.json b/src/main/resources/assets/computercraft/lang/ja_jp.json
index 469245dd2..c006855ed 100644
--- a/src/main/resources/assets/computercraft/lang/ja_jp.json
+++ b/src/main/resources/assets/computercraft/lang/ja_jp.json
@@ -103,7 +103,7 @@
     "gui.computercraft.upload.failed.out_of_space": "これらのファイルに必要なスペースがコンピュータ上にありません。",
     "gui.computercraft.upload.failed.too_much": "アップロードするにはファイルが大きスギます。",
     "gui.computercraft.upload.failed.overwrite_dir": "同じ名前のディレクトリがすでにあるため、%s をアップロードできません。",
-    "computercraft.gui.upload.failed.generic": "ファイルのアップロードに失敗しました(%s)",
+    "gui.computercraft.upload.failed.generic": "ファイルのアップロードに失敗しました(%s)",
     "gui.computercraft.upload.overwrite": "ファイルは上書きされます",
     "gui.computercraft.upload.overwrite_button": "上書き",
     "block.computercraft.wireless_modem_normal": "無線モデム",
diff --git a/src/main/resources/assets/computercraft/lang/ru_ru.json b/src/main/resources/assets/computercraft/lang/ru_ru.json
index e29d1fad5..1c6a51bec 100644
--- a/src/main/resources/assets/computercraft/lang/ru_ru.json
+++ b/src/main/resources/assets/computercraft/lang/ru_ru.json
@@ -124,7 +124,7 @@
     "gui.computercraft.upload.failed.computer_off": "Ты должен включить компьютер перед загрузой файлов.",
     "gui.computercraft.upload.failed.too_much": "Твои файлы слишком большие для загрузки.",
     "gui.computercraft.upload.failed.overwrite_dir": "Нельзя загрузить %s, поскольку папка с таким же названием уже существует.",
-    "computercraft.gui.upload.failed.generic": "Загрузка файлов не удалась (%s)",
+    "gui.computercraft.upload.failed.generic": "Загрузка файлов не удалась (%s)",
     "gui.computercraft.upload.overwrite": "Файлы будут перезаписаны",
     "gui.computercraft.upload.overwrite.detail": "При загрузке следующие файлы будут перезаписаны. Продолжить?%s",
     "gui.computercraft.upload.overwrite_button": "Перезаписать"
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/colors.lua b/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
index ba4050763..7b607757d 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/colors.lua
@@ -204,6 +204,7 @@ black = 0x8000
 --
 -- @tparam number ... The colors to combine.
 -- @treturn number The union of the color sets given in `...`
+-- @since 1.2
 -- @usage
 -- ```lua
 -- colors.combine(colors.white, colors.magenta, colours.lightBlue)
@@ -229,6 +230,7 @@ end
 -- @tparam number colors The color from which to subtract.
 -- @tparam number ... The colors to subtract.
 -- @treturn number The resulting color.
+-- @since 1.2
 -- @usage
 -- ```lua
 -- colours.subtract(colours.lime, colours.orange, colours.white)
@@ -251,6 +253,7 @@ end
 -- @tparam number colors A color, or color set
 -- @tparam number color A color or set of colors that `colors` should contain.
 -- @treturn boolean If `colors` contains all colors within `color`.
+-- @since 1.2
 -- @usage
 -- ```lua
 -- colors.test(colors.combine(colors.white, colors.magenta, colours.lightBlue), colors.lightBlue)
@@ -273,6 +276,7 @@ end
 -- colors.packRGB(0.7, 0.2, 0.6)
 -- -- => 0xb23399
 -- ```
+-- @since 1.81.0
 function packRGB(r, g, b)
     expect(1, r, "number")
     expect(2, g, "number")
@@ -295,6 +299,7 @@ end
 -- -- => 0.7, 0.2, 0.6
 -- ```
 -- @see colors.packRGB
+-- @since 1.81.0
 function unpackRGB(rgb)
     expect(1, rgb, "number")
     return
@@ -325,6 +330,8 @@ end
 -- colors.rgb8(0.7, 0.2, 0.6)
 -- -- => 0xb23399
 -- ```
+-- @since 1.80pr1
+-- @changed 1.81.0 Deprecated in favor of colors.(un)packRGB.
 function rgb8(r, g, b)
     if g == nil and b == nil then
         return unpackRGB(r)
@@ -345,6 +352,7 @@ end
 --
 -- @tparam number color The color to convert.
 -- @treturn string The blit hex code of the color.
+-- @since 1.94.0
 function toBlit(color)
     expect(1, color, "number")
     return color_hex_lookup[color] or
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/colours.lua b/src/main/resources/data/computercraft/lua/rom/apis/colours.lua
index 53d4c5510..287d73de9 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/colours.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/colours.lua
@@ -2,6 +2,7 @@
 --
 -- @see colors
 -- @module colours
+-- @since 1.2
 
 local colours = _ENV
 for k, v in pairs(colors) do
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/disk.lua b/src/main/resources/data/computercraft/lua/rom/apis/disk.lua
index ca5ac21d2..00d691ccc 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/disk.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/disk.lua
@@ -10,6 +10,7 @@
 -- like a disk.
 --
 -- @module disk
+-- @since 1.2
 
 local function isDrive(name)
     if type(name) ~= "string" then
@@ -163,6 +164,7 @@ end
 --
 -- @tparam string name The name of the disk drive.
 -- @treturn string|nil The disk ID, or `nil` if the drive does not contain a floppy disk.
+-- @since 1.4
 function getID(name)
     if isDrive(name) then
         return peripheral.call(name, "getDiskID")
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/gps.lua b/src/main/resources/data/computercraft/lua/rom/apis/gps.lua
index 8cbc63a78..f653166ca 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/gps.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/gps.lua
@@ -21,6 +21,7 @@
 -- [1]: http://www.computercraft.info/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/
 --
 -- @module gps
+-- @since 1.31
 
 local expect = dofile("rom/modules/main/cc/expect.lua").expect
 
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/help.lua b/src/main/resources/data/computercraft/lua/rom/apis/help.lua
index 888c4e78c..503555de2 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/help.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/help.lua
@@ -1,6 +1,7 @@
 --- Provides an API to read help files.
 --
 -- @module help
+-- @since 1.2
 
 local expect = dofile("rom/modules/main/cc/expect.lua").expect
 
@@ -35,6 +36,8 @@ local extensions = { "", ".md", ".txt" }
 -- @treturn string|nil The path to the given topic's help file, or `nil` if it
 -- cannot be found.
 -- @usage help.lookup("disk")
+-- @changed 1.80pr1 Now supports finding .txt files.
+-- @changed 1.97.0 Now supports finding Markdown files.
 function lookup(topic)
     expect(1, topic, "string")
     -- Look on the path variable
@@ -96,6 +99,7 @@ end
 --
 -- @tparam string prefix The prefix to match
 -- @treturn table A list of matching topics.
+-- @since 1.74
 function completeTopic(sText)
     expect(1, sText, "string")
     local tTopics = topics()
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/io.lua b/src/main/resources/data/computercraft/lua/rom/apis/io.lua
index 4d06ccbd1..50ee341e0 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/io.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/io.lua
@@ -65,6 +65,30 @@ handleMetatable = {
             return true
         end,
 
+        --[[- Returns an iterator that, each time it is called, returns a new
+        line from the file.
+
+        This can be used in a for loop to iterate over all lines of a file
+
+        Once the end of the file has been reached, @{nil} will be returned. The file is
+        *not* automatically closed.
+
+        @param ... The argument to pass to @{Handle:read} for each line.
+        @treturn function():string|nil The line iterator.
+        @throws If the file cannot be opened for reading
+        @since 1.3
+
+        @see io.lines
+        @usage Iterate over every line in a file and print it out.
+
+        ```lua
+        local file = io.open("/rom/help/intro.txt")
+        for line in file:lines() do
+          print(line)
+        end
+        file:close()
+        ```
+        ]]
         lines = function(self, ...)
             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
@@ -81,6 +105,23 @@ handleMetatable = {
             end
         end,
 
+        --[[- Reads data from the file, using the specified formats. For each
+        format provided, the function returns either the data read, or `nil` if
+        no data could be read.
+
+        The following formats are available:
+        - `l`: Returns the next line (without a newline on the end).
+        - `L`: Returns the next line (with a newline on the end).
+        - `a`: Returns the entire rest of the file.
+        - ~~`n`: Returns a number~~ (not implemented in CC).
+
+        These formats can be preceded by a `*` to make it compatible with Lua 5.1.
+
+        If no format is provided, `l` is assumed.
+
+        @param ... The formats to use.
+        @treturn (string|nil)... The data read from the file.
+        ]]
         read = function(self, ...)
             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
@@ -124,6 +165,23 @@ handleMetatable = {
             return table.unpack(output, 1, n)
         end,
 
+        --[[- Seeks the file cursor to the specified position, and returns the
+        new position.
+
+        `whence` controls where the seek operation starts, and is a string that
+        may be one of these three values:
+        - `set`: base position is 0 (beginning of the file)
+        - `cur`: base is current position
+        - `end`: base is end of file
+
+        The default value of `whence` is `cur`, and the default value of `offset`
+        is 0. This means that `file:seek()` without arguments returns the current
+        position without moving.
+
+        @tparam[opt] string whence The place to set the cursor from.
+        @tparam[opt] number offset The offset from the start to move to.
+        @treturn number The new location of the file cursor.
+        ]]
         seek = function(self, whence, offset)
             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
@@ -154,6 +212,7 @@ handleMetatable = {
         -- @treturn[1] Handle The current file, allowing chained calls.
         -- @treturn[2] nil If the file could not be written to.
         -- @treturn[2] string The error message which occurred while writing.
+        -- @changed 1.81.0 Multiple arguments are now allowed.
         write = function(self, ...)
             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then
                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2)
@@ -217,6 +276,7 @@ stderr = defaultError
 --
 -- @see Handle:close
 -- @see io.output
+-- @since 1.55
 function close(file)
     if file == nil then return currentOutput:close() end
 
@@ -230,6 +290,7 @@ end
 --
 -- @see Handle:flush
 -- @see io.output
+-- @since 1.55
 function flush()
     return currentOutput:flush()
 end
@@ -239,6 +300,7 @@ end
 -- @tparam[opt] Handle|string file The new input file, either as a file path or pre-existing handle.
 -- @treturn Handle The current input file.
 -- @throws If the provided filename cannot be opened for reading.
+-- @since 1.55
 function input(file)
     if type_of(file) == "string" then
         local res, err = open(file, "rb")
@@ -271,6 +333,7 @@ In this case, the handle is not used.
 
 @see Handle:lines
 @see io.input
+@since 1.55
 @usage Iterate over every line in a file and print it out.
 
 ```lua
@@ -326,6 +389,7 @@ end
 -- @tparam[opt] Handle|string file The new output file, either as a file path or pre-existing handle.
 -- @treturn Handle The current output file.
 -- @throws If the provided filename cannot be opened for writing.
+-- @since 1.55
 function output(file)
     if type_of(file) == "string" then
         local res, err = open(file, "wb")
@@ -374,6 +438,7 @@ end
 -- documentation} there for full details.
 --
 -- @tparam string ... The strings to write
+-- @changed 1.81.0 Multiple arguments are now allowed.
 function write(...)
     return currentOutput:write(...)
 end
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/keys.lua b/src/main/resources/data/computercraft/lua/rom/apis/keys.lua
index 206646c21..aeac84b7a 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/keys.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/keys.lua
@@ -6,6 +6,7 @@
 -- the underlying numerical values.
 --
 -- @module keys
+-- @since 1.4
 
 local expect = dofile("rom/modules/main/cc/expect.lua").expect
 
@@ -76,7 +77,7 @@ tKeys[269] = 'end'
 tKeys[280] = 'capsLock'
 tKeys[281] = 'scrollLock'
 tKeys[282] = 'numLock'
--- tKeys[283] = 'printScreen'
+tKeys[283] = 'printScreen'
 tKeys[284] = 'pause'
 tKeys[290] = 'f1'
 tKeys[291] = 'f2'
@@ -115,7 +116,7 @@ tKeys[328] = 'numPad8'
 tKeys[329] = 'numPad9'
 tKeys[330] = 'numPadDecimal'
 tKeys[331] = 'numPadDivide'
--- tKeys[332] = 'numPadMultiply'
+tKeys[332] = 'numPadMultiply'
 tKeys[333] = 'numPadSubtract'
 tKeys[334] = 'numPadAdd'
 tKeys[335] = 'numPadEnter'
@@ -123,12 +124,12 @@ tKeys[336] = 'numPadEqual'
 tKeys[340] = 'leftShift'
 tKeys[341] = 'leftCtrl'
 tKeys[342] = 'leftAlt'
--- tKeys[343] = 'leftSuper'
+tKeys[343] = 'leftSuper'
 tKeys[344] = 'rightShift'
 tKeys[345] = 'rightCtrl'
 tKeys[346] = 'rightAlt'
 -- tKeys[347] = 'rightSuper'
--- tKeys[348] = 'menu'
+tKeys[348] = 'menu'
 
 local keys = _ENV
 for nKey, sKey in pairs(tKeys) do
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua
index 6eefee8dd..c3a6ce314 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua
@@ -2,6 +2,7 @@
 -- image files. You can use the `colors` API for easier color manipulation.
 --
 -- @module paintutils
+-- @since 1.45
 
 local expect = dofile("rom/modules/main/cc/expect.lua").expect
 
@@ -47,6 +48,7 @@ end
 -- @tparam string image The string containing the raw-image data.
 -- @treturn table The parsed image data, suitable for use with
 -- @{paintutils.drawImage}.
+-- @since 1.80pr1
 function parseImage(image)
     expect(1, image, "string")
     local tImage = {}
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/parallel.lua b/src/main/resources/data/computercraft/lua/rom/apis/parallel.lua
index 00f4b1d3b..78e5abdd8 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/parallel.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/parallel.lua
@@ -13,6 +13,7 @@ etc) can safely be used in one without affecting the event queue accessed by
 the other.
 
 @module parallel
+@since 1.2
 ]]
 
 local function create(...)
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua b/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua
index c22393997..3f42b6510 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua
@@ -15,6 +15,7 @@
 -- determine where given messages should be sent in the first place.
 --
 -- @module rednet
+-- @since 1.2
 
 local expect = dofile("rom/modules/main/cc/expect.lua").expect
 
@@ -80,6 +81,7 @@ end
 -- @tparam[opt] string modem Which modem to check. If not given, all connected
 -- modems will be checked.
 -- @treturn boolean If the given modem is open.
+-- @since 1.31
 function isOpen(modem)
     expect(1, modem, "string", "nil")
     if modem then
@@ -114,6 +116,8 @@ particular protocol.
 @treturn boolean If this message was successfully sent (i.e. if rednet is
 currently @{rednet.open|open}). Note, this does not guarantee the message was
 actually _received_.
+@changed 1.6 Added protocol parameter.
+@changed 1.82.0 Now returns whether the message was successfully sent.
 @see rednet.receive
 @usage Send a message to computer #2.
 
@@ -166,6 +170,7 @@ end
 -- using @{rednet.receive} one can filter to only receive messages sent under a
 -- particular protocol.
 -- @see rednet.receive
+-- @changed 1.6 Added protocol parameter.
 function broadcast(message, sProtocol)
     expect(2, sProtocol, "string", "nil")
     send(CHANNEL_BROADCAST, message, sProtocol)
@@ -185,6 +190,7 @@ received.
 @treturn[2] nil If the timeout elapsed and no message was received.
 @see rednet.broadcast
 @see rednet.send
+@changed 1.6 Added protocol filter parameter.
 @usage Receive a rednet message.
 
     local id, message = rednet.receive()
@@ -262,6 +268,7 @@ end
 -- @throws If trying to register a hostname which is reserved, or currently in use.
 -- @see rednet.unhost
 -- @see rednet.lookup
+-- @since 1.6
 function host(sProtocol, sHostname)
     expect(1, sProtocol, "string")
     expect(2, sHostname, "string")
@@ -280,6 +287,7 @@ end
 -- respond to @{rednet.lookup} requests.
 --
 -- @tparam string sProtocol The protocol to unregister your self from.
+-- @since 1.6
 function unhost(sProtocol)
     expect(1, sProtocol, "string")
     tHostnames[sProtocol] = nil
@@ -299,6 +307,7 @@ end
 -- protocol, or @{nil} if none exist.
 -- @treturn[2] number|nil The computer ID with the provided hostname and protocol,
 -- or @{nil} if none exists.
+-- @since 1.6
 function lookup(sProtocol, sHostname)
     expect(1, sProtocol, "string")
     expect(2, sHostname, "string", "nil")
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/settings.lua b/src/main/resources/data/computercraft/lua/rom/apis/settings.lua
index 1394dec1d..6ef015178 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/settings.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/settings.lua
@@ -5,6 +5,7 @@
 -- `/.settings` file. One can then use @{settings.save} to update the file.
 --
 -- @module settings
+-- @since 1.78
 
 local expect = dofile("rom/modules/main/cc/expect.lua")
 local type, expect, field = type, expect.expect, expect.field
@@ -40,6 +41,7 @@ for _, v in ipairs(valid_types) do valid_types[v] = true end
 --    setting has not been changed.
 --  - `type`: Require values to be of this type. @{set|Setting} the value to another type
 --    will error.
+-- @since 1.87.0
 function define(name, options)
     expect(1, name, "string")
     expect(2, options, "table", "nil")
@@ -67,6 +69,7 @@ end
 -- for that.
 --
 -- @tparam string name The name of this option
+-- @since 1.87.0
 function undefine(name)
     expect(1, name, "string")
     details[name] = nil
@@ -111,6 +114,7 @@ end
 -- this setting. If not given, it will use the setting's default value if given,
 -- or `nil` otherwise.
 -- @return The setting's, or the default if the setting has not been changed.
+-- @changed 1.87.0 Now respects default value if pre-defined and `default` is unset.
 function get(name, default)
     expect(1, name, "string")
     local result = values[name]
@@ -130,6 +134,7 @@ end
 -- @treturn { description? = string, default? = any, type? = string, value? = any }
 -- Information about this setting. This includes all information from @{settings.define},
 -- as well as this setting's value.
+-- @since 1.87.0
 function getDetails(name)
     expect(1, name, "string")
     local deets = copy(details[name]) or {}
@@ -189,6 +194,7 @@ end
 -- corrupted.
 --
 -- @see settings.save
+-- @changed 1.87.0 `sPath` is now optional.
 function load(sPath)
     expect(1, sPath, "string", "nil")
     local file = fs.open(sPath or ".settings", "r")
@@ -226,6 +232,7 @@ end
 -- @treturn boolean If the settings were successfully saved.
 --
 -- @see settings.load
+-- @changed 1.87.0 `sPath` is now optional.
 function save(sPath)
     expect(1, sPath, "string", "nil")
     local file = fs.open(sPath or ".settings", "w")
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/term.lua b/src/main/resources/data/computercraft/lua/rom/apis/term.lua
index 7461a137c..2def0eb50 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/term.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/term.lua
@@ -31,6 +31,7 @@ local term = _ENV
 -- @tparam Redirect target The terminal redirect the @{term} API will draw to.
 -- @treturn Redirect The previous redirect object, as returned by
 -- @{term.current}.
+-- @since 1.31
 -- @usage
 -- Redirect to a monitor on the right of the computer.
 --     term.redirect(peripheral.wrap("right"))
@@ -56,6 +57,7 @@ end
 --- Returns the current terminal object of the computer.
 --
 -- @treturn Redirect The current terminal redirect
+-- @since 1.6
 -- @usage
 -- Create a new @{window} which draws to the current redirect target
 --     window.create(term.current(), 1, 1, 10, 10)
@@ -70,6 +72,7 @@ end
 -- terminal object, and so drawing may interfere with other programs.
 --
 -- @treturn Redirect The native terminal redirect.
+-- @since 1.6
 term.native = function()
     return native
 end
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua
index 855e893c5..aac7d1822 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua
@@ -2,6 +2,7 @@
 -- manipulating strings.
 --
 -- @module textutils
+-- @since 1.2
 
 local expect = dofile("rom/modules/main/cc/expect.lua")
 local expect, field = expect.expect, expect.field
@@ -17,6 +18,7 @@ local wrap = dofile("rom/modules/main/cc/strings.lua").wrap
 -- Defaults to 20.
 -- @usage textutils.slowWrite("Hello, world!")
 -- @usage textutils.slowWrite("Hello, world!", 5)
+-- @since 1.3
 function slowWrite(text, rate)
     expect(2, rate, "number", "nil")
     rate = rate or 20
@@ -55,7 +57,12 @@ end
 -- @tparam[opt] boolean bTwentyFourHour Whether to format this as a 24-hour
 -- clock (`18:30`) rather than a 12-hour one (`6:30 AM`)
 -- @treturn string The formatted time
--- @usage textutils.formatTime(os.time())
+-- @usage Print the current in-game time as a 12-hour clock.
+--
+--     textutils.formatTime(os.time())
+-- @usage Print the local time as a 24-hour clock.
+--
+--     textutils.formatTime(os.time("local"), true)
 function formatTime(nTime, bTwentyFourHour)
     expect(1, nTime, "number")
     expect(2, bTwentyFourHour, "boolean", "nil")
@@ -217,6 +224,7 @@ end
 --
 -- @tparam {string...}|number ... The rows and text colors to display.
 -- @usage textutils.tabulate(colors.orange, { "1", "2", "3" }, colors.lightBlue, { "A", "B", "C" })
+-- @since 1.3
 function tabulate(...)
     return tabulateCommon(false, ...)
 end
@@ -231,6 +239,7 @@ end
 -- @usage textutils.tabulate(colors.orange, { "1", "2", "3" }, colors.lightBlue, { "A", "B", "C" })
 -- @see textutils.tabulate
 -- @see textutils.pagedPrint
+-- @since 1.3
 function pagedTabulate(...)
     return tabulateCommon(true, ...)
 end
@@ -259,11 +268,16 @@ local g_tLuaKeywords = {
     ["while"] = true,
 }
 
+local serialize_infinity = math.huge
 local function serialize_impl(t, tracking, indent, opts)
     local sType = type(t)
     if sType == "table" then
         if tracking[t] ~= nil then
-            error("Cannot serialize table with recursive entries", 0)
+            if tracking[t] == false then
+                error("Cannot serialize table with repeated entries", 0)
+            else
+                error("Cannot serialize table with recursive entries", 0)
+            end
         end
         tracking[t] = true
 
@@ -298,13 +312,28 @@ local function serialize_impl(t, tracking, indent, opts)
             result = result .. indent .. "}"
         end
 
-        if opts.allow_repetitions then tracking[t] = nil end
+        if opts.allow_repetitions then
+            tracking[t] = nil
+        else
+            tracking[t] = false
+        end
         return result
 
     elseif sType == "string" then
         return string.format("%q", t)
 
-    elseif sType == "number" or sType == "boolean" or sType == "nil" then
+    elseif sType == "number" then
+        if t ~= t then --nan
+            return "0/0"
+        elseif t == serialize_infinity then
+            return "1/0"
+        elseif t == -serialize_infinity then
+            return "-1/0"
+        else
+            return tostring(t)
+        end
+
+    elseif sType == "boolean" or sType == "nil" then
         return tostring(t)
 
     else
@@ -620,6 +649,7 @@ do
     -- @return[1] The deserialised object
     -- @treturn[2] nil If the object could not be deserialised.
     -- @treturn string A message describing why the JSON string is invalid.
+    -- @since 1.87.0
     unserialise_json = function(s, options)
         expect(1, s, "string")
         expect(2, options, "table", "nil")
@@ -664,6 +694,8 @@ serialised. This includes functions and tables which appear multiple
 times.
 @see cc.pretty.pretty An alternative way to display a table, often more suitable for
 pretty printing.
+@since 1.3
+@changed 1.97.0 Added `opts` argument.
 @usage Pretty print a basic table.
 
     textutils.serialise({ 1, 2, 3, a = 1, ["another key"] = { true } })
@@ -697,6 +729,7 @@ serialise = serialize -- GB version
 -- @tparam string s The serialised string to deserialise.
 -- @return[1] The deserialised object
 -- @treturn[2] nil If the object could not be deserialised.
+-- @since 1.3
 function unserialize(s)
     expect(1, s, "string")
     local func = load("return " .. s, "unserialize", "t", {})
@@ -729,6 +762,7 @@ unserialise = unserialize -- GB version
 -- serialised. This includes functions and tables which appear multiple
 -- times.
 -- @usage textutils.serializeJSON({ values = { 1, "2", true } })
+-- @since 1.7
 function serializeJSON(t, bNBTStyle)
     expect(1, t, "table", "string", "number", "boolean")
     expect(2, bNBTStyle, "boolean", "nil")
@@ -746,6 +780,7 @@ unserialiseJSON = unserialise_json
 -- @tparam string str The string to encode
 -- @treturn string The encoded string.
 -- @usage print("https://example.com/?view=" .. textutils.urlEncode("some text&things"))
+-- @since 1.31
 function urlEncode(str)
     expect(1, str, "string")
     if str then
@@ -785,6 +820,7 @@ local tEmpty = {}
 -- @see shell.setCompletionFunction
 -- @see _G.read
 -- @usage textutils.complete( "pa", _ENV )
+-- @since 1.74
 function complete(sSearchText, tSearchTable)
     expect(1, sSearchText, "string")
     expect(2, tSearchTable, "table", "nil")
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/vector.lua b/src/main/resources/data/computercraft/lua/rom/apis/vector.lua
index 6f0402ce3..a5227c853 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/vector.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/vector.lua
@@ -5,6 +5,7 @@
 -- [wiki]: http://en.wikipedia.org/wiki/Euclidean_vector
 --
 -- @module vector
+-- @since 1.31
 
 --- A 3-dimensional vector, with `x`, `y`, and `z` values.
 --
diff --git a/src/main/resources/data/computercraft/lua/rom/apis/window.lua b/src/main/resources/data/computercraft/lua/rom/apis/window.lua
index 850ec18e2..feafa00c2 100644
--- a/src/main/resources/data/computercraft/lua/rom/apis/window.lua
+++ b/src/main/resources/data/computercraft/lua/rom/apis/window.lua
@@ -26,6 +26,7 @@
 -- terminal display as its parent, and only one of which is visible at a time.
 --
 -- @module window
+-- @since 1.6
 
 local expect = dofile("rom/modules/main/cc/expect.lua").expect
 
@@ -52,24 +53,40 @@ local type = type
 local string_rep = string.rep
 local string_sub = string.sub
 
---- Returns a terminal object that is a space within the specified parent
--- terminal object. This can then be used (or even redirected to) in the same
--- manner as eg a wrapped monitor. Refer to @{term|the term API} for a list of
--- functions available to it.
---
--- @{term} itself may not be passed as the parent, though @{term.native} is
--- acceptable. Generally, @{term.current} or a wrapped monitor will be most
--- suitable, though windows may even have other windows assigned as their
--- parents.
---
--- @tparam term.Redirect parent The parent terminal redirect to draw to.
--- @tparam number nX The x coordinate this window is drawn at in the parent terminal
--- @tparam number nY The y coordinate this window is drawn at in the parent terminal
--- @tparam number nWidth The width of this window
--- @tparam number nHeight The height of this window
--- @tparam[opt] boolean bStartVisible Whether this window is visible by
--- default. Defaults to `true`.
--- @treturn Window The constructed window
+--[[- Returns a terminal object that is a space within the specified parent
+terminal object. This can then be used (or even redirected to) in the same
+manner as eg a wrapped monitor. Refer to @{term|the term API} for a list of
+functions available to it.
+
+@{term} itself may not be passed as the parent, though @{term.native} is
+acceptable. Generally, @{term.current} or a wrapped monitor will be most
+suitable, though windows may even have other windows assigned as their
+parents.
+
+@tparam term.Redirect parent The parent terminal redirect to draw to.
+@tparam number nX The x coordinate this window is drawn at in the parent terminal
+@tparam number nY The y coordinate this window is drawn at in the parent terminal
+@tparam number nWidth The width of this window
+@tparam number nHeight The height of this window
+@tparam[opt] boolean bStartVisible Whether this window is visible by
+default. Defaults to `true`.
+@treturn Window The constructed window
+@since 1.6
+@usage Create a smaller window, fill it red and write some text to it.
+
+    local my_window = window.create(term.current(), 1, 1, 20, 5)
+    my_window.setBackgroundColour(colours.red)
+    my_window.setTextColour(colours.white)
+    my_window.clear()
+    my_window.write("Testing my window!")
+
+@usage Create a smaller window and redirect to it.
+
+    local my_window = window.create(term.current(), 1, 1, 25, 5)
+    term.redirect(my_window)
+    print("Writing some long text which will wrap around and show the bounds of this window.")
+
+]]
 function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
     expect(1, parent, "table")
     expect(2, nX, "number")
@@ -446,6 +463,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
     -- @treturn string The text colours of this line, suitable for use with @{term.blit}.
     -- @treturn string The background colours of this line, suitable for use with @{term.blit}.
     -- @throws If `y` is not between 1 and this window's height.
+    -- @since 1.84.0
     function window.getLine(y)
         if type(y) ~= "number" then expect(1, y, "number") end
 
@@ -479,6 +497,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
     --
     -- @treturn boolean Whether this window is visible.
     -- @see Window:setVisible
+    -- @since 1.94.0
     function window.isVisible()
         return bVisible
     end
@@ -525,6 +544,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
     -- @tparam number new_height The new height of this window.
     -- @tparam[opt] term.Redirect new_parent The new redirect object this
     -- window should draw to.
+    -- @changed 1.85.0 Add `new_parent` parameter.
     function window.reposition(new_x, new_y, new_width, new_height, new_parent)
         if type(new_x) ~= "number" then expect(1, new_x, "number") end
         if type(new_y) ~= "number" then expect(2, new_y, "number") end
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/completion.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/completion.lua
index 6315a97cc..ade5988ae 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/completion.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/completion.lua
@@ -4,6 +4,7 @@
 -- @module cc.completion
 -- @see cc.shell.completion For additional helpers to use with
 -- @{shell.setCompletionFunction}.
+-- @since 1.85.0
 
 local expect = require "cc.expect".expect
 
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/expect.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/expect.lua
index 31ea4b0ad..e5c5ebcbf 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/expect.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/expect.lua
@@ -2,6 +2,8 @@
 function arguments are well-formed and of the correct type.
 
 @module cc.expect
+@since 1.84.0
+@changed 1.96.0 The module can now be called directly as a function, which wraps around `expect.expect`.
 @usage Define a basic function and check it has the correct arguments.
 
     local expect = require "cc.expect"
@@ -97,6 +99,7 @@ end
 -- @tparam number max The maximum value, if nil then `math.huge` is used.
 -- @return The given `value`.
 -- @throws If the value is outside of the allowed range.
+-- @since 1.96.0
 local function range(num, min, max)
   expect(1, num, "number")
   min = expect(2, min, "number", "nil") or -math.huge
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua
index 225abb6f7..410934c5d 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/image/nft.lua
@@ -5,6 +5,7 @@
 -- text.
 --
 -- @module cc.image.nft
+-- @since 1.90.0
 -- @usage Load an image from `example.nft` and draw it.
 --
 --     local nft = require "cc.image.nft"
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua
index 874703a54..f5cdeee6c 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/pretty.lua
@@ -15,6 +15,7 @@ The structure of this module is based on [A Prettier Printer][prettier].
 [prettier]: https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf "A Prettier Printer"
 
 @module cc.pretty
+@since 1.87.0
 @usage Print a table to the terminal
 
     local pretty = require "cc.pretty"
@@ -457,6 +458,7 @@ end
 --  - `function_source`: Show where the function was defined, instead of
 --    `function: xxxxxxxx` (`false` by default).
 -- @treturn Doc The object formatted as a document.
+-- @changed 1.88.0 Added `options` argument.
 -- @usage Display a table on the screen
 --
 --     local pretty = require "cc.pretty"
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/require.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/require.lua
index c7b2e2741..24c27b15c 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/require.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/require.lua
@@ -6,6 +6,7 @@
 -- custom shell or when running programs yourself.
 --
 -- @module cc.require
+-- @since 1.88.0
 -- @usage Construct the package and require function, and insert them into a
 -- custom environment.
 --
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua
index 5b2c21552..4bdc24674 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/shell/completion.lua
@@ -8,6 +8,7 @@ and so are not directly usable with the @{shell.setCompletionFunction}. Instead,
 wrap them using @{build}, or your own custom function.
 
 @module cc.shell.completion
+@since 1.85.0
 @see cc.completion For more general helpers, suitable for use with @{_G.read}.
 @see shell.setCompletionFunction
 
@@ -89,6 +90,7 @@ end
 -- @tparam { string... } previous The shell arguments before this one.
 -- @tparam number starting Which argument index this program and args start at.
 -- @treturn { string... } A list of suffixes of matching programs or arguments.
+-- @since 1.97.0
 local function programWithArgs(shell, text, previous, starting)
     if #previous + 1 == starting then
         local tCompletionInfo = shell.getCompletionInfo()
diff --git a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/strings.lua b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/strings.lua
index fa149733d..6c9da0d9f 100644
--- a/src/main/resources/data/computercraft/lua/rom/modules/main/cc/strings.lua
+++ b/src/main/resources/data/computercraft/lua/rom/modules/main/cc/strings.lua
@@ -1,6 +1,7 @@
 --- Various utilities for working with strings and text.
 --
 -- @module cc.strings
+-- @since 1.95.0
 -- @see textutils For additional string related utilities.
 
 local expect = (require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua")).expect
diff --git a/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua b/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua
index 6465f9f7a..59de7153d 100644
--- a/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua
+++ b/src/main/resources/data/computercraft/lua/rom/programs/advanced/multishell.lua
@@ -15,6 +15,7 @@
 -- not available to @{os.loadAPI|APIs}.
 --
 -- @module[module] multishell
+-- @since 1.6
 
 local expect = dofile("rom/modules/main/cc/expect.lua").expect
 
diff --git a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua
index 1cc6dfe48..f24c0c4fc 100644
--- a/src/main/resources/data/computercraft/lua/rom/programs/shell.lua
+++ b/src/main/resources/data/computercraft/lua/rom/programs/shell.lua
@@ -56,6 +56,7 @@ end
 -- @tparam string command The program to execute.
 -- @tparam string ... Arguments to this program.
 -- @treturn boolean Whether the program exited successfully.
+-- @since 1.88.0
 -- @usage Run `paint my-image` from within your program:
 --
 --     shell.execute("paint", "my-image")
@@ -131,6 +132,8 @@ end
 --
 --     shell.run("paint", "my-image")
 -- @see shell.execute Run a program directly without parsing the arguments.
+-- @changed 1.80pr1 Programs now get their own environment instead of sharing the same one.
+-- @changed 1.83.0 `arg` is now added to the environment.
 function shell.run(...)
     local tWords = tokenise(...)
     local sCommand = tWords[1]
@@ -193,6 +196,7 @@ end
 -- for from the @{shell.dir|current directory}, rather than the computer's root.
 --
 -- @tparam string path The new program path.
+-- @since 1.2
 function shell.setPath(path)
     expect(1, path, "string")
     sPath = path
@@ -235,6 +239,7 @@ end
 -- @tparam string command The name of the program
 -- @treturn string|nil The absolute path to the program, or @{nil} if it could
 -- not be found.
+-- @since 1.2
 -- @usage Locate the `hello` program.
 --
 --      shell.resolveProgram("hello")
@@ -283,6 +288,7 @@ end
 -- start with `.`.
 -- @treturn { string } A list of available programs.
 -- @usage textutils.tabulate(shell.programs())
+-- @since 1.2
 function shell.programs(include_hidden)
     expect(1, include_hidden, "boolean", "nil")
 
@@ -387,6 +393,7 @@ end
 -- @see shell.completeProgram
 -- @see shell.setCompletionFunction
 -- @see shell.getCompletionInfo
+-- @since 1.74
 function shell.complete(sLine)
     expect(1, sLine, "string")
     if #sLine > 0 then
@@ -462,6 +469,7 @@ end
 -- @see cc.shell.completion Various utilities to help with writing completion functions.
 -- @see shell.complete
 -- @see _G.read For more information about completion.
+-- @since 1.74
 function shell.setCompletionFunction(program, complete)
     expect(1, program, "string")
     expect(2, complete, "function")
@@ -484,6 +492,7 @@ end
 --- Returns the path to the currently running program.
 --
 -- @treturn string The absolute path to the running program.
+-- @since 1.3
 function shell.getRunningProgram()
     if #tProgramStack > 0 then
         return tProgramStack[#tProgramStack]
@@ -495,6 +504,7 @@ end
 --
 -- @tparam string command The name of the alias to add.
 -- @tparam string program The name or path to the program.
+-- @since 1.2
 -- @usage Alias `vim` to the `edit` program
 --
 --     shell.setAlias("vim", "edit")
@@ -543,6 +553,7 @@ if multishell then
     -- @tparam string ... The command line to run.
     -- @see shell.run
     -- @see multishell.launch
+    -- @since 1.6
     -- @usage Launch the Lua interpreter and switch to it.
     --
     --     local id = shell.openTab("lua")
@@ -566,6 +577,7 @@ if multishell then
     --
     -- @tparam number id The tab to switch to.
     -- @see multishell.setFocus
+    -- @since 1.6
     function shell.switchTab(id)
         expect(1, id, "number")
         multishell.setFocus(id)
diff --git a/src/test/resources/test-rom/spec/apis/textutils_spec.lua b/src/test/resources/test-rom/spec/apis/textutils_spec.lua
index b8137414a..71f426366 100644
--- a/src/test/resources/test-rom/spec/apis/textutils_spec.lua
+++ b/src/test/resources/test-rom/spec/apis/textutils_spec.lua
@@ -81,14 +81,17 @@ describe("The textutils library", function()
         it("serialises basic tables", function()
             expect(textutils.serialise({ 1, 2, 3, a = 1, b = {} }))
                 :eq("{\n  1,\n  2,\n  3,\n  a = 1,\n  b = {},\n}")
+
+            expect(textutils.serialise({ 0 / 0, 1 / 0, -1 / 0 }))
+                :eq("{\n  0/0,\n  1/0,\n  -1/0,\n}")
         end)
 
-        it("fails on recursive tables", function()
+        it("fails on recursive/repeated tables", function()
             local rep = {}
-            expect.error(textutils.serialise, { rep, rep }):eq("Cannot serialize table with recursive entries")
+            expect.error(textutils.serialise, { rep, rep }):eq("Cannot serialize table with repeated entries")
 
             local rep2 = { 1 }
-            expect.error(textutils.serialise, { rep2, rep2 }):eq("Cannot serialize table with recursive entries")
+            expect.error(textutils.serialise, { rep2, rep2 }):eq("Cannot serialize table with repeated entries")
 
             local recurse = {}
             recurse[1] = recurse