diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 38142dbea..0dbdaf69c 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -16,7 +16,7 @@ jobs: java-version: 8 - name: Cache gradle dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('gradle.properties') }} diff --git a/build.gradle b/build.gradle index 0be21ae8c..c6287584c 100644 --- a/build.gradle +++ b/build.gradle @@ -55,8 +55,8 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' - modRuntime "me.shedaniel:RoughlyEnoughItems-api:5.2.10" - modRuntime "me.shedaniel:RoughlyEnoughItems:5.2.10" + modRuntime "me.shedaniel:RoughlyEnoughItems-api:5.8.9" + modRuntime "me.shedaniel:RoughlyEnoughItems:5.8.9" } sourceSets { diff --git a/doc/events/alarm.md b/doc/events/alarm.md new file mode 100644 index 000000000..db7f04845 --- /dev/null +++ b/doc/events/alarm.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] alarm +see: os.setAlarm To start an alarm. +--- + +The @{timer} event is fired when an alarm started with @{os.setAlarm} completes. + +## Return Values +1. @{string}: The event name. +2. @{number}: The ID of the alarm that finished. + +## Example +Starts a timer and then prints its ID: +```lua +local alarmID = os.setAlarm(os.time() + 0.05) +local event, id +repeat + event, id = os.pullEvent("alarm") +until id == alarmID +print("Alarm with ID " .. id .. " was fired") +``` diff --git a/doc/events/char.md b/doc/events/char.md new file mode 100644 index 000000000..473313702 --- /dev/null +++ b/doc/events/char.md @@ -0,0 +1,24 @@ +--- +module: [kind=event] char +see: key To listen to any key press. +--- + +The @{char} event is fired when a character is _typed_ on the keyboard. + +The @{char} event is different to a key press. Sometimes multiple key presses may result in one character being +typed (for instance, on some European keyboards). Similarly, some keys (e.g. Ctrl) do not have any +corresponding character. The @{key} should be used if you want to listen to key presses themselves. + +## Return values +1. @{string}: The event name. +2. @{string}: The string representing the character that was pressed. + + +## Example +Prints each character the user presses: +```lua +while true do + local event, character = os.pullEvent("char") + print(character .. " was pressed.") +end +``` diff --git a/doc/events/computer_command.md b/doc/events/computer_command.md new file mode 100644 index 000000000..245252399 --- /dev/null +++ b/doc/events/computer_command.md @@ -0,0 +1,18 @@ +--- +module: [kind=event] computer_command +--- + +The @{computer_command} event is fired when the `/computercraft queue` command is run for the current computer. + +## Return Values +1. @{string}: The event name. +... @{string}: The arguments passed to the command. + +## Example +Prints the contents of messages sent: +```lua +while true do + local event = {os.pullEvent("computer_command")} + print("Received message:", table.unpack(event, 2)) +end +``` diff --git a/doc/events/disk.md b/doc/events/disk.md new file mode 100644 index 000000000..2946d70c4 --- /dev/null +++ b/doc/events/disk.md @@ -0,0 +1,19 @@ +--- +module: [kind=event] disk +see: disk_eject For the event sent when a disk is removed. +--- + +The @{disk} event is fired when a disk is inserted into an adjacent or networked disk drive. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side of the disk drive that had a disk inserted. + +## Example +Prints a message when a disk is inserted: +```lua +while true do + local event, side = os.pullEvent("disk") + print("Inserted a disk on side " .. side) +end +``` diff --git a/doc/events/disk_eject.md b/doc/events/disk_eject.md new file mode 100644 index 000000000..71c3ede0a --- /dev/null +++ b/doc/events/disk_eject.md @@ -0,0 +1,19 @@ +--- +module: [kind=event] disk_eject +see: disk For the event sent when a disk is inserted. +--- + +The @{disk_eject} event is fired when a disk is removed from an adjacent or networked disk drive. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side of the disk drive that had a disk removed. + +## Example +Prints a message when a disk is removed: +```lua +while true do + local event, side = os.pullEvent("disk_eject") + print("Removed a disk on side " .. side) +end +``` diff --git a/doc/events/http_check.md b/doc/events/http_check.md new file mode 100644 index 000000000..9af5ea7ca --- /dev/null +++ b/doc/events/http_check.md @@ -0,0 +1,14 @@ +--- +module: [kind=event] http_check +see: http.checkURLAsync To check a URL asynchronously. +--- + +The @{http_check} event is fired when a URL check finishes. + +This event is normally handled inside @{http.checkURL}, but it can still be seen when using @{http.checkURLAsync}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL requested to be checked. +3. @{boolean}: Whether the check succeeded. +4. @{string|nil}: If the check failed, a reason explaining why the check failed. diff --git a/doc/events/http_failure.md b/doc/events/http_failure.md new file mode 100644 index 000000000..dc10b40d7 --- /dev/null +++ b/doc/events/http_failure.md @@ -0,0 +1,39 @@ +--- +module: [kind=event] http_failure +see: http.request To send an HTTP request. +--- + +The @{http_failure} event is fired when an HTTP request fails. + +This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the site requested. +3. @{string}: An error describing the failure. +4. @{http.Response|nil}: A response handle if the connection succeeded, but the server's response indicated failure. + +## Example +Prints an error why the website cannot be contacted: +```lua +local myURL = "https://does.not.exist.tweaked.cc" +http.request(myURL) +local event, url, err +repeat + event, url, err = os.pullEvent("http_failure") +until url == myURL +print("The URL " .. url .. " could not be reached: " .. err) +``` + +Prints the contents of a webpage that does not exist: +```lua +local myURL = "https://tweaked.cc/this/does/not/exist" +http.request(myURL) +local event, url, err, handle +repeat + event, url, err, handle = os.pullEvent("http_failure") +until url == myURL +print("The URL " .. url .. " could not be reached: " .. err) +print(handle.getResponseCode()) +handle.close() +``` diff --git a/doc/events/http_success.md b/doc/events/http_success.md new file mode 100644 index 000000000..3700b9211 --- /dev/null +++ b/doc/events/http_success.md @@ -0,0 +1,27 @@ +--- +module: [kind=event] http_success +see: http.request To make an HTTP request. +--- + +The @{http_success} event is fired when an HTTP request returns successfully. + +This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the site requested. +3. @{http.Response}: The handle for the response text. + +## Example +Prints the content of a website (this may fail if the request fails): +```lua +local myURL = "https://tweaked.cc/" +http.request(myURL) +local event, url, handle +repeat + event, url, handle = os.pullEvent("http_success") +until url == myURL +print("Contents of " .. url .. ":") +print(handle.readAll()) +handle.close() +``` diff --git a/doc/events/key.md b/doc/events/key.md new file mode 100644 index 000000000..59598d8dd --- /dev/null +++ b/doc/events/key.md @@ -0,0 +1,26 @@ +--- +module: [kind=event] key +--- + +This event is fired when any key is pressed while the terminal is focused. + +This event returns a numerical "key code" (for instance, F1 is 290). This value may vary between versions and +so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values. + +If the button pressed represented a printable character, then the @{key} event will be followed immediately by a @{char} +event. If you are consuming text input, use a @{char} event instead! + +## Return values +1. @{string}: The event name. +2. @{number}: The numerical key value of the key pressed. +3. @{boolean}: Whether the key event was generated while holding the key (@{true}), rather than pressing it the first time (@{false}). + +## Example +Prints each key when the user presses it, and if the key is being held. + +```lua +while true do + local event, key, is_held = os.pullEvent("key") + print(("%s held=%b"):format(keys.getName(key), is_held)) +end +``` diff --git a/doc/events/key_up.md b/doc/events/key_up.md new file mode 100644 index 000000000..e957cae6b --- /dev/null +++ b/doc/events/key_up.md @@ -0,0 +1,24 @@ +--- +module: [kind=event] key_up +see: keys For a lookup table of the given keys. +--- + +Fired whenever a key is released (or the terminal is closed while a key was being pressed). + +This event returns a numerical "key code" (for instance, F1 is 290). This value may vary between versions and +so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values. + +## Return values +1. @{string}: The event name. +2. @{number}: The numerical key value of the key pressed. + +## Example +Prints each key released on the keyboard whenever a @{key_up} event is fired. + +```lua +while true do + local event, key = os.pullEvent("key_up") + local name = keys.getName(key) or "unknown key" + print(name .. " was released.") +end +``` diff --git a/doc/events/modem_message.md b/doc/events/modem_message.md new file mode 100644 index 000000000..ec619f3d4 --- /dev/null +++ b/doc/events/modem_message.md @@ -0,0 +1,22 @@ +--- +module: [kind=event] modem_message +--- + +The @{modem_message} event is fired when a message is received on an open channel on any modem. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side of the modem that received the message. +3. @{number}: The channel that the message was sent on. +4. @{number}: The reply channel set by the sender. +5. @{any}: The message as sent by the sender. +6. @{number}: The distance between the sender and the receiver, in blocks (decimal). + +## Example +Prints a message when one is sent: +```lua +while true do + local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message") + print(("Message received on side %s on channel %d (reply to %d) from %f blocks away with message %s"):format(side, channel, replyChannel, distance, tostring(message))) +end +``` diff --git a/doc/events/monitor_resize.md b/doc/events/monitor_resize.md new file mode 100644 index 000000000..03de804e7 --- /dev/null +++ b/doc/events/monitor_resize.md @@ -0,0 +1,18 @@ +--- +module: [kind=event] monitor_resize +--- + +The @{monitor_resize} event is fired when an adjacent or networked monitor's size is changed. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side or network ID of the monitor that resized. + +## Example +Prints a message when a monitor is resized: +```lua +while true do + local event, side = os.pullEvent("monitor_resize") + print("The monitor on side " .. side .. " was resized.") +end +``` diff --git a/doc/events/monitor_touch.md b/doc/events/monitor_touch.md new file mode 100644 index 000000000..0f27a9cc1 --- /dev/null +++ b/doc/events/monitor_touch.md @@ -0,0 +1,20 @@ +--- +module: [kind=event] monitor_touch +--- + +The @{monitor_touch} event is fired when an adjacent or networked Advanced Monitor is right-clicked. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side or network ID of the monitor that was touched. +3. @{number}: The X coordinate of the touch, in characters. +4. @{number}: The Y coordinate of the touch, in characters. + +## Example +Prints a message when a monitor is touched: +```lua +while true do + local event, side, x, y = os.pullEvent("monitor_touch") + print("The monitor on side " .. side .. " was touched at (" .. x .. ", " .. y .. ")") +end +``` diff --git a/doc/events/mouse_click.md b/doc/events/mouse_click.md new file mode 100644 index 000000000..83d371260 --- /dev/null +++ b/doc/events/mouse_click.md @@ -0,0 +1,34 @@ +--- +module: [kind=event] mouse_click +--- + +This event is fired when the terminal is clicked with a mouse. This event is only fired on advanced computers (including +advanced turtles and pocket computers). + +## Return values +1. @{string}: The event name. +2. @{number}: The mouse button that was clicked. +3. @{number}: The X-coordinate of the click. +4. @{number}: The Y-coordinate of the click. + +## Mouse buttons +Several mouse events (@{mouse_click}, @{mouse_up}, @{mouse_scroll}) contain a "mouse button" code. This takes a +numerical value depending on which button on your mouse was last pressed when this event occurred. + + + + + + + +
Button codeMouse button
1Left button
2Middle button
3Right button
+ +## Example +Print the button and the coordinates whenever the mouse is clicked. + +```lua +while true do + local event, button, x, y = os.pullEvent("mouse_click") + print(("The mouse button %s was pressed at %d, %d"):format(button, x, y)) +end +``` diff --git a/doc/events/mouse_drag.md b/doc/events/mouse_drag.md new file mode 100644 index 000000000..15451c9f8 --- /dev/null +++ b/doc/events/mouse_drag.md @@ -0,0 +1,22 @@ +--- +module: [kind=event] mouse_drag +see: mouse_click For when a mouse button is initially pressed. +--- + +This event is fired every time the mouse is moved while a mouse button is being held. + +## Return values +1. @{string}: The event name. +2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed. +3. @{number}: The X-coordinate of the mouse. +4. @{number}: The Y-coordinate of the mouse. + +## Example +Print the button and the coordinates whenever the mouse is dragged. + +```lua +while true do + local event, button, x, y = os.pullEvent("mouse_drag") + print(("The mouse button %s was dragged at %d, %d"):format(button, x, y)) +end +``` diff --git a/doc/events/mouse_scroll.md b/doc/events/mouse_scroll.md new file mode 100644 index 000000000..6248220a5 --- /dev/null +++ b/doc/events/mouse_scroll.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] mouse_scroll +--- + +This event is fired when a mouse wheel is scrolled in the terminal. + +## Return values +1. @{string}: The event name. +2. @{number}: The direction of the scroll. (-1 = up, 1 = down) +3. @{number}: The X-coordinate of the mouse when scrolling. +4. @{number}: The Y-coordinate of the mouse when scrolling. + +## Example +Prints the direction of each scroll, and the position of the mouse at the time. + +```lua +while true do + local event, dir, x, y = os.pullEvent("mouse_scroll") + print(("The mouse was scrolled in direction %s at %d, %d"):format(dir, x, y)) +end +``` diff --git a/doc/events/mouse_up.md b/doc/events/mouse_up.md new file mode 100644 index 000000000..886330a6d --- /dev/null +++ b/doc/events/mouse_up.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] mouse_up +--- + +This event is fired when a mouse button is released or a held mouse leaves the computer's terminal. + +## Return values +1. @{string}: The event name. +2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that was released. +3. @{number}: The X-coordinate of the mouse. +4. @{number}: The Y-coordinate of the mouse. + +## Example +Prints the coordinates and button number whenever the mouse is released. + +```lua +while true do + local event, button, x, y = os.pullEvent("mouse_up") + print(("The mouse button %s was released at %d, %d"):format(button, x, y)) +end +``` diff --git a/doc/events/paste.md b/doc/events/paste.md new file mode 100644 index 000000000..b4f8713c5 --- /dev/null +++ b/doc/events/paste.md @@ -0,0 +1,18 @@ +--- +module: [kind=event] paste +--- + +The @{paste} event is fired when text is pasted into the computer through Ctrl-V (or ⌘V on Mac). + +## Return values +1. @{string}: The event name. +2. @{string} The text that was pasted. + +## Example +Prints pasted text: +```lua +while true do + local event, text = os.pullEvent("paste") + print('"' .. text .. '" was pasted') +end +``` diff --git a/doc/events/peripheral.md b/doc/events/peripheral.md new file mode 100644 index 000000000..5769f3942 --- /dev/null +++ b/doc/events/peripheral.md @@ -0,0 +1,19 @@ +--- +module: [kind=event] peripheral +see: peripheral_detach For the event fired when a peripheral is detached. +--- + +The @{peripheral} event is fired when a peripheral is attached on a side or to a modem. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side the peripheral was attached to. + +## Example +Prints a message when a peripheral is attached: +```lua +while true do + local event, side = os.pullEvent("peripheral") + print("A peripheral was attached on side " .. side) +end +``` diff --git a/doc/events/peripheral_detach.md b/doc/events/peripheral_detach.md new file mode 100644 index 000000000..c8a462cf0 --- /dev/null +++ b/doc/events/peripheral_detach.md @@ -0,0 +1,19 @@ +--- +module: [kind=event] peripheral_detach +see: peripheral For the event fired when a peripheral is attached. +--- + +The @{peripheral_detach} event is fired when a peripheral is detached from a side or from a modem. + +## Return Values +1. @{string}: The event name. +2. @{string}: The side the peripheral was detached from. + +## Example +Prints a message when a peripheral is detached: +```lua +while true do + local event, side = os.pullEvent("peripheral_detach") + print("A peripheral was detached on side " .. side) +end +``` diff --git a/doc/events/rednet_message.md b/doc/events/rednet_message.md new file mode 100644 index 000000000..8d0bdf697 --- /dev/null +++ b/doc/events/rednet_message.md @@ -0,0 +1,30 @@ +--- +module: [kind=event] rednet_message +see: modem_message For raw modem messages sent outside of Rednet. +see: rednet.receive To wait for a Rednet message with an optional timeout and protocol filter. +--- + +The @{rednet_message} event is fired when a message is sent over Rednet. + +This event is usually handled by @{rednet.receive}, but it can also be pulled manually. + +@{rednet_message} events are sent by @{rednet.run} in the top-level coroutine in response to @{modem_message} events. A @{rednet_message} event is always preceded by a @{modem_message} event. They are generated inside CraftOS rather than being sent by the ComputerCraft machine. + +## Return Values +1. @{string}: The event name. +2. @{number}: The ID of the sending computer. +3. @{any}: The message sent. +4. @{string|nil}: The protocol of the message, if provided. + +## Example +Prints a message when one is sent: +```lua +while true do + local event, sender, message, protocol = os.pullEvent("rednet_message") + if protocol ~= nil then + print("Received message from " .. sender .. " with protocol " .. protocol .. " and message " .. tostring(message)) + else + print("Received message from " .. sender .. " with message " .. tostring(message)) + end +end +``` diff --git a/doc/events/redstone.md b/doc/events/redstone.md new file mode 100644 index 000000000..44eda304a --- /dev/null +++ b/doc/events/redstone.md @@ -0,0 +1,14 @@ +--- +module: [kind=event] redstone +--- + +The @{redstone} event is fired whenever any redstone inputs on the computer change. + +## Example +Prints a message when a redstone input changes: +```lua +while true do + os.pullEvent("redstone") + print("A redstone input has changed!") +end +``` diff --git a/doc/events/task_complete.md b/doc/events/task_complete.md new file mode 100644 index 000000000..eddec51d2 --- /dev/null +++ b/doc/events/task_complete.md @@ -0,0 +1,28 @@ +--- +module: [kind=event] task_complete +see: commands.execAsync To run a command which fires a task_complete event. +--- + +The @{task_complete} event is fired when an asynchronous task completes. This is usually handled inside the function call that queued the task; however, functions such as @{commands.execAsync} return immediately so the user can wait for completion. + +## Return Values +1. @{string}: The event name. +2. @{number}: The ID of the task that completed. +3. @{boolean}: Whether the command succeeded. +4. @{string}: If the command failed, an error message explaining the failure. (This is not present if the command succeeded.) +...: Any parameters returned from the command. + +## Example +Prints the results of an asynchronous command: +```lua +local taskID = commands.execAsync("say Hello") +local event +repeat + event = {os.pullEvent("task_complete")} +until event[2] == taskID +if event[3] == true then + print("Task " .. event[2] .. " succeeded:", table.unpack(event, 4)) +else + print("Task " .. event[2] .. " failed: " .. event[4]) +end +``` diff --git a/doc/events/term_resize.md b/doc/events/term_resize.md new file mode 100644 index 000000000..0eb503bad --- /dev/null +++ b/doc/events/term_resize.md @@ -0,0 +1,15 @@ +--- +module: [kind=event] term_resize +--- + +The @{term_resize} event is fired when the main terminal is resized, mainly when a new tab is opened or closed in @{multishell}. + +## Example +Prints : +```lua +while true do + os.pullEvent("term_resize") + local w, h = term.getSize() + print("The term was resized to (" .. w .. ", " .. h .. ")") +end +``` diff --git a/doc/events/terminate.md b/doc/events/terminate.md new file mode 100644 index 000000000..0760b8c3b --- /dev/null +++ b/doc/events/terminate.md @@ -0,0 +1,25 @@ +--- +module: [kind=event] terminate +--- + +The @{terminate} event is fired when Ctrl-T is held down. + +This event is normally handled by @{os.pullEvent}, and will not be returned. However, @{os.pullEventRaw} will return this event when fired. + +@{terminate} will be sent even when a filter is provided to @{os.pullEventRaw}. When using @{os.pullEventRaw} with a filter, make sure to check that the event is not @{terminate}. + +## Example +Prints a message when Ctrl-T is held: +```lua +while true do + local event = os.pullEventRaw("terminate") + if event == "terminate" then print("Terminate requested!") end +end +``` + +Exits when Ctrl-T is held: +```lua +while true do + os.pullEvent() +end +``` diff --git a/doc/events/timer.md b/doc/events/timer.md new file mode 100644 index 000000000..c359c37b4 --- /dev/null +++ b/doc/events/timer.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] timer +see: os.startTimer To start a timer. +--- + +The @{timer} event is fired when a timer started with @{os.startTimer} completes. + +## Return Values +1. @{string}: The event name. +2. @{number}: The ID of the timer that finished. + +## Example +Starts a timer and then prints its ID: +```lua +local timerID = os.startTimer(2) +local event, id +repeat + event, id = os.pullEvent("timer") +until id == timerID +print("Timer with ID " .. id .. " was fired") +``` diff --git a/doc/events/turtle_inventory.md b/doc/events/turtle_inventory.md new file mode 100644 index 000000000..bc9392b6b --- /dev/null +++ b/doc/events/turtle_inventory.md @@ -0,0 +1,14 @@ +--- +module: [kind=event] turtle_inventory +--- + +The @{turtle_inventory} event is fired when a turtle's inventory is changed. + +## Example +Prints a message when the inventory is changed: +```lua +while true do + os.pullEvent("turtle_inventory") + print("The inventory was changed.") +end +``` diff --git a/doc/events/websocket_closed.md b/doc/events/websocket_closed.md new file mode 100644 index 000000000..9e3783d19 --- /dev/null +++ b/doc/events/websocket_closed.md @@ -0,0 +1,21 @@ +--- +module: [kind=event] websocket_closed +--- + +The @{websocket_closed} event is fired when an open WebSocket connection is closed. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the WebSocket that was closed. + +## Example +Prints a message when a WebSocket is closed (this may take a minute): +```lua +local myURL = "wss://example.tweaked.cc/echo" +local ws = http.websocket(myURL) +local event, url +repeat + event, url = os.pullEvent("websocket_closed") +until url == myURL +print("The WebSocket at " .. url .. " was closed.") +``` diff --git a/doc/events/websocket_failure.md b/doc/events/websocket_failure.md new file mode 100644 index 000000000..eef34e777 --- /dev/null +++ b/doc/events/websocket_failure.md @@ -0,0 +1,25 @@ +--- +module: [kind=event] websocket_failure +see: http.websocketAsync To send an HTTP request. +--- + +The @{websocket_failure} event is fired when a WebSocket connection request fails. + +This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the site requested. +3. @{string}: An error describing the failure. + +## Example +Prints an error why the website cannot be contacted: +```lua +local myURL = "wss://example.tweaked.cc/not-a-websocket" +http.websocketAsync(myURL) +local event, url, err +repeat + event, url, err = os.pullEvent("websocket_failure") +until url == myURL +print("The URL " .. url .. " could not be reached: " .. err) +``` diff --git a/doc/events/websocket_message.md b/doc/events/websocket_message.md new file mode 100644 index 000000000..53b9d4bd2 --- /dev/null +++ b/doc/events/websocket_message.md @@ -0,0 +1,26 @@ +--- +module: [kind=event] websocket_message +--- + +The @{websocket_message} event is fired when a message is received on an open WebSocket connection. + +This event is normally handled by @{http.Websocket.receive}, but it can also be pulled manually. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the WebSocket. +3. @{string}: The contents of the message. + +## Example +Prints a message sent by a WebSocket: +```lua +local myURL = "wss://example.tweaked.cc/echo" +local ws = http.websocket(myURL) +ws.send("Hello!") +local event, url, message +repeat + event, url, message = os.pullEvent("websocket_message") +until url == myURL +print("Received message from " .. url .. " with contents " .. message) +ws.close() +``` diff --git a/doc/events/websocket_success.md b/doc/events/websocket_success.md new file mode 100644 index 000000000..dcde934b3 --- /dev/null +++ b/doc/events/websocket_success.md @@ -0,0 +1,28 @@ +--- +module: [kind=event] websocket_success +see: http.websocketAsync To open a WebSocket asynchronously. +--- + +The @{websocket_success} event is fired when a WebSocket connection request returns successfully. + +This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}. + +## Return Values +1. @{string}: The event name. +2. @{string}: The URL of the site. +3. @{http.Websocket}: The handle for the WebSocket. + +## Example +Prints the content of a website (this may fail if the request fails): +```lua +local myURL = "wss://example.tweaked.cc/echo" +http.websocketAsync(myURL) +local event, url, handle +repeat + event, url, handle = os.pullEvent("websocket_success") +until url == myURL +print("Connected to " .. url) +handle.send("Hello!") +print(handle.receive()) +handle.close() +``` diff --git a/gradle.properties b/gradle.properties index 96b57d2a6..a645c8f2d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,16 +2,16 @@ org.gradle.jvmargs=-Xmx1G # Mod properties -mod_version=1.95.0-beta +mod_version=1.95.3-beta # Minecraft properties -mc_version=1.16.2 -mappings_version=31 +mc_version=1.16.4 +mappings_version=9 # Dependencies cloth_config_version=4.8.1 -fabric_api_version=0.19.0+build.398-1.16 -fabric_loader_version=0.9.2+build.206 +fabric_api_version=0.34.2+1.16 +fabric_loader_version=0.11.3 jankson_version=1.2.0 modmenu_version=1.14.6+ cloth_api_version=1.4.5 diff --git a/patchwork.md b/patchwork.md index 97d1ecd5d..40372bb64 100644 --- a/patchwork.md +++ b/patchwork.md @@ -536,4 +536,92 @@ e4b0a5b3ce035eb23feb4191432fc49af5772c5b 2020 -> 2021 ``` -A huge amount of changes. \ No newline at end of file +A huge amount of changes. + +``` +542b66c79a9b08e080c39c9a73d74ffe71c0106a + +Add back command computer block drops +``` +Didn't port some forge-related changes, but it works. + +``` +dd6f97622e6c18ce0d8988da6a5bede45c94ca5d + +Prevent reflection errors crashing the game +``` + +``` +92be0126df63927d07fc695945f8b98e328f945a + +Fix disk recipes +``` +Dye recipes actually work now. + +``` +1edb7288b974aec3764b0a820edce7e9eee38e66 + +Merge branch 'mc-1.15.x' into mc-1.16.x +``` +New version: 1.95.1. + +``` +41226371f3b5fd35f48b6d39c2e8e0c277125b21 + +Add isReadOnly to fs.attributes (#639) +``` +Also changed some lua test files, but made the changes anyway. + +``` +b2e54014869fac4b819b01b6c24e550ca113ce8a + +Added Numpad Enter Support in rom lua programs. (#657) +``` +Just lua changes. + +``` +247c05305d106af430fcdaee41371a152bf7c38c + +Fix problem with RepeatArgumentType +``` + +``` +c864576619751077a0d8ac1a18123e14b095ec03 + +Fix impostor recipes for disks +``` +[TODO] [JUMT-03] REI still shows white disks, probably because it doesn' show nbt items. + +``` +c5694ea9661c7a40021ebd280c378bd7bdc56988 + +Merge branch 'mc-1.15.x' into mc-1.16.x +``` +Update to 1.16.4. + +``` +1f84480a80677cfaaf19d319290f5b44635eba47 + +Make rightAlt only close menu, never open it. (#672) +``` +Lua changes. + +``` +1255bd00fd21247a50046020d7d9a396f66bc6bd + +Fix mounts being usable after a disk is ejected +``` +Reverted a lot of code style changes made by Zundrel, so the diffs are huge. + +``` +b90611b4b4c176ec1c80df002cc4ac36aa0c4dc8 + +Preserve registration order of upgrades +``` +Again, a huge diff because of code style changes. + +``` +8494ba8ce29cd8d7b9105eef497fe3fe3f89d350 + +Improve UX when a resource mount cannot be found +``` diff --git a/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java index 7a7de9cb7..4868de353 100644 --- a/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java +++ b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java @@ -108,7 +108,10 @@ public final class ComputerCraftAPI { * Use in conjunction with {@link IComputerAccess#mount} or {@link IComputerAccess#mountWritable} to mount a resource folder onto a computer's file * system. * - * The files in this mount will be a combination of files in all mod jar, and data packs that contain resources with the same domain and path. + * The files in this mount will be a combination of files in all mod jar, and data packs that contain + * resources with the same domain and path. For instance, ComputerCraft's resources are stored in + * "/data/computercraft/lua/rom". We construct a mount for that with + * {@code createResourceMount("computercraft", "lua/rom")}. * * @param domain The domain under which to look for resources. eg: "mymod". * @param subPath The subPath under which to look for resources. eg: "lua/myfiles". diff --git a/src/main/java/dan200/computercraft/api/IUpgradeBase.java b/src/main/java/dan200/computercraft/api/IUpgradeBase.java index c659ea5f5..06675b7ed 100644 --- a/src/main/java/dan200/computercraft/api/IUpgradeBase.java +++ b/src/main/java/dan200/computercraft/api/IUpgradeBase.java @@ -1,3 +1,8 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ package dan200.computercraft.api; import dan200.computercraft.api.pocket.IPocketUpgrade; diff --git a/src/main/java/dan200/computercraft/api/turtle/FakePlayer.java b/src/main/java/dan200/computercraft/api/turtle/FakePlayer.java index 76656fa78..4eac506e4 100644 --- a/src/main/java/dan200/computercraft/api/turtle/FakePlayer.java +++ b/src/main/java/dan200/computercraft/api/turtle/FakePlayer.java @@ -48,7 +48,7 @@ import net.minecraft.util.Hand; import net.minecraft.util.collection.DefaultedList; import net.minecraft.util.math.ChunkPos; import net.minecraft.util.math.Vec3d; -import net.minecraft.village.TraderOfferList; +import net.minecraft.village.TradeOfferList; import net.minecraft.world.GameMode; /** @@ -105,7 +105,7 @@ public class FakePlayer extends ServerPlayerEntity { } @Override - public void sendTradeOffers(int id, TraderOfferList list, int level, int experience, boolean levelled, boolean refreshable) { } + public void sendTradeOffers(int id, TradeOfferList list, int level, int experience, boolean levelled, boolean refreshable) { } @Override public void openHorseInventory(HorseBaseEntity horse, Inventory inventory) { } @@ -251,10 +251,6 @@ public class FakePlayer extends ServerPlayerEntity { public void disconnect(Text message) { } - @Override - public void setupEncryption(SecretKey key) { - } - @Override public void disableAutoRead() { } diff --git a/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/src/main/java/dan200/computercraft/core/apis/FSAPI.java index 52a2bcbbe..c2156a069 100644 --- a/src/main/java/dan200/computercraft/core/apis/FSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -396,7 +396,8 @@ public class FSAPI implements ILuaAPI { /** * Get attributes about a specific file or folder. * - * The returned attributes table contains information about the size of the file, whether it is a directory, and when it was created and last modified. + * The returned attributes table contains information about the size of the file, whether it is a directory, + * when it was created and last modified, and whether it is read only. * * The creation and modification times are given as the number of milliseconds since the UNIX epoch. This may be given to {@link OSAPI#date} in order to * convert it to more usable form. @@ -404,7 +405,7 @@ public class FSAPI implements ILuaAPI { * @param path The path to get attributes for. * @return The resulting attributes. * @throws LuaException If the path does not exist. - * @cc.treturn { size = number, isDir = boolean, created = number, modified = number } The resulting attributes. + * @cc.treturn { size = number, isDir = boolean, isReadOnly = boolean, created = number, modified = number } The resulting attributes. * @see #getSize If you only care about the file's size. * @see #isDir If you only care whether a path is a directory or not. */ @@ -413,11 +414,12 @@ public class FSAPI implements ILuaAPI { try { BasicFileAttributes attributes = this.fileSystem.getAttributes(path); Map result = new HashMap<>(); - result.put("modification", getFileTime(attributes.lastModifiedTime())); - result.put("modified", getFileTime(attributes.lastModifiedTime())); - result.put("created", getFileTime(attributes.creationTime())); - result.put("size", attributes.isDirectory() ? 0 : attributes.size()); - result.put("isDir", attributes.isDirectory()); + result.put( "modification", getFileTime( attributes.lastModifiedTime() ) ); + result.put( "modified", getFileTime( attributes.lastModifiedTime() ) ); + result.put( "created", getFileTime( attributes.creationTime() ) ); + result.put( "size", attributes.isDirectory() ? 0 : attributes.size() ); + result.put( "isDir", attributes.isDirectory() ); + result.put( "isReadOnly", fileSystem.isReadOnly( path ) ); return result; } catch (FileSystemException e) { throw new LuaException(e.getMessage()); diff --git a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java index 9f1c5f951..0996e5300 100644 --- a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -3,185 +3,206 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis; -import static dan200.computercraft.core.apis.TableHelper.getStringField; -import static dan200.computercraft.core.apis.TableHelper.optBooleanField; -import static dan200.computercraft.core.apis.TableHelper.optStringField; -import static dan200.computercraft.core.apis.TableHelper.optTableField; - -import java.net.URI; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; - -import javax.annotation.Nonnull; - import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.core.apis.http.CheckUrl; -import dan200.computercraft.core.apis.http.HTTPRequestException; -import dan200.computercraft.core.apis.http.Resource; -import dan200.computercraft.core.apis.http.ResourceGroup; -import dan200.computercraft.core.apis.http.ResourceQueue; +import dan200.computercraft.core.apis.http.*; import dan200.computercraft.core.apis.http.request.HttpRequest; import dan200.computercraft.core.apis.http.websocket.Websocket; import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; +import javax.annotation.Nonnull; +import java.net.URI; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static dan200.computercraft.core.apis.TableHelper.*; + /** * The http library allows communicating with web servers, sending and receiving data from them. * * @cc.module http * @hidden */ -public class HTTPAPI implements ILuaAPI { - private final IAPIEnvironment m_apiEnvironment; +public class HTTPAPI implements ILuaAPI +{ + private final IAPIEnvironment apiEnvironment; private final ResourceGroup checkUrls = new ResourceGroup<>(); - private final ResourceGroup requests = new ResourceQueue<>(() -> ComputerCraft.httpMaxRequests); - private final ResourceGroup websockets = new ResourceGroup<>(() -> ComputerCraft.httpMaxWebsockets); + private final ResourceGroup requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests ); + private final ResourceGroup websockets = new ResourceGroup<>( () -> ComputerCraft.httpMaxWebsockets ); - public HTTPAPI(IAPIEnvironment environment) { - this.m_apiEnvironment = environment; + public HTTPAPI( IAPIEnvironment environment ) + { + apiEnvironment = environment; } @Override - public String[] getNames() { - return new String[] {"http"}; + public String[] getNames() + { + return new String[] { "http" }; } @Override - public void startup() { - this.checkUrls.startup(); - this.requests.startup(); - this.websockets.startup(); + public void startup() + { + checkUrls.startup(); + requests.startup(); + websockets.startup(); } @Override - public void update() { + public void shutdown() + { + checkUrls.shutdown(); + requests.shutdown(); + websockets.shutdown(); + } + + @Override + public void update() + { // It's rather ugly to run this here, but we need to clean up // resources as often as possible to reduce blocking. Resource.cleanup(); } - @Override - public void shutdown() { - this.checkUrls.shutdown(); - this.requests.shutdown(); - this.websockets.shutdown(); - } - @LuaFunction - public final Object[] request(IArguments args) throws LuaException { + public final Object[] request( IArguments args ) throws LuaException + { String address, postString, requestMethod; Map headerTable; boolean binary, redirect; - if (args.get(0) instanceof Map) { - Map options = args.getTable(0); - address = getStringField(options, "url"); - postString = optStringField(options, "body", null); - headerTable = optTableField(options, "headers", Collections.emptyMap()); - binary = optBooleanField(options, "binary", false); - requestMethod = optStringField(options, "method", null); - redirect = optBooleanField(options, "redirect", true); + if( args.get( 0 ) instanceof Map ) + { + Map options = args.getTable( 0 ); + address = getStringField( options, "url" ); + postString = optStringField( options, "body", null ); + headerTable = optTableField( options, "headers", Collections.emptyMap() ); + binary = optBooleanField( options, "binary", false ); + requestMethod = optStringField( options, "method", null ); + redirect = optBooleanField( options, "redirect", true ); - } else { + } + else + { // Get URL and post information - address = args.getString(0); - postString = args.optString(1, null); - headerTable = args.optTable(2, Collections.emptyMap()); - binary = args.optBoolean(3, false); + address = args.getString( 0 ); + postString = args.optString( 1, null ); + headerTable = args.optTable( 2, Collections.emptyMap() ); + binary = args.optBoolean( 3, false ); requestMethod = null; redirect = true; } - HttpHeaders headers = getHeaders(headerTable); + HttpHeaders headers = getHeaders( headerTable ); HttpMethod httpMethod; - if (requestMethod == null) { + if( requestMethod == null ) + { httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST; - } else { - httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT)); - if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) { - throw new LuaException("Unsupported HTTP method"); + } + else + { + httpMethod = HttpMethod.valueOf( requestMethod.toUpperCase( Locale.ROOT ) ); + if( httpMethod == null || requestMethod.equalsIgnoreCase( "CONNECT" ) ) + { + throw new LuaException( "Unsupported HTTP method" ); } } - try { - URI uri = HttpRequest.checkUri(address); - HttpRequest request = new HttpRequest(this.requests, this.m_apiEnvironment, address, postString, headers, binary, redirect); + try + { + URI uri = HttpRequest.checkUri( address ); + HttpRequest request = new HttpRequest( requests, apiEnvironment, address, postString, headers, binary, redirect ); // Make the request - request.queue(r -> r.request(uri, httpMethod)); + request.queue( r -> r.request( uri, httpMethod ) ); - return new Object[] {true}; - } catch (HTTPRequestException e) { - return new Object[] { - false, - e.getMessage() - }; + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] checkURL( String address ) + { + try + { + URI uri = HttpRequest.checkUri( address ); + new CheckUrl( checkUrls, apiEnvironment, address, uri ).queue( CheckUrl::run ); + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] websocket( String address, Optional> headerTbl ) throws LuaException + { + if( !ComputerCraft.http_websocket_enable ) + { + throw new LuaException( "Websocket connections are disabled" ); + } + + HttpHeaders headers = getHeaders( headerTbl.orElse( Collections.emptyMap() ) ); + + try + { + URI uri = Websocket.checkUri( address ); + if( !new Websocket( websockets, apiEnvironment, uri, address, headers ).queue( Websocket::connect ) ) + { + throw new LuaException( "Too many websockets already open" ); + } + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; } } @Nonnull - private static HttpHeaders getHeaders(@Nonnull Map headerTable) throws LuaException { + private HttpHeaders getHeaders( @Nonnull Map headerTable ) throws LuaException + { HttpHeaders headers = new DefaultHttpHeaders(); - for (Map.Entry entry : headerTable.entrySet()) { + for( Map.Entry entry : headerTable.entrySet() ) + { Object value = entry.getValue(); - if (entry.getKey() instanceof String && value instanceof String) { - try { - headers.add((String) entry.getKey(), value); - } catch (IllegalArgumentException e) { - throw new LuaException(e.getMessage()); + if( entry.getKey() instanceof String && value instanceof String ) + { + try + { + headers.add( (String) entry.getKey(), value ); + } + catch( IllegalArgumentException e ) + { + throw new LuaException( e.getMessage() ); } } } + + if( !headers.contains( HttpHeaderNames.USER_AGENT ) ) + { + headers.set( HttpHeaderNames.USER_AGENT, apiEnvironment.getComputerEnvironment().getUserAgent() ); + } return headers; } - - @LuaFunction - public final Object[] checkURL(String address) { - try { - URI uri = HttpRequest.checkUri(address); - new CheckUrl(this.checkUrls, this.m_apiEnvironment, address, uri).queue(CheckUrl::run); - - return new Object[] {true}; - } catch (HTTPRequestException e) { - return new Object[] { - false, - e.getMessage() - }; - } - } - - @LuaFunction - public final Object[] websocket(String address, Optional> headerTbl) throws LuaException { - if (!ComputerCraft.http_websocket_enable) { - throw new LuaException("Websocket connections are disabled"); - } - - HttpHeaders headers = getHeaders(headerTbl.orElse(Collections.emptyMap())); - - try { - URI uri = Websocket.checkUri(address); - if (!new Websocket(this.websockets, this.m_apiEnvironment, uri, address, headers).queue(Websocket::connect)) { - throw new LuaException("Too many websockets already open"); - } - - return new Object[] {true}; - } catch (HTTPRequestException e) { - return new Object[] { - false, - e.getMessage() - }; - } - } -} +} \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java b/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java index 4ce5ac261..9f026729a 100644 --- a/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java @@ -185,8 +185,9 @@ public class RedstoneAPI implements ILuaAPI { * @see #testBundledInput To determine if a specific colour is set. */ @LuaFunction - public final int getBundledInput(ComputerSide side) { - return this.environment.getBundledOutput(side); + public final int getBundledInput( ComputerSide side ) + { + return environment.getBundledInput( side ); } /** diff --git a/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java index 345e4b949..464e23a4e 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/ArrayByteChannel.java @@ -3,7 +3,6 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; import java.nio.ByteBuffer; @@ -15,83 +14,81 @@ import java.util.Objects; /** * A seekable, readable byte channel which is backed by a simple byte array. */ -public class ArrayByteChannel implements SeekableByteChannel { - private final byte[] backing; +public class ArrayByteChannel implements SeekableByteChannel +{ private boolean closed = false; private int position = 0; - public ArrayByteChannel(byte[] backing) { + private final byte[] backing; + + public ArrayByteChannel( byte[] backing ) + { this.backing = backing; } @Override - public int read(ByteBuffer destination) throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } - Objects.requireNonNull(destination, "destination"); + public int read( ByteBuffer destination ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + Objects.requireNonNull( destination, "destination" ); - if (this.position >= this.backing.length) { - return -1; - } + if( position >= backing.length ) return -1; - int remaining = Math.min(this.backing.length - this.position, destination.remaining()); - destination.put(this.backing, this.position, remaining); - this.position += remaining; + int remaining = Math.min( backing.length - position, destination.remaining() ); + destination.put( backing, position, remaining ); + position += remaining; return remaining; } @Override - public int write(ByteBuffer src) throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } + public int write( ByteBuffer src ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); throw new NonWritableChannelException(); } @Override - public long position() throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } - return this.position; + public long position() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return position; } @Override - public SeekableByteChannel position(long newPosition) throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); + public SeekableByteChannel position( long newPosition ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + if( newPosition < 0 || newPosition > Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "Position out of bounds" ); } - if (newPosition < 0 || newPosition > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Position out of bounds"); - } - this.position = (int) newPosition; + position = (int) newPosition; return this; } @Override - public long size() throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } - return this.backing.length; + public long size() throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); + return backing.length; } @Override - public SeekableByteChannel truncate(long size) throws ClosedChannelException { - if (this.closed) { - throw new ClosedChannelException(); - } + public SeekableByteChannel truncate( long size ) throws ClosedChannelException + { + if( closed ) throw new ClosedChannelException(); throw new NonWritableChannelException(); } @Override - public boolean isOpen() { - return !this.closed; + public boolean isOpen() + { + return !closed; } @Override - public void close() { - this.closed = true; + public void close() + { + closed = true; } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java index e6dcc17ad..4477dde92 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java @@ -3,11 +3,13 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; + import java.io.ByteArrayOutputStream; -import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; @@ -16,40 +18,43 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; - /** - * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} mode. + * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} + * mode. * * @cc.module fs.BinaryReadHandle */ -public class BinaryReadableHandle extends HandleGeneric { +public class BinaryReadableHandle extends HandleGeneric +{ private static final int BUFFER_SIZE = 8192; - final SeekableByteChannel seekable; - private final ReadableByteChannel reader; - private final ByteBuffer single = ByteBuffer.allocate(1); - BinaryReadableHandle(ReadableByteChannel reader, SeekableByteChannel seekable, Closeable closeable) { - super(closeable); + private final ReadableByteChannel reader; + final SeekableByteChannel seekable; + private final ByteBuffer single = ByteBuffer.allocate( 1 ); + + BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable ) + { + super( closeable ); this.reader = reader; this.seekable = seekable; } - public static BinaryReadableHandle of(ReadableByteChannel channel) { - return of(channel, channel); + public static BinaryReadableHandle of( ReadableByteChannel channel, TrackingCloseable closeable ) + { + SeekableByteChannel seekable = asSeekable( channel ); + return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); } - public static BinaryReadableHandle of(ReadableByteChannel channel, Closeable closeable) { - SeekableByteChannel seekable = asSeekable(channel); - return seekable == null ? new BinaryReadableHandle(channel, null, closeable) : new Seekable(seekable, closeable); + public static BinaryReadableHandle of( ReadableByteChannel channel ) + { + return of( channel, new TrackingCloseable.Impl( channel ) ); } /** * Read a number of bytes from this file. * - * @param countArg The number of bytes to read. When absent, a single byte will be read as a number. This may be 0 to determine we are at - * the end of the file. + * @param countArg The number of bytes to read. When absent, a single byte will be read as a number. This + * may be 0 to determine we are at the end of the file. * @return The read bytes. * @throws LuaException When trying to read a negative number of bytes. * @throws LuaException If the file has been closed. @@ -58,72 +63,78 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given. */ @LuaFunction - public final Object[] read(Optional countArg) throws LuaException { - this.checkOpen(); - try { - if (countArg.isPresent()) { + public final Object[] read( Optional countArg ) throws LuaException + { + checkOpen(); + try + { + if( countArg.isPresent() ) + { int count = countArg.get(); - if (count < 0) { - throw new LuaException("Cannot read a negative number of bytes"); - } - if (count == 0 && this.seekable != null) { - return this.seekable.position() >= this.seekable.size() ? null : new Object[] {""}; + if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" ); + if( count == 0 && seekable != null ) + { + return seekable.position() >= seekable.size() ? null : new Object[] { "" }; } - if (count <= BUFFER_SIZE) { - ByteBuffer buffer = ByteBuffer.allocate(count); + if( count <= BUFFER_SIZE ) + { + ByteBuffer buffer = ByteBuffer.allocate( count ); - int read = this.reader.read(buffer); - if (read < 0) { - return null; - } + int read = reader.read( buffer ); + if( read < 0 ) return null; buffer.flip(); - return new Object[] {buffer}; - } else { + return new Object[] { buffer }; + } + else + { // Read the initial set of characters, failing if none are read. - ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); - int read = this.reader.read(buffer); - if (read < 0) { - return null; - } + ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE ); + int read = reader.read( buffer ); + if( read < 0 ) return null; // If we failed to read "enough" here, let's just abort - if (read >= count || read < BUFFER_SIZE) { + if( read >= count || read < BUFFER_SIZE ) + { buffer.flip(); - return new Object[] {buffer}; + return new Object[] { buffer }; } // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation // than doubling up the buffer each time. int totalRead = read; - List parts = new ArrayList<>(4); - parts.add(buffer); - while (read >= BUFFER_SIZE && totalRead < count) { - buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead)); - read = this.reader.read(buffer); - if (read < 0) { - break; - } + List parts = new ArrayList<>( 4 ); + parts.add( buffer ); + while( read >= BUFFER_SIZE && totalRead < count ) + { + buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) ); + read = reader.read( buffer ); + if( read < 0 ) break; totalRead += read; - parts.add(buffer); + parts.add( buffer ); } // Now just copy all the bytes across! byte[] bytes = new byte[totalRead]; int pos = 0; - for (ByteBuffer part : parts) { - System.arraycopy(part.array(), 0, bytes, pos, part.position()); + for( ByteBuffer part : parts ) + { + System.arraycopy( part.array(), 0, bytes, pos, part.position() ); pos += part.position(); } - return new Object[] {bytes}; + return new Object[] { bytes }; } - } else { - this.single.clear(); - int b = this.reader.read(this.single); - return b == -1 ? null : new Object[] {this.single.get(0) & 0xFF}; } - } catch (IOException e) { + else + { + single.clear(); + int b = reader.read( single ); + return b == -1 ? null : new Object[] { single.get( 0 ) & 0xFF }; + } + } + catch( IOException e ) + { return null; } } @@ -136,29 +147,30 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end. */ @LuaFunction - public final Object[] readAll() throws LuaException { - this.checkOpen(); - try { + public final Object[] readAll() throws LuaException + { + checkOpen(); + try + { int expected = 32; - if (this.seekable != null) { - expected = Math.max(expected, (int) (this.seekable.size() - this.seekable.position())); - } - ByteArrayOutputStream stream = new ByteArrayOutputStream(expected); + if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) ); + ByteArrayOutputStream stream = new ByteArrayOutputStream( expected ); - ByteBuffer buf = ByteBuffer.allocate(8192); + ByteBuffer buf = ByteBuffer.allocate( 8192 ); boolean readAnything = false; - while (true) { + while( true ) + { buf.clear(); - int r = this.reader.read(buf); - if (r == -1) { - break; - } + int r = reader.read( buf ); + if( r == -1 ) break; readAnything = true; - stream.write(buf.array(), 0, r); + stream.write( buf.array(), 0, r ); } - return readAnything ? new Object[] {stream.toByteArray()} : null; - } catch (IOException e) { + return readAnything ? new Object[] { stream.toByteArray() } : null; + } + catch( IOException e ) + { return null; } } @@ -172,65 +184,70 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.treturn string|nil The read line or {@code nil} if at the end of the file. */ @LuaFunction - public final Object[] readLine(Optional withTrailingArg) throws LuaException { - this.checkOpen(); - boolean withTrailing = withTrailingArg.orElse(false); - try { + public final Object[] readLine( Optional withTrailingArg ) throws LuaException + { + checkOpen(); + boolean withTrailing = withTrailingArg.orElse( false ); + try + { ByteArrayOutputStream stream = new ByteArrayOutputStream(); boolean readAnything = false, readRc = false; - while (true) { - this.single.clear(); - int read = this.reader.read(this.single); - if (read <= 0) { + while( true ) + { + single.clear(); + int read = reader.read( single ); + if( read <= 0 ) + { // Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it // back. - if (readRc) { - stream.write('\r'); - } - return readAnything ? new Object[] {stream.toByteArray()} : null; + if( readRc ) stream.write( '\r' ); + return readAnything ? new Object[] { stream.toByteArray() } : null; } readAnything = true; - byte chr = this.single.get(0); - if (chr == '\n') { - if (withTrailing) { - if (readRc) { - stream.write('\r'); - } - stream.write(chr); + byte chr = single.get( 0 ); + if( chr == '\n' ) + { + if( withTrailing ) + { + if( readRc ) stream.write( '\r' ); + stream.write( chr ); } - return new Object[] {stream.toByteArray()}; - } else { + return new Object[] { stream.toByteArray() }; + } + else + { // We want to skip \r\n, but obviously need to include cases where \r is not followed by \n. // Note, this behaviour is non-standard compliant (strictly speaking we should have no // special logic for \r), but we preserve compatibility with EncodedReadableHandle and // previous behaviour of the io library. - if (readRc) { - stream.write('\r'); - } + if( readRc ) stream.write( '\r' ); readRc = chr == '\r'; - if (!readRc) { - stream.write(chr); - } + if( !readRc ) stream.write( chr ); } } - } catch (IOException e) { + } + catch( IOException e ) + { return null; } } - public static class Seekable extends BinaryReadableHandle { - Seekable(SeekableByteChannel seekable, Closeable closeable) { - super(seekable, seekable, closeable); + public static class Seekable extends BinaryReadableHandle + { + Seekable( SeekableByteChannel seekable, TrackingCloseable closeable ) + { + super( seekable, seekable, closeable ); } /** - * Seek to a new position within the file, changing where bytes are written to. The new position is an offset given by {@code offset}, relative to a - * start position determined by {@code whence}: + * Seek to a new position within the file, changing where bytes are written to. The new position is an offset + * given by {@code offset}, relative to a start position determined by {@code whence}: * - * - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default. + * - {@code "set"}: {@code offset} is relative to the beginning of the file. + * - {@code "cur"}: Relative to the current position. This is the default. * - {@code "end"}: Relative to the end of the file. * * In case of success, {@code seek} returns the new file position from the beginning of the file. @@ -244,9 +261,10 @@ public class BinaryReadableHandle extends HandleGeneric { * @cc.treturn string The reason seeking failed. */ @LuaFunction - public final Object[] seek(Optional whence, Optional offset) throws LuaException { - this.checkOpen(); - return handleSeek(this.seekable, whence, offset); + public final Object[] seek( Optional whence, Optional offset ) throws LuaException + { + checkOpen(); + return handleSeek( seekable, whence, offset ); } } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java index da466dca1..796582855 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java @@ -3,10 +3,14 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; -import java.io.Closeable; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.LuaValues; +import dan200.computercraft.core.filesystem.TrackingCloseable; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; @@ -14,34 +18,34 @@ import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.util.Optional; -import dan200.computercraft.api.lua.IArguments; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.api.lua.LuaValues; - /** - * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} modes. + * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} + * modes. * * @cc.module fs.BinaryWriteHandle */ -public class BinaryWritableHandle extends HandleGeneric { - final SeekableByteChannel seekable; +public class BinaryWritableHandle extends HandleGeneric +{ private final WritableByteChannel writer; - private final ByteBuffer single = ByteBuffer.allocate(1); + final SeekableByteChannel seekable; + private final ByteBuffer single = ByteBuffer.allocate( 1 ); - protected BinaryWritableHandle(WritableByteChannel writer, SeekableByteChannel seekable, Closeable closeable) { - super(closeable); + protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, TrackingCloseable closeable ) + { + super( closeable ); this.writer = writer; this.seekable = seekable; } - public static BinaryWritableHandle of(WritableByteChannel channel) { - return of(channel, channel); + public static BinaryWritableHandle of( WritableByteChannel channel, TrackingCloseable closeable ) + { + SeekableByteChannel seekable = asSeekable( channel ); + return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); } - public static BinaryWritableHandle of(WritableByteChannel channel, Closeable closeable) { - SeekableByteChannel seekable = asSeekable(channel); - return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable); + public static BinaryWritableHandle of( WritableByteChannel channel ) + { + return of( channel, new TrackingCloseable.Impl( channel ) ); } /** @@ -53,24 +57,33 @@ public class BinaryWritableHandle extends HandleGeneric { * @cc.tparam [2] string The string to write. */ @LuaFunction - public final void write(IArguments arguments) throws LuaException { - this.checkOpen(); - try { - Object arg = arguments.get(0); - if (arg instanceof Number) { + public final void write( IArguments arguments ) throws LuaException + { + checkOpen(); + try + { + Object arg = arguments.get( 0 ); + if( arg instanceof Number ) + { int number = ((Number) arg).intValue(); - this.single.clear(); - this.single.put((byte) number); - this.single.flip(); + single.clear(); + single.put( (byte) number ); + single.flip(); - this.writer.write(this.single); - } else if (arg instanceof String) { - this.writer.write(arguments.getBytes(0)); - } else { - throw LuaValues.badArgumentOf(0, "string or number", arg); + writer.write( single ); } - } catch (IOException e) { - throw new LuaException(e.getMessage()); + else if( arg instanceof String ) + { + writer.write( arguments.getBytes( 0 ) ); + } + else + { + throw LuaValues.badArgumentOf( 0, "string or number", arg ); + } + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); } } @@ -80,27 +93,32 @@ public class BinaryWritableHandle extends HandleGeneric { * @throws LuaException If the file has been closed. */ @LuaFunction - public final void flush() throws LuaException { - this.checkOpen(); - try { + public final void flush() throws LuaException + { + checkOpen(); + try + { // Technically this is not needed - if (this.writer instanceof FileChannel) { - ((FileChannel) this.writer).force(false); - } - } catch (IOException ignored) { + if( writer instanceof FileChannel ) ((FileChannel) writer).force( false ); + } + catch( IOException ignored ) + { } } - public static class Seekable extends BinaryWritableHandle { - public Seekable(SeekableByteChannel seekable, Closeable closeable) { - super(seekable, seekable, closeable); + public static class Seekable extends BinaryWritableHandle + { + public Seekable( SeekableByteChannel seekable, TrackingCloseable closeable ) + { + super( seekable, seekable, closeable ); } /** - * Seek to a new position within the file, changing where bytes are written to. The new position is an offset given by {@code offset}, relative to a - * start position determined by {@code whence}: + * Seek to a new position within the file, changing where bytes are written to. The new position is an offset + * given by {@code offset}, relative to a start position determined by {@code whence}: * - * - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default. + * - {@code "set"}: {@code offset} is relative to the beginning of the file. + * - {@code "cur"}: Relative to the current position. This is the default. * - {@code "end"}: Relative to the end of the file. * * In case of success, {@code seek} returns the new file position from the beginning of the file. @@ -114,9 +132,10 @@ public class BinaryWritableHandle extends HandleGeneric { * @cc.treturn string The reason seeking failed. */ @LuaFunction - public final Object[] seek(Optional whence, Optional offset) throws LuaException { - this.checkOpen(); - return handleSeek(this.seekable, whence, offset); + public final Object[] seek( Optional whence, Optional offset ) throws LuaException + { + checkOpen(); + return handleSeek( seekable, whence, offset ); } } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java index c173a26ef..28576f70d 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java @@ -3,11 +3,14 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; + +import javax.annotation.Nonnull; import java.io.BufferedReader; -import java.io.Closeable; import java.io.IOException; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; @@ -17,41 +20,27 @@ import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.Optional; -import javax.annotation.Nonnull; - -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; - /** - * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} mode. + * A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} + * mode. * * @cc.module fs.ReadHandle */ -public class EncodedReadableHandle extends HandleGeneric { +public class EncodedReadableHandle extends HandleGeneric +{ private static final int BUFFER_SIZE = 8192; private final BufferedReader reader; - public EncodedReadableHandle(@Nonnull BufferedReader reader) { - this(reader, reader); - } - - public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull Closeable closable) { - super(closable); + public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull TrackingCloseable closable ) + { + super( closable ); this.reader = reader; } - public static BufferedReader openUtf8(ReadableByteChannel channel) { - return open(channel, StandardCharsets.UTF_8); - } - - public static BufferedReader open(ReadableByteChannel channel, Charset charset) { - // Create a charset decoder with the same properties as StreamDecoder does for - // InputStreams: namely, replace everything instead of erroring. - CharsetDecoder decoder = charset.newDecoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - return new BufferedReader(Channels.newReader(channel, decoder, -1)); + public EncodedReadableHandle( @Nonnull BufferedReader reader ) + { + this( reader, new TrackingCloseable.Impl( reader ) ); } /** @@ -63,21 +52,26 @@ public class EncodedReadableHandle extends HandleGeneric { * @cc.treturn string|nil The read line or {@code nil} if at the end of the file. */ @LuaFunction - public final Object[] readLine(Optional withTrailingArg) throws LuaException { - this.checkOpen(); - boolean withTrailing = withTrailingArg.orElse(false); - try { - String line = this.reader.readLine(); - if (line != null) { + public final Object[] readLine( Optional withTrailingArg ) throws LuaException + { + checkOpen(); + boolean withTrailing = withTrailingArg.orElse( false ); + try + { + String line = reader.readLine(); + if( line != null ) + { // While this is technically inaccurate, it's better than nothing - if (withTrailing) { - line += "\n"; - } - return new Object[] {line}; - } else { + if( withTrailing ) line += "\n"; + return new Object[] { line }; + } + else + { return null; } - } catch (IOException e) { + } + catch( IOException e ) + { return null; } } @@ -90,20 +84,26 @@ public class EncodedReadableHandle extends HandleGeneric { * @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end. */ @LuaFunction - public final Object[] readAll() throws LuaException { - this.checkOpen(); - try { + public final Object[] readAll() throws LuaException + { + checkOpen(); + try + { StringBuilder result = new StringBuilder(); - String line = this.reader.readLine(); - while (line != null) { - result.append(line); - line = this.reader.readLine(); - if (line != null) { - result.append("\n"); + String line = reader.readLine(); + while( line != null ) + { + result.append( line ); + line = reader.readLine(); + if( line != null ) + { + result.append( "\n" ); } } - return new Object[] {result.toString()}; - } catch (IOException e) { + return new Object[] { result.toString() }; + } + catch( IOException e ) + { return null; } } @@ -118,50 +118,71 @@ public class EncodedReadableHandle extends HandleGeneric { * @cc.treturn string|nil The read characters, or {@code nil} if at the of the file. */ @LuaFunction - public final Object[] read(Optional countA) throws LuaException { - this.checkOpen(); - try { - int count = countA.orElse(1); - if (count < 0) { + public final Object[] read( Optional countA ) throws LuaException + { + checkOpen(); + try + { + int count = countA.orElse( 1 ); + if( count < 0 ) + { // Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so // it seems best to remain somewhat consistent. - throw new LuaException("Cannot read a negative number of characters"); - } else if (count <= BUFFER_SIZE) { + throw new LuaException( "Cannot read a negative number of characters" ); + } + else if( count <= BUFFER_SIZE ) + { // If we've got a small count, then allocate that and read it. char[] chars = new char[count]; - int read = this.reader.read(chars); + int read = reader.read( chars ); - return read < 0 ? null : new Object[] {new String(chars, 0, read)}; - } else { + return read < 0 ? null : new Object[] { new String( chars, 0, read ) }; + } + else + { // If we've got a large count, read in bunches of 8192. char[] buffer = new char[BUFFER_SIZE]; // Read the initial set of characters, failing if none are read. - int read = this.reader.read(buffer, 0, Math.min(buffer.length, count)); - if (read < 0) { - return null; - } + int read = reader.read( buffer, 0, Math.min( buffer.length, count ) ); + if( read < 0 ) return null; - StringBuilder out = new StringBuilder(read); + StringBuilder out = new StringBuilder( read ); int totalRead = read; - out.append(buffer, 0, read); + out.append( buffer, 0, read ); // Otherwise read until we either reach the limit or we no longer consume // the full buffer. - while (read >= BUFFER_SIZE && totalRead < count) { - read = this.reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead)); - if (read < 0) { - break; - } + while( read >= BUFFER_SIZE && totalRead < count ) + { + read = reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) ); + if( read < 0 ) break; totalRead += read; - out.append(buffer, 0, read); + out.append( buffer, 0, read ); } - return new Object[] {out.toString()}; + return new Object[] { out.toString() }; } - } catch (IOException e) { + } + catch( IOException e ) + { return null; } } + + public static BufferedReader openUtf8( ReadableByteChannel channel ) + { + return open( channel, StandardCharsets.UTF_8 ); + } + + public static BufferedReader open( ReadableByteChannel channel, Charset charset ) + { + // Create a charset decoder with the same properties as StreamDecoder does for + // InputStreams: namely, replace everything instead of erroring. + CharsetDecoder decoder = charset.newDecoder() + .onMalformedInput( CodingErrorAction.REPLACE ) + .onUnmappableCharacter( CodingErrorAction.REPLACE ); + return new BufferedReader( Channels.newReader( channel, decoder, -1 ) ); + } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java index 7b02d91bd..b012b6d0d 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java @@ -3,11 +3,16 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; +import dan200.computercraft.shared.util.StringUtil; + +import javax.annotation.Nonnull; import java.io.BufferedWriter; -import java.io.Closeable; import java.io.IOException; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; @@ -16,39 +21,21 @@ import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; -import javax.annotation.Nonnull; - -import dan200.computercraft.api.lua.IArguments; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.shared.util.StringUtil; - /** * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes. * * @cc.module fs.WriteHandle */ -public class EncodedWritableHandle extends HandleGeneric { +public class EncodedWritableHandle extends HandleGeneric +{ private final BufferedWriter writer; - public EncodedWritableHandle(@Nonnull BufferedWriter writer, @Nonnull Closeable closable) { - super(closable); + public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull TrackingCloseable closable ) + { + super( closable ); this.writer = writer; } - public static BufferedWriter openUtf8(WritableByteChannel channel) { - return open(channel, StandardCharsets.UTF_8); - } - - public static BufferedWriter open(WritableByteChannel channel, Charset charset) { - // Create a charset encoder with the same properties as StreamEncoder does for - // OutputStreams: namely, replace everything instead of erroring. - CharsetEncoder encoder = charset.newEncoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - return new BufferedWriter(Channels.newWriter(channel, encoder, -1)); - } - /** * Write a string of characters to the file. * @@ -57,13 +44,17 @@ public class EncodedWritableHandle extends HandleGeneric { * @cc.param value The value to write to the file. */ @LuaFunction - public final void write(IArguments args) throws LuaException { - this.checkOpen(); - String text = StringUtil.toString(args.get(0)); - try { - this.writer.write(text, 0, text.length()); - } catch (IOException e) { - throw new LuaException(e.getMessage()); + public final void write( IArguments args ) throws LuaException + { + checkOpen(); + String text = StringUtil.toString( args.get( 0 ) ); + try + { + writer.write( text, 0, text.length() ); + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); } } @@ -75,14 +66,18 @@ public class EncodedWritableHandle extends HandleGeneric { * @cc.param value The value to write to the file. */ @LuaFunction - public final void writeLine(IArguments args) throws LuaException { - this.checkOpen(); - String text = StringUtil.toString(args.get(0)); - try { - this.writer.write(text, 0, text.length()); - this.writer.newLine(); - } catch (IOException e) { - throw new LuaException(e.getMessage()); + public final void writeLine( IArguments args ) throws LuaException + { + checkOpen(); + String text = StringUtil.toString( args.get( 0 ) ); + try + { + writer.write( text, 0, text.length() ); + writer.newLine(); + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); } } @@ -92,11 +87,30 @@ public class EncodedWritableHandle extends HandleGeneric { * @throws LuaException If the file has been closed. */ @LuaFunction - public final void flush() throws LuaException { - this.checkOpen(); - try { - this.writer.flush(); - } catch (IOException ignored) { + public final void flush() throws LuaException + { + checkOpen(); + try + { + writer.flush(); + } + catch( IOException ignored ) + { } } + + public static BufferedWriter openUtf8( WritableByteChannel channel ) + { + return open( channel, StandardCharsets.UTF_8 ); + } + + public static BufferedWriter open( WritableByteChannel channel, Charset charset ) + { + // Create a charset encoder with the same properties as StreamEncoder does for + // OutputStreams: namely, replace everything instead of erroring. + CharsetEncoder encoder = charset.newEncoder() + .onMalformedInput( CodingErrorAction.REPLACE ) + .onUnmappableCharacter( CodingErrorAction.REPLACE ); + return new BufferedWriter( Channels.newWriter( channel, encoder, -1 ) ); + } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java index f7b25dffa..fc1954354 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java @@ -3,79 +3,38 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.handles; -import java.io.Closeable; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.filesystem.TrackingCloseable; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; import java.io.IOException; import java.nio.channels.Channel; import java.nio.channels.SeekableByteChannel; import java.util.Optional; -import javax.annotation.Nonnull; +public abstract class HandleGeneric +{ + private TrackingCloseable closeable; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.LuaFunction; -import dan200.computercraft.shared.util.IoUtil; - -public abstract class HandleGeneric { - private Closeable closable; - private boolean open = true; - - protected HandleGeneric(@Nonnull Closeable closable) { - this.closable = closable; + protected HandleGeneric( @Nonnull TrackingCloseable closeable ) + { + this.closeable = closeable; } - /** - * Shared implementation for various file handle types. - * - * @param channel The channel to seek in - * @param whence The seeking mode. - * @param offset The offset to seek to. - * @return The new position of the file, or null if some error occurred. - * @throws LuaException If the arguments were invalid - * @see {@code file:seek} in the Lua manual. - */ - protected static Object[] handleSeek(SeekableByteChannel channel, Optional whence, Optional offset) throws LuaException { - long actualOffset = offset.orElse(0L); - try { - switch (whence.orElse("cur")) { - case "set": - channel.position(actualOffset); - break; - case "cur": - channel.position(channel.position() + actualOffset); - break; - case "end": - channel.position(channel.size() + actualOffset); - break; - default: - throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'"); - } - - return new Object[] {channel.position()}; - } catch (IllegalArgumentException e) { - return new Object[] { - null, - "Position is negative" - }; - } catch (IOException e) { - return null; - } + protected void checkOpen() throws LuaException + { + TrackingCloseable closeable = this.closeable; + if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" ); } - protected static SeekableByteChannel asSeekable(Channel channel) { - if (!(channel instanceof SeekableByteChannel)) { - return null; - } - - SeekableByteChannel seekable = (SeekableByteChannel) channel; - try { - seekable.position(seekable.position()); - return seekable; - } catch (IOException | UnsupportedOperationException e) { - return null; - } + protected final void close() + { + IoUtil.closeQuietly( closeable ); + closeable = null; } /** @@ -85,22 +44,69 @@ public abstract class HandleGeneric { * * @throws LuaException If the file has already been closed. */ - @LuaFunction ("close") - public final void doClose() throws LuaException { - this.checkOpen(); - this.close(); + @LuaFunction( "close" ) + public final void doClose() throws LuaException + { + checkOpen(); + close(); } - protected void checkOpen() throws LuaException { - if (!this.open) { - throw new LuaException("attempt to use a closed file"); + + /** + * Shared implementation for various file handle types. + * + * @param channel The channel to seek in + * @param whence The seeking mode. + * @param offset The offset to seek to. + * @return The new position of the file, or null if some error occurred. + * @throws LuaException If the arguments were invalid + * @see {@code file:seek} in the Lua manual. + */ + protected static Object[] handleSeek( SeekableByteChannel channel, Optional whence, Optional offset ) throws LuaException + { + long actualOffset = offset.orElse( 0L ); + try + { + switch( whence.orElse( "cur" ) ) + { + case "set": + channel.position( actualOffset ); + break; + case "cur": + channel.position( channel.position() + actualOffset ); + break; + case "end": + channel.position( channel.size() + actualOffset ); + break; + default: + throw new LuaException( "bad argument #1 to 'seek' (invalid option '" + whence + "'" ); + } + + return new Object[] { channel.position() }; + } + catch( IllegalArgumentException e ) + { + return new Object[] { null, "Position is negative" }; + } + catch( IOException e ) + { + return null; } } - protected final void close() { - this.open = false; + protected static SeekableByteChannel asSeekable( Channel channel ) + { + if( !(channel instanceof SeekableByteChannel) ) return null; - IoUtil.closeQuietly(this.closable); - this.closable = null; + SeekableByteChannel seekable = (SeekableByteChannel) channel; + try + { + seekable.position( seekable.position() ); + return seekable; + } + catch( IOException | UnsupportedOperationException e ) + { + return null; + } } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java index 23931eb18..845a185ee 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java @@ -3,19 +3,8 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.apis.http.request; -import static dan200.computercraft.core.apis.http.request.HttpRequest.getHeaderSize; - -import java.io.Closeable; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - import dan200.computercraft.ComputerCraft; import dan200.computercraft.core.apis.handles.ArrayByteChannel; import dan200.computercraft.core.apis.handles.BinaryReadableHandle; @@ -29,22 +18,20 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.*; -public final class HttpRequestHandler extends SimpleChannelInboundHandler implements Closeable { +import java.io.Closeable; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static dan200.computercraft.core.apis.http.request.HttpRequest.getHeaderSize; + +public final class HttpRequestHandler extends SimpleChannelInboundHandler implements Closeable +{ /** * Same as {@link io.netty.handler.codec.MessageAggregator}. */ @@ -53,16 +40,19 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler 0) { - URI redirect = this.getRedirect(response.status(), response.headers()); - if (redirect != null && !this.uri.equals(redirect) && this.request.redirects.getAndDecrement() > 0) { + if( request.redirects.get() > 0 ) + { + URI redirect = getRedirect( response.status(), response.headers() ); + if( redirect != null && !uri.equals( redirect ) && request.redirects.getAndDecrement() > 0 ) + { // If we have a redirect, and don't end up at the same place, then follow it. // We mark ourselves as disposed first though, to avoid firing events when the channel // becomes inactive or disposed. - this.closed = true; + closed = true; ctx.close(); - try { - HttpRequest.checkUri(redirect); - } catch (HTTPRequestException e) { + try + { + HttpRequest.checkUri( redirect ); + } + catch( HTTPRequestException e ) + { // If we cannot visit this uri, then fail. - this.request.failure(e.getMessage()); + request.failure( e.getMessage() ); return; } - this.request.request(redirect, - response.status() - .code() == 303 ? HttpMethod.GET : this.method); + request.request( redirect, response.status().code() == 303 ? HttpMethod.GET : method ); return; } } - this.responseCharset = HttpUtil.getCharset(response, StandardCharsets.UTF_8); - this.responseStatus = response.status(); - this.responseHeaders.add(response.headers()); + responseCharset = HttpUtil.getCharset( response, StandardCharsets.UTF_8 ); + responseStatus = response.status(); + responseHeaders.add( response.headers() ); } - if (message instanceof HttpContent) { + if( message instanceof HttpContent ) + { HttpContent content = (HttpContent) message; - if (this.responseBody == null) { - this.responseBody = ctx.alloc() - .compositeBuffer(DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS); + if( responseBody == null ) + { + responseBody = ctx.alloc().compositeBuffer( DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS ); } ByteBuf partial = content.content(); - if (partial.isReadable()) { + if( partial.isReadable() ) + { // If we've read more than we're allowed to handle, abort as soon as possible. - if (this.options.maxDownload != 0 && this.responseBody.readableBytes() + partial.readableBytes() > this.options.maxDownload) { - this.closed = true; + if( options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload ) + { + closed = true; ctx.close(); - this.request.failure("Response is too large"); + request.failure( "Response is too large" ); return; } - this.responseBody.addComponent(true, partial.retain()); + responseBody.addComponent( true, partial.retain() ); } - if (message instanceof LastHttpContent) { + if( message instanceof LastHttpContent ) + { LastHttpContent last = (LastHttpContent) message; - this.responseHeaders.add(last.trailingHeaders()); + responseHeaders.add( last.trailingHeaders() ); // Set the content length, if not already given. - if (this.responseHeaders.contains(HttpHeaderNames.CONTENT_LENGTH)) { - this.responseHeaders.set(HttpHeaderNames.CONTENT_LENGTH, this.responseBody.readableBytes()); + if( responseHeaders.contains( HttpHeaderNames.CONTENT_LENGTH ) ) + { + responseHeaders.set( HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes() ); } ctx.close(); - this.sendResponse(); + sendResponse(); } } } + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) + { + if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error handling HTTP response", cause ); + request.failure( cause ); + } + + private void sendResponse() + { + // Read the ByteBuf into a channel. + CompositeByteBuf body = responseBody; + byte[] bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes( body ); + + // Decode the headers + HttpResponseStatus status = responseStatus; + Map headers = new HashMap<>(); + for( Map.Entry header : responseHeaders ) + { + String existing = headers.get( header.getKey() ); + headers.put( header.getKey(), existing == null ? header.getValue() : existing + "," + header.getValue() ); + } + + // Fire off a stats event + request.environment().addTrackingChange( TrackingField.HTTP_DOWNLOAD, getHeaderSize( responseHeaders ) + bytes.length ); + + // Prepare to queue an event + ArrayByteChannel contents = new ArrayByteChannel( bytes ); + HandleGeneric reader = request.isBinary() + ? BinaryReadableHandle.of( contents ) + : new EncodedReadableHandle( EncodedReadableHandle.open( contents, responseCharset ) ); + HttpResponseHandle stream = new HttpResponseHandle( reader, status.code(), status.reasonPhrase(), headers ); + + if( status.code() >= 200 && status.code() < 400 ) + { + request.success( stream ); + } + else + { + request.failure( status.reasonPhrase(), stream ); + } + } + /** * Determine the redirect from this response. * - * @param status The status of the HTTP response. + * @param status The status of the HTTP response. * @param headers The headers of the HTTP response. * @return The URI to redirect to, or {@code null} if no redirect should occur. */ - private URI getRedirect(HttpResponseStatus status, HttpHeaders headers) { + private URI getRedirect( HttpResponseStatus status, HttpHeaders headers ) + { int code = status.code(); - if (code < 300 || code > 307 || code == 304 || code == 306) { + if( code < 300 || code > 307 || code == 304 || code == 306 ) return null; + + String location = headers.get( HttpHeaderNames.LOCATION ); + if( location == null ) return null; + + try + { + return uri.resolve( new URI( location ) ); + } + catch( IllegalArgumentException | URISyntaxException e ) + { return null; } - - String location = headers.get(HttpHeaderNames.LOCATION); - if (location == null) { - return null; - } - - try { - return this.uri.resolve(new URI( location )); - } catch( IllegalArgumentException | URISyntaxException e ) { - return null; - } - } - - private void sendResponse() { - // Read the ByteBuf into a channel. - CompositeByteBuf body = this.responseBody; - byte[] bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes(body); - - // Decode the headers - HttpResponseStatus status = this.responseStatus; - Map headers = new HashMap<>(); - for (Map.Entry header : this.responseHeaders) { - String existing = headers.get(header.getKey()); - headers.put(header.getKey(), existing == null ? header.getValue() : existing + "," + header.getValue()); - } - - // Fire off a stats event - this.request.environment() - .addTrackingChange(TrackingField.HTTP_DOWNLOAD, getHeaderSize(this.responseHeaders) + bytes.length); - - // Prepare to queue an event - ArrayByteChannel contents = new ArrayByteChannel(bytes); - HandleGeneric reader = this.request.isBinary() ? BinaryReadableHandle.of(contents) : new EncodedReadableHandle(EncodedReadableHandle.open(contents, - this.responseCharset)); - HttpResponseHandle stream = new HttpResponseHandle(reader, status.code(), status.reasonPhrase(), headers); - - if (status.code() >= 200 && status.code() < 400) { - this.request.success(stream); - } else { - this.request.failure(status.reasonPhrase(), stream); - } } @Override - public void close() { - this.closed = true; - if (this.responseBody != null) { - this.responseBody.release(); - this.responseBody = null; + public void close() + { + closed = true; + if( responseBody != null ) + { + responseBody.release(); + responseBody = null; } } -} +} \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java index 7dd5e7e6e..e600880c0 100644 --- a/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -64,9 +64,9 @@ public final class Generator { private final Function wrap; private final LoadingCache> methodCache = CacheBuilder.newBuilder() - .build(CacheLoader.from(this::build)); + .build(CacheLoader.from(catching(this::build, Optional.empty()))); private final LoadingCache, List>> classCache = CacheBuilder.newBuilder() - .build(CacheLoader.from(this::build)); + .build(CacheLoader.from(catching(this::build, Collections.emptyList()))); Generator(Class base, List> context, Function wrap) { this.base = base; @@ -374,4 +374,22 @@ public final class Generator { method.getName()); return null; } + + @SuppressWarnings( "Guava" ) + private static com.google.common.base.Function catching( Function function, U def ) + { + return x -> { + try + { + return function.apply( x ); + } + catch( Exception | LinkageError e ) + { + // LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching + // methods on a class which references non-existent (i.e. client-only) types. + ComputerCraft.log.error( "Error generating @LuaFunctions", e ); + return def; + } + }; + } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java index 0a2ebab45..f5de836bc 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java +++ b/src/main/java/dan200/computercraft/core/filesystem/ChannelWrapper.java @@ -3,7 +3,6 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.filesystem; import java.io.Closeable; @@ -13,30 +12,38 @@ import java.nio.channels.Channel; /** * Wraps some closeable object such as a buffered writer, and the underlying stream. * - * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown this causes us to release the channel, - * but not actually close it. This wrapper will attempt to close the wrapper (and so hopefully flush the channel), and then close the underlying channel. + * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown + * this causes us to release the channel, but not actually close it. This wrapper will attempt to close the wrapper (and + * so hopefully flush the channel), and then close the underlying channel. * * @param The type of the closeable object to write. */ -class ChannelWrapper implements Closeable { +class ChannelWrapper implements Closeable +{ private final T wrapper; private final Channel channel; - ChannelWrapper(T wrapper, Channel channel) { + ChannelWrapper( T wrapper, Channel channel ) + { this.wrapper = wrapper; this.channel = channel; } @Override - public void close() throws IOException { - try { - this.wrapper.close(); - } finally { - this.channel.close(); + public void close() throws IOException + { + try + { + wrapper.close(); + } + finally + { + channel.close(); } } - public T get() { - return this.wrapper; + T get() + { + return wrapper; } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java index fd66c09ae..cfbc9d92e 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java @@ -3,9 +3,16 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.filesystem; +import com.google.common.io.ByteStreams; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.filesystem.IFileSystem; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; import java.io.Closeable; import java.io.IOException; import java.lang.ref.Reference; @@ -16,506 +23,598 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.AccessDeniedException; import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.OptionalLong; -import java.util.Stack; +import java.util.*; import java.util.function.Function; import java.util.regex.Pattern; -import javax.annotation.Nonnull; - -import com.google.common.io.ByteStreams; -import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.filesystem.IFileSystem; -import dan200.computercraft.api.filesystem.IMount; -import dan200.computercraft.api.filesystem.IWritableMount; -import dan200.computercraft.shared.util.IoUtil; - -public class FileSystem { +public class FileSystem +{ /** * Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into. * - * This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This exists to prevent it overflowing if it - * ever gets into an infinite loop. + * This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This + * exists to prevent it overflowing if it ever gets into an infinite loop. */ private static final int MAX_COPY_DEPTH = 128; - private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$"); - private final FileSystemWrapperMount m_wrapper = new FileSystemWrapperMount(this); - private final Map mounts = new HashMap<>(); - private final HashMap>, ChannelWrapper> m_openFiles = new HashMap<>(); - private final ReferenceQueue> m_openFileQueue = new ReferenceQueue<>(); - public FileSystem(String rootLabel, IMount rootMount) throws FileSystemException { - this.mount(rootLabel, "", rootMount); + private final FileSystemWrapperMount wrapper = new FileSystemWrapperMount( this ); + private final Map mounts = new HashMap<>(); + + private final HashMap>, ChannelWrapper> openFiles = new HashMap<>(); + private final ReferenceQueue> openFileQueue = new ReferenceQueue<>(); + + public FileSystem( String rootLabel, IMount rootMount ) throws FileSystemException + { + mount( rootLabel, "", rootMount ); } - public synchronized void mount(String label, String location, IMount mount) throws FileSystemException { - if (mount == null) { + public FileSystem( String rootLabel, IWritableMount rootMount ) throws FileSystemException + { + mountWritable( rootLabel, "", rootMount ); + } + + public void close() + { + // Close all dangling open files + synchronized( openFiles ) + { + for( Closeable file : openFiles.values() ) IoUtil.closeQuietly( file ); + openFiles.clear(); + while( openFileQueue.poll() != null ) ; + } + } + + public synchronized void mount( String label, String location, IMount mount ) throws FileSystemException + { + if( mount == null ) throw new NullPointerException(); + location = sanitizePath( location ); + if( location.contains( ".." ) ) throw new FileSystemException( "Cannot mount below the root" ); + mount( new MountWrapper( label, location, mount ) ); + } + + public synchronized void mountWritable( String label, String location, IWritableMount mount ) throws FileSystemException + { + if( mount == null ) + { throw new NullPointerException(); } - location = sanitizePath(location); - if (location.contains("..")) { - throw new FileSystemException("Cannot mount below the root"); + location = sanitizePath( location ); + if( location.contains( ".." ) ) + { + throw new FileSystemException( "Cannot mount below the root" ); } - this.mount(new MountWrapper(label, location, mount)); + mount( new MountWrapper( label, location, mount ) ); } - private static String sanitizePath(String path) { - return sanitizePath(path, false); - } - - private synchronized void mount(MountWrapper wrapper) { + private synchronized void mount( MountWrapper wrapper ) + { String location = wrapper.getLocation(); - this.mounts.remove(location); - this.mounts.put(location, wrapper); + mounts.remove( location ); + mounts.put( location, wrapper ); } - public static String sanitizePath(String path, boolean allowWildcards) { + public synchronized void unmount( String path ) + { + MountWrapper mount = mounts.remove( sanitizePath( path ) ); + if( mount == null ) return; + + cleanup(); + + // Close any files which belong to this mount - don't want people writing to a disk after it's been ejected! + // There's no point storing a Mount -> Wrapper[] map, as openFiles is small and unmount isn't called very + // often. + synchronized( openFiles ) + { + for( Iterator>> iterator = openFiles.keySet().iterator(); iterator.hasNext(); ) + { + WeakReference> reference = iterator.next(); + FileSystemWrapper wrapper = reference.get(); + if( wrapper == null ) continue; + + if( wrapper.mount == mount ) + { + wrapper.closeExternally(); + iterator.remove(); + } + } + } + } + + public String combine( String path, String childPath ) + { + path = sanitizePath( path, true ); + childPath = sanitizePath( childPath, true ); + + if( path.isEmpty() ) + { + return childPath; + } + else if( childPath.isEmpty() ) + { + return path; + } + else + { + return sanitizePath( path + '/' + childPath, true ); + } + } + + public static String getDirectory( String path ) + { + path = sanitizePath( path, true ); + if( path.isEmpty() ) + { + return ".."; + } + + int lastSlash = path.lastIndexOf( '/' ); + if( lastSlash >= 0 ) + { + return path.substring( 0, lastSlash ); + } + else + { + return ""; + } + } + + public static String getName( String path ) + { + path = sanitizePath( path, true ); + if( path.isEmpty() ) return "root"; + + int lastSlash = path.lastIndexOf( '/' ); + return lastSlash >= 0 ? path.substring( lastSlash + 1 ) : path; + } + + public synchronized long getSize( String path ) throws FileSystemException + { + return getMount( sanitizePath( path ) ).getSize( sanitizePath( path ) ); + } + + public synchronized BasicFileAttributes getAttributes( String path ) throws FileSystemException + { + return getMount( sanitizePath( path ) ).getAttributes( sanitizePath( path ) ); + } + + public synchronized String[] list( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + + // Gets a list of the files in the mount + List list = new ArrayList<>(); + mount.list( path, list ); + + // Add any mounts that are mounted at this location + for( MountWrapper otherMount : mounts.values() ) + { + if( getDirectory( otherMount.getLocation() ).equals( path ) ) + { + list.add( getName( otherMount.getLocation() ) ); + } + } + + // Return list + String[] array = new String[list.size()]; + list.toArray( array ); + Arrays.sort( array ); + return array; + } + + private void findIn( String dir, List matches, Pattern wildPattern ) throws FileSystemException + { + String[] list = list( dir ); + for( String entry : list ) + { + String entryPath = dir.isEmpty() ? entry : dir + "/" + entry; + if( wildPattern.matcher( entryPath ).matches() ) + { + matches.add( entryPath ); + } + if( isDir( entryPath ) ) + { + findIn( entryPath, matches, wildPattern ); + } + } + } + + public synchronized String[] find( String wildPath ) throws FileSystemException + { + // Match all the files on the system + wildPath = sanitizePath( wildPath, true ); + + // If we don't have a wildcard at all just check the file exists + int starIndex = wildPath.indexOf( '*' ); + if( starIndex == -1 ) + { + return exists( wildPath ) ? new String[] { wildPath } : new String[0]; + } + + // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar + int prevDir = wildPath.substring( 0, starIndex ).lastIndexOf( '/' ); + String startDir = prevDir == -1 ? "" : wildPath.substring( 0, prevDir ); + + // If this isn't a directory then just abort + if( !isDir( startDir ) ) return new String[0]; + + // Scan as normal, starting from this directory + Pattern wildPattern = Pattern.compile( "^\\Q" + wildPath.replaceAll( "\\*", "\\\\E[^\\\\/]*\\\\Q" ) + "\\E$" ); + List matches = new ArrayList<>(); + findIn( startDir, matches, wildPattern ); + + // Return matches + String[] array = new String[matches.size()]; + matches.toArray( array ); + return array; + } + + public synchronized boolean exists( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.exists( path ); + } + + public synchronized boolean isDir( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.isDirectory( path ); + } + + public synchronized boolean isReadOnly( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.isReadOnly( path ); + } + + public synchronized String getMountLabel( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getLabel(); + } + + public synchronized void makeDir( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + mount.makeDirectory( path ); + } + + public synchronized void delete( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + mount.delete( path ); + } + + public synchronized void move( String sourcePath, String destPath ) throws FileSystemException + { + sourcePath = sanitizePath( sourcePath ); + destPath = sanitizePath( destPath ); + if( isReadOnly( sourcePath ) || isReadOnly( destPath ) ) + { + throw new FileSystemException( "Access denied" ); + } + if( !exists( sourcePath ) ) + { + throw new FileSystemException( "No such file" ); + } + if( exists( destPath ) ) + { + throw new FileSystemException( "File exists" ); + } + if( contains( sourcePath, destPath ) ) + { + throw new FileSystemException( "Can't move a directory inside itself" ); + } + copy( sourcePath, destPath ); + delete( sourcePath ); + } + + public synchronized void copy( String sourcePath, String destPath ) throws FileSystemException + { + sourcePath = sanitizePath( sourcePath ); + destPath = sanitizePath( destPath ); + if( isReadOnly( destPath ) ) + { + throw new FileSystemException( "/" + destPath + ": Access denied" ); + } + if( !exists( sourcePath ) ) + { + throw new FileSystemException( "/" + sourcePath + ": No such file" ); + } + if( exists( destPath ) ) + { + throw new FileSystemException( "/" + destPath + ": File exists" ); + } + if( contains( sourcePath, destPath ) ) + { + throw new FileSystemException( "/" + sourcePath + ": Can't copy a directory inside itself" ); + } + copyRecursive( sourcePath, getMount( sourcePath ), destPath, getMount( destPath ), 0 ); + } + + private synchronized void copyRecursive( String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, int depth ) throws FileSystemException + { + if( !sourceMount.exists( sourcePath ) ) return; + if( depth >= MAX_COPY_DEPTH ) throw new FileSystemException( "Too many directories to copy" ); + + if( sourceMount.isDirectory( sourcePath ) ) + { + // Copy a directory: + // Make the new directory + destinationMount.makeDirectory( destinationPath ); + + // Copy the source contents into it + List sourceChildren = new ArrayList<>(); + sourceMount.list( sourcePath, sourceChildren ); + for( String child : sourceChildren ) + { + copyRecursive( + combine( sourcePath, child ), sourceMount, + combine( destinationPath, child ), destinationMount, + depth + 1 + ); + } + } + else + { + // Copy a file: + try( ReadableByteChannel source = sourceMount.openForRead( sourcePath ); + WritableByteChannel destination = destinationMount.openForWrite( destinationPath ) ) + { + // Copy bytes as fast as we can + ByteStreams.copy( source, destination ); + } + catch( AccessDeniedException e ) + { + throw new FileSystemException( "Access denied" ); + } + catch( IOException e ) + { + throw new FileSystemException( e.getMessage() ); + } + } + } + + private void cleanup() + { + synchronized( openFiles ) + { + Reference ref; + while( (ref = openFileQueue.poll()) != null ) + { + IoUtil.closeQuietly( openFiles.remove( ref ) ); + } + } + } + + private synchronized FileSystemWrapper openFile( @Nonnull MountWrapper mount, @Nonnull Channel channel, @Nonnull T file ) throws FileSystemException + { + synchronized( openFiles ) + { + if( ComputerCraft.maximumFilesOpen > 0 && + openFiles.size() >= ComputerCraft.maximumFilesOpen ) + { + IoUtil.closeQuietly( file ); + IoUtil.closeQuietly( channel ); + throw new FileSystemException( "Too many files already open" ); + } + + ChannelWrapper channelWrapper = new ChannelWrapper<>( file, channel ); + FileSystemWrapper fsWrapper = new FileSystemWrapper<>( this, mount, channelWrapper, openFileQueue ); + openFiles.put( fsWrapper.self, channelWrapper ); + return fsWrapper; + } + } + + void removeFile( FileSystemWrapper handle ) + { + synchronized( openFiles ) + { + openFiles.remove( handle.self ); + } + } + + public synchronized FileSystemWrapper openForRead( String path, Function open ) throws FileSystemException + { + cleanup(); + + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + ReadableByteChannel channel = mount.openForRead( path ); + return channel != null ? openFile( mount, channel, open.apply( channel ) ) : null; + } + + public synchronized FileSystemWrapper openForWrite( String path, boolean append, Function open ) throws FileSystemException + { + cleanup(); + + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + WritableByteChannel channel = append ? mount.openForAppend( path ) : mount.openForWrite( path ); + return channel != null ? openFile( mount, channel, open.apply( channel ) ) : null; + } + + public synchronized long getFreeSpace( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getFreeSpace(); + } + + @Nonnull + public synchronized OptionalLong getCapacity( String path ) throws FileSystemException + { + path = sanitizePath( path ); + MountWrapper mount = getMount( path ); + return mount.getCapacity(); + } + + private synchronized MountWrapper getMount( String path ) throws FileSystemException + { + // Return the deepest mount that contains a given path + Iterator it = mounts.values().iterator(); + MountWrapper match = null; + int matchLength = 999; + while( it.hasNext() ) + { + MountWrapper mount = it.next(); + if( contains( mount.getLocation(), path ) ) + { + int len = toLocal( path, mount.getLocation() ).length(); + if( match == null || len < matchLength ) + { + match = mount; + matchLength = len; + } + } + } + if( match == null ) + { + throw new FileSystemException( "/" + path + ": Invalid Path" ); + } + return match; + } + + public IFileSystem getMountWrapper() + { + return wrapper; + } + + private static String sanitizePath( String path ) + { + return sanitizePath( path, false ); + } + + private static final Pattern threeDotsPattern = Pattern.compile( "^\\.{3,}$" ); + + public static String sanitizePath( String path, boolean allowWildcards ) + { // Allow windowsy slashes - path = path.replace('\\', '/'); + path = path.replace( '\\', '/' ); // Clean the path or illegal characters. final char[] specialChars = new char[] { - '"', - ':', - '<', - '>', - '?', - '|', - // Sorted by ascii value (important) + '"', ':', '<', '>', '?', '|', // Sorted by ascii value (important) }; StringBuilder cleanName = new StringBuilder(); - for (int i = 0; i < path.length(); i++) { - char c = path.charAt(i); - if (c >= 32 && Arrays.binarySearch(specialChars, c) < 0 && (allowWildcards || c != '*')) { - cleanName.append(c); + for( int i = 0; i < path.length(); i++ ) + { + char c = path.charAt( i ); + if( c >= 32 && Arrays.binarySearch( specialChars, c ) < 0 && (allowWildcards || c != '*') ) + { + cleanName.append( c ); } } path = cleanName.toString(); // Collapse the string into its component parts, removing ..'s - String[] parts = path.split("/"); + String[] parts = path.split( "/" ); Stack outputParts = new Stack<>(); - for (String part : parts) { - if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part) - .matches()) { + for( String part : parts ) + { + if( part.isEmpty() || part.equals( "." ) || threeDotsPattern.matcher( part ).matches() ) + { // . is redundant // ... and more are treated as . continue; } - if (part.equals("..")) { + if( part.equals( ".." ) ) + { // .. can cancel out the last folder entered - if (!outputParts.empty()) { + if( !outputParts.empty() ) + { String top = outputParts.peek(); - if (!top.equals("..")) { + if( !top.equals( ".." ) ) + { outputParts.pop(); - } else { - outputParts.push(".."); } - } else { - outputParts.push(".."); + else + { + outputParts.push( ".." ); + } } - } else if (part.length() >= 255) { + else + { + outputParts.push( ".." ); + } + } + else if( part.length() >= 255 ) + { // If part length > 255 and it is the last part - outputParts.push(part.substring(0, 255)); - } else { + outputParts.push( part.substring( 0, 255 ) ); + } + else + { // Anything else we add to the stack - outputParts.push(part); + outputParts.push( part ); } } // Recombine the output parts into a new string StringBuilder result = new StringBuilder(); Iterator it = outputParts.iterator(); - while (it.hasNext()) { + while( it.hasNext() ) + { String part = it.next(); - result.append(part); - if (it.hasNext()) { - result.append('/'); + result.append( part ); + if( it.hasNext() ) + { + result.append( '/' ); } } return result.toString(); } - public FileSystem(String rootLabel, IWritableMount rootMount) throws FileSystemException { - this.mountWritable(rootLabel, "", rootMount); - } + public static boolean contains( String pathA, String pathB ) + { + pathA = sanitizePath( pathA ).toLowerCase( Locale.ROOT ); + pathB = sanitizePath( pathB ).toLowerCase( Locale.ROOT ); - public synchronized void mountWritable(String label, String location, IWritableMount mount) throws FileSystemException { - if (mount == null) { - throw new NullPointerException(); - } - location = sanitizePath(location); - if (location.contains("..")) { - throw new FileSystemException("Cannot mount below the root"); - } - this.mount(new MountWrapper(label, location, mount)); - } - - public static String getDirectory(String path) { - path = sanitizePath(path, true); - if (path.isEmpty()) { - return ".."; - } - - int lastSlash = path.lastIndexOf('/'); - if (lastSlash >= 0) { - return path.substring(0, lastSlash); - } else { - return ""; - } - } - - public static String getName(String path) { - path = sanitizePath(path, true); - if (path.isEmpty()) { - return "root"; - } - - int lastSlash = path.lastIndexOf('/'); - return lastSlash >= 0 ? path.substring(lastSlash + 1) : path; - } - - public static boolean contains(String pathA, String pathB) { - pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT); - pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT); - - if (pathB.equals("..")) { + if( pathB.equals( ".." ) ) + { return false; - } else if (pathB.startsWith("../")) { + } + else if( pathB.startsWith( "../" ) ) + { return false; - } else if (pathB.equals(pathA)) { + } + else if( pathB.equals( pathA ) ) + { return true; - } else if (pathA.isEmpty()) { + } + else if( pathA.isEmpty() ) + { return true; - } else { - return pathB.startsWith(pathA + "/"); + } + else + { + return pathB.startsWith( pathA + "/" ); } } - public static String toLocal(String path, String location) { - path = sanitizePath(path); - location = sanitizePath(location); + public static String toLocal( String path, String location ) + { + path = sanitizePath( path ); + location = sanitizePath( location ); - assert contains(location, path); - String local = path.substring(location.length()); - if (local.startsWith("/")) { - return local.substring(1); - } else { + assert contains( location, path ); + String local = path.substring( location.length() ); + if( local.startsWith( "/" ) ) + { + return local.substring( 1 ); + } + else + { return local; } } - - public void close() { - // Close all dangling open files - synchronized (this.m_openFiles) { - for (Closeable file : this.m_openFiles.values()) { - IoUtil.closeQuietly(file); - } - this.m_openFiles.clear(); - while (this.m_openFileQueue.poll() != null) { - } - } - } - - public synchronized void unmount(String path) { - this.mounts.remove(sanitizePath(path)); - } - - public String combine( String path, String childPath ) { - path = sanitizePath(path, true); - childPath = sanitizePath(childPath, true); - - if (path.isEmpty()) { - return childPath; - } else if (childPath.isEmpty()) { - return path; - } else { - return sanitizePath(path + '/' + childPath, true); - } - } - - public synchronized long getSize(String path) throws FileSystemException { - return this.getMount(sanitizePath(path)).getSize(sanitizePath(path)); - } - - public synchronized BasicFileAttributes getAttributes(String path) throws FileSystemException { - return this.getMount(sanitizePath(path)).getAttributes(sanitizePath(path)); - } - - public synchronized String[] list(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - - // Gets a list of the files in the mount - List list = new ArrayList<>(); - mount.list(path, list); - - // Add any mounts that are mounted at this location - for (MountWrapper otherMount : this.mounts.values()) { - if (getDirectory(otherMount.getLocation()).equals(path)) { - list.add(getName(otherMount.getLocation())); - } - } - - // Return list - String[] array = new String[list.size()]; - list.toArray(array); - Arrays.sort(array); - return array; - } - - private void findIn(String dir, List matches, Pattern wildPattern) throws FileSystemException { - String[] list = this.list(dir); - for (String entry : list) { - String entryPath = dir.isEmpty() ? entry : dir + "/" + entry; - if (wildPattern.matcher(entryPath) - .matches()) { - matches.add(entryPath); - } - if (this.isDir(entryPath)) { - this.findIn(entryPath, matches, wildPattern); - } - } - } - - public synchronized String[] find(String wildPath) throws FileSystemException { - // Match all the files on the system - wildPath = sanitizePath(wildPath, true); - - // If we don't have a wildcard at all just check the file exists - int starIndex = wildPath.indexOf('*'); - if (starIndex == -1) { - return this.exists(wildPath) ? new String[] {wildPath} : new String[0]; - } - - // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar - int prevDir = wildPath.substring(0, starIndex) - .lastIndexOf('/'); - String startDir = prevDir == -1 ? "" : wildPath.substring(0, prevDir); - - // If this isn't a directory then just abort - if (!this.isDir(startDir)) { - return new String[0]; - } - - // Scan as normal, starting from this directory - Pattern wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$"); - List matches = new ArrayList<>(); - this.findIn(startDir, matches, wildPattern); - - // Return matches - String[] array = new String[matches.size()]; - matches.toArray(array); - return array; - } - - public synchronized boolean exists(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.exists(path); - } - - public synchronized boolean isDir(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.isDirectory(path); - } - - public synchronized boolean isReadOnly(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.isReadOnly(path); - } - - public synchronized String getMountLabel(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.getLabel(); - } - - public synchronized void makeDir(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - mount.makeDirectory(path); - } - - public synchronized void delete(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - mount.delete(path); - } - - public synchronized void move(String sourcePath, String destPath) throws FileSystemException { - sourcePath = sanitizePath(sourcePath); - destPath = sanitizePath(destPath); - if (this.isReadOnly(sourcePath) || this.isReadOnly(destPath)) { - throw new FileSystemException("Access denied"); - } - if (!this.exists(sourcePath)) { - throw new FileSystemException("No such file"); - } - if (this.exists(destPath)) { - throw new FileSystemException("File exists"); - } - if (contains(sourcePath, destPath)) { - throw new FileSystemException("Can't move a directory inside itself"); - } - this.copy(sourcePath, destPath); - this.delete(sourcePath); - } - - public synchronized void copy(String sourcePath, String destPath) throws FileSystemException { - sourcePath = sanitizePath(sourcePath); - destPath = sanitizePath(destPath); - if (this.isReadOnly(destPath)) { - throw new FileSystemException("/" + destPath + ": Access denied"); - } - if (!this.exists(sourcePath)) { - throw new FileSystemException("/" + sourcePath + ": No such file"); - } - if (this.exists(destPath)) { - throw new FileSystemException("/" + destPath + ": File exists"); - } - if (contains(sourcePath, destPath)) { - throw new FileSystemException("/" + sourcePath + ": Can't copy a directory inside itself"); - } - this.copyRecursive(sourcePath, this.getMount(sourcePath), destPath, this.getMount(destPath), 0); - } - - private synchronized void copyRecursive(String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, - int depth) throws FileSystemException { - if (!sourceMount.exists(sourcePath)) { - return; - } - if (depth >= MAX_COPY_DEPTH) { - throw new FileSystemException("Too many directories to copy"); - } - - if (sourceMount.isDirectory(sourcePath)) { - // Copy a directory: - // Make the new directory - destinationMount.makeDirectory(destinationPath); - - // Copy the source contents into it - List sourceChildren = new ArrayList<>(); - sourceMount.list(sourcePath, sourceChildren); - for (String child : sourceChildren) { - this.copyRecursive(this.combine(sourcePath, child), sourceMount, this.combine(destinationPath, child), destinationMount, depth + 1); - } - } else { - // Copy a file: - try (ReadableByteChannel source = sourceMount.openForRead(sourcePath); WritableByteChannel destination = destinationMount.openForWrite( - destinationPath)) { - // Copy bytes as fast as we can - ByteStreams.copy(source, destination); - } catch (AccessDeniedException e) { - throw new FileSystemException("Access denied"); - } catch (IOException e) { - throw new FileSystemException(e.getMessage()); - } - } - } - - private void cleanup() { - synchronized (this.m_openFiles) { - Reference ref; - while ((ref = this.m_openFileQueue.poll()) != null) { - IoUtil.closeQuietly(this.m_openFiles.remove(ref)); - } - } - } - - private synchronized FileSystemWrapper openFile(@Nonnull Channel channel, @Nonnull T file) throws FileSystemException { - synchronized (this.m_openFiles) { - if (ComputerCraft.maximumFilesOpen > 0 && this.m_openFiles.size() >= ComputerCraft.maximumFilesOpen) { - IoUtil.closeQuietly(file); - IoUtil.closeQuietly(channel); - throw new FileSystemException("Too many files already open"); - } - - ChannelWrapper channelWrapper = new ChannelWrapper<>(file, channel); - FileSystemWrapper fsWrapper = new FileSystemWrapper<>(this, channelWrapper, this.m_openFileQueue); - this.m_openFiles.put(fsWrapper.self, channelWrapper); - return fsWrapper; - } - } - - synchronized void removeFile(FileSystemWrapper handle) { - synchronized (this.m_openFiles) { - this.m_openFiles.remove(handle.self); - } - } - - public synchronized FileSystemWrapper openForRead(String path, Function open) throws FileSystemException { - this.cleanup(); - - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - ReadableByteChannel channel = mount.openForRead(path); - if (channel != null) { - return this.openFile(channel, open.apply(channel)); - } - return null; - } - - public synchronized FileSystemWrapper openForWrite(String path, boolean append, Function open) throws FileSystemException { - this.cleanup(); - - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - WritableByteChannel channel = append ? mount.openForAppend(path) : mount.openForWrite(path); - if (channel != null) { - return this.openFile(channel, open.apply(channel)); - } - return null; - } - - public synchronized long getFreeSpace(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.getFreeSpace(); - } - - @Nonnull - public synchronized OptionalLong getCapacity(String path) throws FileSystemException { - path = sanitizePath(path); - MountWrapper mount = this.getMount(path); - return mount.getCapacity(); - } - - private synchronized MountWrapper getMount(String path) throws FileSystemException { - // Return the deepest mount that contains a given path - Iterator it = this.mounts.values() - .iterator(); - MountWrapper match = null; - int matchLength = 999; - while (it.hasNext()) { - MountWrapper mount = it.next(); - if (contains(mount.getLocation(), path)) { - int len = toLocal(path, mount.getLocation()).length(); - if (match == null || len < matchLength) { - match = mount; - matchLength = len; - } - } - } - if (match == null) { - throw new FileSystemException("/" + path + ": Invalid Path"); - } - return match; - } - - public IFileSystem getMountWrapper() { - return this.m_wrapper; - } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java index e3a359478..a65e04326 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java +++ b/src/main/java/dan200/computercraft/core/filesystem/FileSystemWrapper.java @@ -3,48 +3,68 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.filesystem; +import dan200.computercraft.shared.util.IoUtil; + +import javax.annotation.Nonnull; import java.io.Closeable; import java.io.IOException; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; -import javax.annotation.Nonnull; - /** * An alternative closeable implementation that will free up resources in the filesystem. * - * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of (say, the Lua object referencing it has - * gone), then the wrapped object will be closed by the filesystem. + * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of + * (say, the Lua object referencing it has gone), then the wrapped object will be closed by the filesystem. * * Closing this will stop the filesystem tracking it, reducing the current descriptor count. * - * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks on the stream, it's not really possible as it'd require - * numerous instances. + * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks + * on the stream, it's not really possible as it'd require numerous instances. * * @param The type of writer or channel to wrap. */ -public class FileSystemWrapper implements Closeable { - final WeakReference> self; +public class FileSystemWrapper implements TrackingCloseable +{ private final FileSystem fileSystem; + final MountWrapper mount; private final ChannelWrapper closeable; + final WeakReference> self; + private boolean isOpen = true; - FileSystemWrapper(FileSystem fileSystem, ChannelWrapper closeable, ReferenceQueue> queue) { + FileSystemWrapper( FileSystem fileSystem, MountWrapper mount, ChannelWrapper closeable, ReferenceQueue> queue ) + { this.fileSystem = fileSystem; + this.mount = mount; this.closeable = closeable; - this.self = new WeakReference<>(this, queue); + self = new WeakReference<>( this, queue ); } @Override - public void close() throws IOException { - this.fileSystem.removeFile(this); - this.closeable.close(); + public void close() throws IOException + { + isOpen = false; + fileSystem.removeFile( this ); + closeable.close(); + } + + void closeExternally() + { + isOpen = false; + IoUtil.closeQuietly( closeable ); + } + + @Override + public boolean isOpen() + { + return isOpen; } @Nonnull - public T get() { - return this.closeable.get(); + public T get() + { + return closeable.get(); } } diff --git a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java index a9f2b243c..e767525b6 100644 --- a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java +++ b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java @@ -90,19 +90,30 @@ public final class ResourceMount implements IMount { private void load() { boolean hasAny = false; - FileEntry newRoot = new FileEntry(new Identifier(this.namespace, this.subPath)); - for (Identifier file : this.manager.findResources(this.subPath, s -> true)) { - if (!file.getNamespace() - .equals(this.namespace)) { - continue; - } + String existingNamespace = null; + + FileEntry newRoot = new FileEntry( new Identifier( namespace, subPath ) ); + for( Identifier file : manager.findResources( subPath, s -> true ) ) + { + existingNamespace = file.getNamespace(); + + if( !file.getNamespace().equals( namespace ) ) continue; String localPath = FileSystem.toLocal(file.getPath(), this.subPath); this.create(newRoot, localPath); hasAny = true; } - this.root = hasAny ? newRoot : null; + root = hasAny ? newRoot : null; + + if( !hasAny ) + { + ComputerCraft.log.warn("Cannot find any files under /data/{}/{} for resource mount.", namespace, subPath); + if( newRoot != null ) + { + ComputerCraft.log.warn("There are files under /data/{}/{} though. Did you get the wrong namespace?", existingNamespace, subPath); + } + } } private void create(FileEntry lastEntry, String path) { diff --git a/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java b/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java new file mode 100644 index 000000000..19ffc978f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/filesystem/TrackingCloseable.java @@ -0,0 +1,44 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.filesystem; + +import java.io.Closeable; +import java.io.IOException; + +/** + * A {@link Closeable} which knows when it has been closed. + * + * This is a quick (though racey) way of providing more friendly (and more similar to Lua) + * error messages to the user. + */ +public interface TrackingCloseable extends Closeable +{ + boolean isOpen(); + + class Impl implements TrackingCloseable + { + private final Closeable object; + private boolean isOpen = true; + + public Impl( Closeable object ) + { + this.object = object; + } + + @Override + public boolean isOpen() + { + return isOpen; + } + + @Override + public void close() throws IOException + { + isOpen = false; + object.close(); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java b/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java index 8047cc8b7..44d5bcd2c 100644 --- a/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java +++ b/src/main/java/dan200/computercraft/core/terminal/TextBuffer.java @@ -3,168 +3,84 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.core.terminal; -public class TextBuffer { - private final char[] m_text; +public class TextBuffer +{ + private final char[] text; - public TextBuffer(char c, int length) { - this.m_text = new char[length]; - for (int i = 0; i < length; i++) { - this.m_text[i] = c; - } + public TextBuffer( char c, int length ) + { + text = new char[length]; + this.fill( c ); } - public TextBuffer(String text) { - this(text, 1); + public TextBuffer( String text ) + { + this.text = text.toCharArray(); } - public TextBuffer(String text, int repetitions) { - int textLength = text.length(); - this.m_text = new char[textLength * repetitions]; - for (int i = 0; i < repetitions; i++) { - for (int j = 0; j < textLength; j++) { - this.m_text[j + i * textLength] = text.charAt(j); - } - } + public int length() + { + return text.length; } - public TextBuffer(TextBuffer text) { - this(text, 1); + public void write( String text ) + { + write( text, 0 ); } - public TextBuffer(TextBuffer text, int repetitions) { - int textLength = text.length(); - this.m_text = new char[textLength * repetitions]; - for (int i = 0; i < repetitions; i++) { - for (int j = 0; j < textLength; j++) { - this.m_text[j + i * textLength] = text.charAt(j); - } - } - } - - public int length() { - return this.m_text.length; - } - - public char charAt(int i) { - return this.m_text[i]; - } - - public String read() { - return this.read(0, this.m_text.length); - } - - public String read(int start, int end) { - start = Math.max(start, 0); - end = Math.min(end, this.m_text.length); - int textLength = Math.max(end - start, 0); - return new String(this.m_text, start, textLength); - } - - public String read(int start) { - return this.read(start, this.m_text.length); - } - - public void write(String text) { - this.write(text, 0, text.length()); - } - - public void write(String text, int start, int end) { + public void write( String text, int start ) + { int pos = start; - start = Math.max(start, 0); - end = Math.min(end, pos + text.length()); - end = Math.min(end, this.m_text.length); - for (int i = start; i < end; i++) { - this.m_text[i] = text.charAt(i - pos); + start = Math.max( start, 0 ); + int end = Math.min( start + text.length(), pos + text.length() ); + end = Math.min( end, this.text.length ); + for( int i = start; i < end; i++ ) + { + this.text[i] = text.charAt( i - pos ); } } - public void write(String text, int start) { - this.write(text, start, start + text.length()); - } - - public void write(TextBuffer text) { - this.write(text, 0, text.length()); - } - - public void write(TextBuffer text, int start, int end) { - int pos = start; - start = Math.max(start, 0); - end = Math.min(end, pos + text.length()); - end = Math.min(end, this.m_text.length); - for (int i = start; i < end; i++) { - this.m_text[i] = text.charAt(i - pos); + public void write( TextBuffer text ) + { + int end = Math.min( text.length(), this.text.length ); + for( int i = 0; i < end; i++ ) + { + this.text[i] = text.charAt( i ); } } - public void write(TextBuffer text, int start) { - this.write(text, start, start + text.length()); + public void fill( char c ) + { + fill( c, 0, text.length ); } - public void fill(char c) { - this.fill(c, 0, this.m_text.length); - } - - public void fill(char c, int start, int end) { - start = Math.max(start, 0); - end = Math.min(end, this.m_text.length); - for (int i = start; i < end; i++) { - this.m_text[i] = c; + public void fill( char c, int start, int end ) + { + start = Math.max( start, 0 ); + end = Math.min( end, text.length ); + for( int i = start; i < end; i++ ) + { + text[i] = c; } } - public void fill(char c, int start) { - this.fill(c, start, this.m_text.length); + public char charAt( int i ) + { + return text[i]; } - public void fill(String text) { - this.fill(text, 0, this.m_text.length); - } - - public void fill(String text, int start, int end) { - int pos = start; - start = Math.max(start, 0); - end = Math.min(end, this.m_text.length); - - int textLength = text.length(); - for (int i = start; i < end; i++) { - this.m_text[i] = text.charAt((i - pos) % textLength); + public void setChar( int i, char c ) + { + if( i >= 0 && i < text.length ) + { + text[i] = c; } } - public void fill(String text, int start) { - this.fill(text, start, this.m_text.length); + public String toString() + { + return new String( text ); } - - public void fill(TextBuffer text) { - this.fill(text, 0, this.m_text.length); - } - - public void fill(TextBuffer text, int start, int end) { - int pos = start; - start = Math.max(start, 0); - end = Math.min(end, this.m_text.length); - - int textLength = text.length(); - for (int i = start; i < end; i++) { - this.m_text[i] = text.charAt((i - pos) % textLength); - } - } - - public void fill(TextBuffer text, int start) { - this.fill(text, start, this.m_text.length); - } - - public void setChar(int i, char c) { - if (i >= 0 && i < this.m_text.length) { - this.m_text[i] = c; - } - } - - @Override - public String toString() { - return new String(this.m_text); - } -} +} \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/mixin/MixinWorld.java b/src/main/java/dan200/computercraft/mixin/MixinWorld.java index 04b45e796..be271fca2 100644 --- a/src/main/java/dan200/computercraft/mixin/MixinWorld.java +++ b/src/main/java/dan200/computercraft/mixin/MixinWorld.java @@ -32,7 +32,7 @@ public class MixinWorld { @Inject (method = "setBlockEntity", at = @At ("HEAD")) public void setBlockEntity(BlockPos pos, @Nullable BlockEntity entity, CallbackInfo info) { - if (!World.isHeightInvalid(pos) && entity != null && !entity.isRemoved() && this.iteratingTickingBlockEntities) { + if (!World.isOutOfBuildLimitVertically(pos) && entity != null && !entity.isRemoved() && this.iteratingTickingBlockEntities) { setWorld(entity, this); } } diff --git a/src/main/java/dan200/computercraft/shared/BundledRedstone.java b/src/main/java/dan200/computercraft/shared/BundledRedstone.java index dec3f0bf7..2e53b20b0 100644 --- a/src/main/java/dan200/computercraft/shared/BundledRedstone.java +++ b/src/main/java/dan200/computercraft/shared/BundledRedstone.java @@ -31,7 +31,7 @@ public final class BundledRedstone { } public static int getDefaultOutput(@Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side) { - return World.method_24794(pos) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput(world, pos, side) : -1; + return World.isValid(pos) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput(world, pos, side) : -1; } public static int getOutput(World world, BlockPos pos, Direction side) { @@ -40,7 +40,7 @@ public final class BundledRedstone { } private static int getUnmaskedOutput(World world, BlockPos pos, Direction side) { - if (!World.method_24794(pos)) { + if (!World.isValid(pos)) { return -1; } diff --git a/src/main/java/dan200/computercraft/shared/Peripherals.java b/src/main/java/dan200/computercraft/shared/Peripherals.java index 3357d264b..9f7b9e179 100644 --- a/src/main/java/dan200/computercraft/shared/Peripherals.java +++ b/src/main/java/dan200/computercraft/shared/Peripherals.java @@ -35,13 +35,11 @@ public final class Peripherals { @Nullable public static IPeripheral getPeripheral(World world, BlockPos pos, Direction side) { - return World.method_24794(pos) && !world.isClient ? getPeripheralAt(world, pos, side) : null; + return World.isValid(pos) && !world.isClient ? getPeripheralAt(world, pos, side) : null; } @Nullable private static IPeripheral getPeripheralAt(World world, BlockPos pos, Direction side) { - BlockEntity block = world.getBlockEntity(pos); - // Try the handlers in order: for (IPeripheralProvider peripheralProvider : providers) { try { diff --git a/src/main/java/dan200/computercraft/shared/PocketUpgrades.java b/src/main/java/dan200/computercraft/shared/PocketUpgrades.java index 072bc47f6..6704ac02b 100644 --- a/src/main/java/dan200/computercraft/shared/PocketUpgrades.java +++ b/src/main/java/dan200/computercraft/shared/PocketUpgrades.java @@ -3,61 +3,59 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.shared; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import dan200.computercraft.api.pocket.IPocketUpgrade; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Util; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.*; -import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.pocket.IPocketUpgrade; -import dan200.computercraft.shared.util.InventoryUtil; - -import net.minecraft.item.ItemStack; - -public final class PocketUpgrades { +public final class PocketUpgrades +{ private static final Map upgrades = new HashMap<>(); - private static final IdentityHashMap upgradeOwners = new IdentityHashMap<>(); + private static final Map upgradeOwners = new Object2ObjectLinkedOpenCustomHashMap<>( Util.identityHashStrategy() ); private PocketUpgrades() {} - public static synchronized void register(@Nonnull IPocketUpgrade upgrade) { - Objects.requireNonNull(upgrade, "upgrade cannot be null"); + public static synchronized void register( @Nonnull IPocketUpgrade upgrade ) + { + Objects.requireNonNull( upgrade, "upgrade cannot be null" ); - String id = upgrade.getUpgradeID() - .toString(); - IPocketUpgrade existing = upgrades.get(id); - if (existing != null) { - throw new IllegalStateException("Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is " + - "already registered by '" + existing.getUnlocalisedAdjective() + " pocket computer'"); + String id = upgrade.getUpgradeID().toString(); + IPocketUpgrade existing = upgrades.get( id ); + if( existing != null ) + { + throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " pocket computer'" ); } - upgrades.put(id, upgrade); + upgrades.put( id, upgrade ); + + // Infer the mod id by the identifier of the upgrade. This is not how the forge api works, so it may break peripheral mods using the api. + // TODO: get the mod id of the mod that is currently being loaded. + ModContainer mc = FabricLoader.getInstance().getModContainer(upgrade.getUpgradeID().getNamespace()).orElseGet(null); + if( mc != null && mc.getMetadata().getId() != null ) upgradeOwners.put( upgrade, mc.getMetadata().getId() ); } - public static IPocketUpgrade get(String id) { + public static IPocketUpgrade get( String id ) + { // Fix a typo in the advanced modem upgrade's name. I'm sorry, I realise this is horrible. - if (id.equals("computercraft:advanved_modem")) { - id = "computercraft:advanced_modem"; - } + if( id.equals( "computercraft:advanved_modem" ) ) id = "computercraft:advanced_modem"; - return upgrades.get(id); + return upgrades.get( id ); } - public static IPocketUpgrade get(@Nonnull ItemStack stack) { - if (stack.isEmpty()) { - return null; - } + public static IPocketUpgrade get( @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return null; - for (IPocketUpgrade upgrade : upgrades.values()) { + for( IPocketUpgrade upgrade : upgrades.values() ) + { ItemStack craftingStack = upgrade.getCraftingItem(); if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && upgrade.isItemSuitable( stack ) ) { @@ -69,19 +67,22 @@ public final class PocketUpgrades { } @Nullable - public static String getOwner(IPocketUpgrade upgrade) { - return upgradeOwners.get(upgrade); + public static String getOwner( IPocketUpgrade upgrade ) + { + return upgradeOwners.get( upgrade ); } - public static Iterable getVanillaUpgrades() { + public static Iterable getVanillaUpgrades() + { List vanilla = new ArrayList<>(); - vanilla.add(ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal); - vanilla.add(ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced); - vanilla.add(ComputerCraftRegistry.PocketUpgrades.speaker); + vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal ); + vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced ); + vanilla.add( ComputerCraftRegistry.PocketUpgrades.speaker ); return vanilla; } - public static Iterable getUpgrades() { - return Collections.unmodifiableCollection(upgrades.values()); + public static Iterable getUpgrades() + { + return Collections.unmodifiableCollection( upgrades.values() ); } -} +} \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java b/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java index 31d3686f7..5153367ca 100644 --- a/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java +++ b/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java @@ -3,9 +3,15 @@ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ - package dan200.computercraft.shared; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.turtle.ITurtleUpgrade; +import dan200.computercraft.shared.computer.core.ComputerFamily; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Arrays; import java.util.HashMap; import java.util.IdentityHashMap; @@ -13,86 +19,72 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Stream; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +public final class TurtleUpgrades +{ + private static class Wrapper + { + final ITurtleUpgrade upgrade; + final String id; + final String modId; + boolean enabled; -import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.turtle.ITurtleUpgrade; -import dan200.computercraft.shared.computer.core.ComputerFamily; + Wrapper( ITurtleUpgrade upgrade ) + { + this.upgrade = upgrade; + this.id = upgrade.getUpgradeID() + .toString(); + // TODO This should be the mod id of the mod the peripheral comes from + this.modId = ComputerCraft.MOD_ID; + this.enabled = true; + } + } -import net.minecraft.item.ItemStack; + private static ITurtleUpgrade[] vanilla; -public final class TurtleUpgrades { private static final Map upgrades = new HashMap<>(); private static final IdentityHashMap wrappers = new IdentityHashMap<>(); - private static ITurtleUpgrade[] vanilla; private static boolean needsRebuild; + private TurtleUpgrades() {} - public static void register(@Nonnull ITurtleUpgrade upgrade) { - Objects.requireNonNull(upgrade, "upgrade cannot be null"); + public static void register( @Nonnull ITurtleUpgrade upgrade ) + { + Objects.requireNonNull( upgrade, "upgrade cannot be null" ); rebuild(); - Wrapper wrapper = new Wrapper(upgrade); + Wrapper wrapper = new Wrapper( upgrade ); String id = wrapper.id; - ITurtleUpgrade existing = upgrades.get(id); - if (existing != null) { - throw new IllegalStateException("Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already " + - "registered by '" + existing.getUnlocalisedAdjective() + " Turtle'"); + ITurtleUpgrade existing = upgrades.get( id ); + if( existing != null ) + { + throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" ); } - upgrades.put(id, upgrade); - wrappers.put(upgrade, wrapper); - } - - /** - * Rebuild the cache of turtle upgrades. This is done before querying the cache or registering new upgrades. - */ - private static void rebuild() { - if (!needsRebuild) { - return; - } - - upgrades.clear(); - for (Wrapper wrapper : wrappers.values()) { - if (!wrapper.enabled) { - continue; - } - - ITurtleUpgrade existing = upgrades.get(wrapper.id); - if (existing != null) { - ComputerCraft.log.error("Error registering '" + wrapper.upgrade.getUnlocalisedAdjective() + " Turtle'." + " Upgrade ID '" + wrapper.id + - "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'"); - continue; - } - - upgrades.put(wrapper.id, wrapper.upgrade); - } - - needsRebuild = false; + upgrades.put( id, upgrade ); + wrappers.put( upgrade, wrapper ); } @Nullable - public static ITurtleUpgrade get(String id) { + public static ITurtleUpgrade get( String id ) + { rebuild(); - return upgrades.get(id); + return upgrades.get( id ); } @Nullable - public static String getOwner(@Nonnull ITurtleUpgrade upgrade) { - Wrapper wrapper = wrappers.get(upgrade); + public static String getOwner( @Nonnull ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); return wrapper != null ? wrapper.modId : null; } - public static ITurtleUpgrade get(@Nonnull ItemStack stack) { - if (stack.isEmpty()) { - return null; - } + public static ITurtleUpgrade get( @Nonnull ItemStack stack ) + { + if( stack.isEmpty() ) return null; - for (Wrapper wrapper : wrappers.values()) { - if (!wrapper.enabled) { - continue; - } + for( Wrapper wrapper : wrappers.values() ) + { + if( !wrapper.enabled ) continue; ItemStack craftingStack = wrapper.upgrade.getCraftingItem(); if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade.isItemSuitable( stack ) ) @@ -104,8 +96,10 @@ public final class TurtleUpgrades { return null; } - public static Stream getVanillaUpgrades() { - if (vanilla == null) { + public static Stream getVanillaUpgrades() + { + if( vanilla == null ) + { vanilla = new ITurtleUpgrade[] { // ComputerCraft upgrades ComputerCraftRegistry.TurtleUpgrades.wirelessModemNormal, @@ -119,64 +113,69 @@ public final class TurtleUpgrades { ComputerCraftRegistry.TurtleUpgrades.diamondShovel, ComputerCraftRegistry.TurtleUpgrades.diamondHoe, ComputerCraftRegistry.TurtleUpgrades.craftingTable, - - ComputerCraftRegistry.TurtleUpgrades.netheritePickaxe, }; } - return Arrays.stream(vanilla) - .filter(x -> x != null && wrappers.get(x).enabled); + return Arrays.stream( vanilla ).filter( x -> x != null && wrappers.get( x ).enabled ); } - public static Stream getUpgrades() { - return wrappers.values() - .stream() - .filter(x -> x.enabled) - .map(x -> x.upgrade); + public static Stream getUpgrades() + { + return wrappers.values().stream().filter( x -> x.enabled ).map( x -> x.upgrade ); } - public static boolean suitableForFamily(ComputerFamily family, ITurtleUpgrade upgrade) { + public static boolean suitableForFamily( ComputerFamily family, ITurtleUpgrade upgrade ) + { return true; } - public static void enable(ITurtleUpgrade upgrade) { - Wrapper wrapper = wrappers.get(upgrade); - if (wrapper.enabled) { - return; + /** + * Rebuild the cache of turtle upgrades. This is done before querying the cache or registering new upgrades. + */ + private static void rebuild() + { + if( !needsRebuild ) return; + + upgrades.clear(); + for( Wrapper wrapper : wrappers.values() ) + { + if( !wrapper.enabled ) continue; + + ITurtleUpgrade existing = upgrades.get( wrapper.id ); + if( existing != null ) + { + ComputerCraft.log.error( "Error registering '" + wrapper.upgrade.getUnlocalisedAdjective() + " Turtle'." + + " Upgrade ID '" + wrapper.id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" ); + continue; + } + + upgrades.put( wrapper.id, wrapper.upgrade ); } + needsRebuild = false; + } + + public static void enable( ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); + if( wrapper.enabled ) return; + wrapper.enabled = true; needsRebuild = true; } - public static void disable(ITurtleUpgrade upgrade) { - Wrapper wrapper = wrappers.get(upgrade); - if (!wrapper.enabled) { - return; - } + public static void disable( ITurtleUpgrade upgrade ) + { + Wrapper wrapper = wrappers.get( upgrade ); + if( !wrapper.enabled ) return; wrapper.enabled = false; - upgrades.remove(wrapper.id); + upgrades.remove( wrapper.id ); } - public static void remove(ITurtleUpgrade upgrade) { - wrappers.remove(upgrade); + public static void remove( ITurtleUpgrade upgrade ) + { + wrappers.remove( upgrade ); needsRebuild = true; } - - private static class Wrapper { - final ITurtleUpgrade upgrade; - final String id; - final String modId; - boolean enabled; - - Wrapper(ITurtleUpgrade upgrade) { - this.upgrade = upgrade; - this.id = upgrade.getUpgradeID() - .toString(); - // TODO This should be the mod id of the mod the peripheral comes from - this.modId = ComputerCraft.MOD_ID; - this.enabled = true; - } - } -} +} \ No newline at end of file diff --git a/src/main/java/dan200/computercraft/shared/command/CommandUtils.java b/src/main/java/dan200/computercraft/shared/command/CommandUtils.java index 26754058b..375dd4402 100644 --- a/src/main/java/dan200/computercraft/shared/command/CommandUtils.java +++ b/src/main/java/dan200/computercraft/shared/command/CommandUtils.java @@ -17,7 +17,7 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import dan200.computercraft.api.turtle.FakePlayer; import net.minecraft.entity.Entity; -import net.minecraft.server.command.CommandSource; +import net.minecraft.command.CommandSource; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java index 2a66975f9..36a6f1009 100644 --- a/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java +++ b/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java @@ -55,8 +55,9 @@ public final class RepeatArgumentType implements ArgumentType> { this.some = some; } - public static RepeatArgumentType some(ArgumentType appender, SimpleCommandExceptionType missing) { - return new RepeatArgumentType<>(appender, List::add, true, missing); + public static RepeatArgumentType some( ArgumentType appender, SimpleCommandExceptionType missing ) + { + return new RepeatArgumentType<>( appender, List::add, false, missing ); } public static RepeatArgumentType> someFlat(ArgumentType> appender, SimpleCommandExceptionType missing) { diff --git a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java index 3006954a0..87f84404e 100644 --- a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java +++ b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java @@ -8,7 +8,6 @@ package dan200.computercraft.shared.common; import javax.annotation.Nonnull; -import dan200.computercraft.shared.util.Colour; import dan200.computercraft.shared.util.ColourTracker; import dan200.computercraft.shared.util.ColourUtils; @@ -66,17 +65,10 @@ public final class ColourableRecipe extends SpecialCraftingRecipe { if (stack.isEmpty()) { continue; } - - if (stack.getItem() instanceof IColouredItem) { - colourable = stack; - } else { - DyeColor dye = ColourUtils.getStackColour(stack); - if (dye == null) { - continue; - } - - Colour colour = Colour.fromInt(15 - dye.getId()); - tracker.addColour(colour.getR(), colour.getG(), colour.getB()); + else + { + DyeColor dye = ColourUtils.getStackColour( stack ); + if( dye != null ) tracker.addColour( dye ); } } 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 fe87d15dd..fa7cf62cd 100644 --- a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java +++ b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java @@ -207,7 +207,7 @@ public class CommandAPI implements ILuaAPI { World world = this.computer.getWorld(); BlockPos min = new BlockPos(Math.min(minX, maxX), Math.min(minY, maxY), Math.min(minZ, maxZ)); BlockPos max = new BlockPos(Math.max(minX, maxX), Math.max(minY, maxY), Math.max(minZ, maxZ)); - if (!World.method_24794(min) || !World.method_24794(max)) { + if (!World.isValid(min) || !World.isValid(max)) { throw new LuaException("Co-ordinates out of range"); } @@ -284,7 +284,7 @@ public class CommandAPI implements ILuaAPI { // Get the details of the block World world = this.computer.getWorld(); BlockPos position = new BlockPos(x, y, z); - if (World.method_24794(position)) { + if (World.isValid(position)) { return getBlockInfo(world, position); } else { throw new LuaException("Co-ordinates out of range"); diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index 770e78af8..cc4b1a7fa 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -316,7 +316,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput @Nonnull @Override public String getHostString() { - return String.format("ComputerCraft %s (Minecraft %s)", ComputerCraftAPI.getInstalledVersion(), "1.16.2"); + return String.format("ComputerCraft %s (Minecraft %s)", ComputerCraftAPI.getInstalledVersion(), "1.16.4"); } @Nonnull diff --git a/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java b/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java index 615985082..24006f1ee 100644 --- a/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java +++ b/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java @@ -53,7 +53,9 @@ public class DiskRecipe extends SpecialCraftingRecipe { return false; } redstoneFound = true; - } else if (ColourUtils.getStackColour(stack) != null) { + } + else if( ColourUtils.getStackColour( stack ) == null ) + { return false; } } @@ -74,14 +76,10 @@ public class DiskRecipe extends SpecialCraftingRecipe { continue; } - if (!this.paper.test(stack) && !this.redstone.test(stack)) { - DyeColor dye = ColourUtils.getStackColour(stack); - if (dye == null) { - continue; - } - - Colour colour = Colour.VALUES[dye.getId()]; - tracker.addColour(colour.getR(), colour.getG(), colour.getB()); + if( !paper.test( stack ) && !redstone.test( stack ) ) + { + DyeColor dye = ColourUtils.getStackColour( stack ); + if( dye != null ) tracker.addColour( dye ); } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java index 76186fc24..48ea969fb 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java @@ -85,8 +85,8 @@ public class InventoryMethods implements GenericSource * @link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail includes. More information can be fetched * with {@link #getItemDetail}. * - * The table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs` rather than - * `ipairs`. + * The returned table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs` + * rather than `ipairs`. * * @param inventory The current inventory. * @return All items in this inventory. 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 85d602672..00a4f3638 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -191,6 +191,10 @@ public class TurtleAPI implements ILuaAPI { /** * Place a block or item into the world in front of the turtle. * + * "Placing" an item allows it to interact with blocks and entities in front of the turtle. For instance, buckets + * can pick up and place down fluids, and wheat can be used to breed cows. However, you cannot use {@link #place} to + * perform arbitrary block interactions, such as clicking buttons or flipping levers. + * * @param args Arguments to place. * @return The turtle command result. * @cc.tparam [opt] string text When placing a sign, set its contents to this text. @@ -210,6 +214,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. + * @see #place For more information about placing items. */ @LuaFunction public final MethodResult placeUp(IArguments args) { @@ -224,6 +229,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. + * @see #place For more information about placing items. */ @LuaFunction public final MethodResult placeDown(IArguments args) { @@ -380,16 +386,34 @@ public class TurtleAPI implements ILuaAPI { return this.trackCommand(new TurtleDetectCommand(InteractDirection.DOWN)); } + /** + * Check if the block in front of the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ @LuaFunction public final MethodResult compare() { return this.trackCommand(new TurtleCompareCommand(InteractDirection.FORWARD)); } + /** + * Check if the block above the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ @LuaFunction public final MethodResult compareUp() { return this.trackCommand(new TurtleCompareCommand(InteractDirection.UP)); } + /** + * Check if the block below the turtle is equal to the item in the currently selected slot. + * + * @return If the block and item are equal. + * @cc.treturn boolean If the block and item are equal. + */ @LuaFunction public final MethodResult compareDown() { return this.trackCommand(new TurtleCompareCommand(InteractDirection.DOWN)); @@ -478,11 +502,56 @@ public class TurtleAPI implements ILuaAPI { return this.trackCommand(new TurtleSuckCommand(InteractDirection.DOWN, checkCount(count))); } + /** + * Get the maximum amount of fuel this turtle currently holds. + * + * @return The fuel level, or "unlimited". + * @cc.treturn[1] number The current amount of fuel a turtle this turtle has. + * @cc.treturn[2] "unlimited" If turtles do not consume fuel when moving. + * @see #getFuelLimit() + * @see #refuel(Optional) + */ @LuaFunction public final Object getFuelLevel() { return this.turtle.isFuelNeeded() ? this.turtle.getFuelLevel() : "unlimited"; } + /** + * Refuel this turtle. + * + * While most actions a turtle can perform (such as digging or placing blocks), moving consumes fuel from the + * turtle's internal buffer. If a turtle has no fuel, it will not move. + * + * {@link #refuel} refuels the turtle, consuming fuel items (such as coal or lava buckets) from the currently + * selected slot and converting them into energy. This finishes once the turtle is fully refuelled or all items have + * been consumed. + * + * @param countA The maximum number of items to consume. One can pass `0` to check if an item is combustable or not. + * @return If this turtle could be refuelled. + * @throws LuaException If the refuel count is out of range. + * @cc.treturn[1] true If the turtle was refuelled. + * @cc.treturn[2] false If the turtle was not refuelled. + * @cc.treturn[2] string The reason the turtle was not refuelled ( + * @cc.usage Refuel a turtle from the currently selected slot. + *
{@code
+     * local level = turtle.getFuelLevel()
+     * if new_level == "unlimited" then error("Turtle does not need fuel", 0) end
+     *
+     * local ok, err = turtle.refuel()
+     * if ok then
+     *   local new_level = turtle.getFuelLevel()
+     *   print(("Refuelled %d, current level is %d"):format(new_level - level, new_level))
+     * else
+     *   printError(err)
+     * end}
+ * @cc.usage Check if the current item is a valid fuel source. + *
{@code
+     * local is_fuel, reason = turtle.refuel(0)
+     * if not is_fuel then printError(reason) end
+     * }
+ * @see #getFuelLevel() + * @see #getFuelLimit() + */ @LuaFunction public final MethodResult refuel(Optional countA) throws LuaException { int count = countA.orElse(Integer.MAX_VALUE); @@ -492,11 +561,29 @@ public class TurtleAPI implements ILuaAPI { return this.trackCommand(new TurtleRefuelCommand(count)); } + /** + * Compare the item in the currently selected slot to the item in another slot. + * + * @param slot The slot to compare to. + * @return If the items are the same. + * @throws LuaException If the slot is out of range. + * @cc.treturn boolean If the two items are equal. + */ @LuaFunction public final MethodResult compareTo(int slot) throws LuaException { return this.trackCommand(new TurtleCompareToCommand(checkSlot(slot))); } + /** + * Move an item from the selected slot to another one. + * + * @param slotArg The slot to move this item to. + * @param countArg The maximum number of items to move. + * @return If the item was moved or not. + * @throws LuaException If the slot is out of range. + * @throws LuaException If the number of items is out of range. + * @cc.treturn boolean If some items were successfully moved. + */ @LuaFunction public final MethodResult transferTo(int slotArg, Optional countArg) throws LuaException { int slot = checkSlot(slotArg); @@ -515,16 +602,53 @@ public class TurtleAPI implements ILuaAPI { return this.turtle.getSelectedSlot() + 1; } + /** + * Get the maximum amount of fuel this turtle can hold. + * + * By default, normal turtles have a limit of 20,000 and advanced turtles of 100,000. + * + * @return The limit, or "unlimited". + * @cc.treturn[1] number The maximum amount of fuel a turtle can hold. + * @cc.treturn[2] "unlimited" If turtles do not consume fuel when moving. + * @see #getFuelLevel() + * @see #refuel(Optional) + */ @LuaFunction public final Object getFuelLimit() { return this.turtle.isFuelNeeded() ? this.turtle.getFuelLimit() : "unlimited"; } + /** + * Equip (or unequip) an item on the left side of this turtle. + * + * This finds the item in the currently selected slot and attempts to equip it to the left side of the turtle. The + * previous upgrade is removed and placed into the turtle's inventory. If there is no item in the slot, the previous + * upgrade is removed, but no new one is equipped. + * + * @return Whether an item was equiped or not. + * @cc.treturn[1] true If the item was equipped. + * @cc.treturn[2] false If we could not equip the item. + * @cc.treturn[2] string The reason equipping this item failed. + * @see #equipRight() + */ @LuaFunction public final MethodResult equipLeft() { return this.trackCommand(new TurtleEquipCommand(TurtleSide.LEFT)); } + /** + * Equip (or unequip) an item on the right side of this turtle. + * + * This finds the item in the currently selected slot and attempts to equip it to the right side of the turtle. The + * previous upgrade is removed and placed into the turtle's inventory. If there is no item in the slot, the previous + * upgrade is removed, but no new one is equipped. + * + * @return Whether an item was equiped or not. + * @cc.treturn[1] true If the item was equipped. + * @cc.treturn[2] false If we could not equip the item. + * @cc.treturn[2] string The reason equipping this item failed. + * @see #equipRight() + */ @LuaFunction public final MethodResult equipRight() { return this.trackCommand(new TurtleEquipCommand(TurtleSide.RIGHT)); diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java index 780e117b1..1e0bfde8c 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java @@ -119,10 +119,10 @@ public class TurtleMoveCommand implements ITurtleCommand { } private static TurtleCommandResult canEnter(TurtlePlayer turtlePlayer, World world, BlockPos position) { - if (World.isHeightInvalid(position)) { + if (World.isOutOfBuildLimitVertically(position)) { return TurtleCommandResult.failure(position.getY() < 0 ? "Too low to move" : "Too high to move"); } - if (!World.method_24794(position)) { + if (!World.isValid(position)) { return TurtleCommandResult.failure("Cannot leave the world"); } diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java index e02ec415e..a21479d1c 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java @@ -364,7 +364,7 @@ public class TurtlePlaceCommand implements ITurtleCommand { private static boolean canDeployOnBlock(@Nonnull ItemPlacementContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position, Direction side, boolean allowReplaceable, String[] outErrorMessage) { World world = turtle.getWorld(); - if (!World.method_24794(position) || world.isAir(position) || (context.getStack() + if (!World.isValid(position) || world.isAir(position) || (context.getStack() .getItem() instanceof BlockItem && WorldUtil.isLiquidBlock(world, position))) { return false; diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java index 35150d795..724898819 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java @@ -77,7 +77,7 @@ public final class TurtlePlayer extends FakePlayer { private void setState(ITurtleAccess turtle) { if (this.currentScreenHandler != playerScreenHandler) { ComputerCraft.log.warn("Turtle has open container ({})", this.currentScreenHandler); - closeCurrentScreen(); + closeHandledScreen(); } BlockPos position = turtle.getPosition(); @@ -91,13 +91,8 @@ public final class TurtlePlayer extends FakePlayer { } public static TurtlePlayer get(ITurtleAccess access) { - ServerWorld world = (ServerWorld) access.getWorld(); if( !(access instanceof TurtleBrain) ) return create( access ); - /*if (!(access instanceof TurtleBrain)) { - return new TurtlePlayer(world, access.getOwningPlayer()); - }*/ - TurtleBrain brain = (TurtleBrain) access; TurtlePlayer player = brain.m_cachedPlayer; if (player == null || player.getGameProfile() != getProfile(access.getOwningPlayer()) || player.getEntityWorld() != access.getWorld()) { diff --git a/src/main/java/dan200/computercraft/shared/util/ColourTracker.java b/src/main/java/dan200/computercraft/shared/util/ColourTracker.java index 55b4571f7..25daa65e2 100644 --- a/src/main/java/dan200/computercraft/shared/util/ColourTracker.java +++ b/src/main/java/dan200/computercraft/shared/util/ColourTracker.java @@ -6,6 +6,8 @@ package dan200.computercraft.shared.util; +import net.minecraft.util.DyeColor; + /** * A reimplementation of the colour system in {@link ArmorDyeRecipe}, but bundled together as an object. */ @@ -28,8 +30,15 @@ public class ColourTracker { this.count++; } - public boolean hasColour() { - return this.count > 0; + public void addColour( DyeColor dye ) + { + Colour colour = Colour.VALUES[15 - dye.getId()]; + addColour( colour.getR(), colour.getG(), colour.getB() ); + } + + public boolean hasColour() + { + return count > 0; } public int getColour() { diff --git a/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java b/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java index cd5d9d8b6..3b0b93d99 100644 --- a/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java +++ b/src/main/java/dan200/computercraft/shared/util/FakeNetHandler.java @@ -14,7 +14,6 @@ import dan200.computercraft.api.turtle.FakePlayer; import io.netty.channel.ChannelHandlerContext; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; - import net.minecraft.network.ClientConnection; import net.minecraft.network.NetworkSide; import net.minecraft.network.NetworkState; @@ -25,15 +24,12 @@ import net.minecraft.network.packet.c2s.play.BoatPaddleStateC2SPacket; import net.minecraft.network.packet.c2s.play.BookUpdateC2SPacket; import net.minecraft.network.packet.c2s.play.ButtonClickC2SPacket; import net.minecraft.network.packet.c2s.play.ChatMessageC2SPacket; -import net.minecraft.network.packet.c2s.play.ClickWindowC2SPacket; import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket; import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket; import net.minecraft.network.packet.c2s.play.ClientStatusC2SPacket; -import net.minecraft.network.packet.c2s.play.ConfirmGuiActionC2SPacket; import net.minecraft.network.packet.c2s.play.CraftRequestC2SPacket; import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; -import net.minecraft.network.packet.c2s.play.GuiCloseC2SPacket; import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket; import net.minecraft.network.packet.c2s.play.KeepAliveC2SPacket; import net.minecraft.network.packet.c2s.play.PickFromInventoryC2SPacket; @@ -48,7 +44,6 @@ import net.minecraft.network.packet.c2s.play.QueryEntityNbtC2SPacket; import net.minecraft.network.packet.c2s.play.RenameItemC2SPacket; import net.minecraft.network.packet.c2s.play.RequestCommandCompletionsC2SPacket; import net.minecraft.network.packet.c2s.play.ResourcePackStatusC2SPacket; -import net.minecraft.network.packet.c2s.play.SelectVillagerTradeC2SPacket; import net.minecraft.network.packet.c2s.play.SpectatorTeleportC2SPacket; import net.minecraft.network.packet.c2s.play.TeleportConfirmC2SPacket; import net.minecraft.network.packet.c2s.play.UpdateBeaconC2SPacket; @@ -127,10 +122,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler { public void onJigsawUpdate(@Nonnull UpdateJigsawC2SPacket packet) { } - @Override - public void onVillagerTradeSelect(@Nonnull SelectVillagerTradeC2SPacket packet) { - } - @Override public void onBookUpdate(@Nonnull BookUpdateC2SPacket packet) { } @@ -207,14 +198,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler { public void onClientStatus(@Nonnull ClientStatusC2SPacket packet) { } - @Override - public void onGuiClose(@Nonnull GuiCloseC2SPacket packet) { - } - - @Override - public void onClickWindow(@Nonnull ClickWindowC2SPacket packet) { - } - @Override public void onCraftRequest(@Nonnull CraftRequestC2SPacket packet) { } @@ -227,10 +210,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler { public void onCreativeInventoryAction(@Nonnull CreativeInventoryActionC2SPacket packet) { } - @Override - public void onConfirmTransaction(@Nonnull ConfirmGuiActionC2SPacket packet) { - } - @Override public void onSignUpdate(@Nonnull UpdateSignC2SPacket packet) { } @@ -309,10 +288,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler { this.closeReason = message; } - @Override - public void setupEncryption(@Nonnull SecretKey key) { - } - @Nonnull @Override public PacketListener getPacketListener() { diff --git a/src/main/java/dan200/computercraft/shared/util/WorldUtil.java b/src/main/java/dan200/computercraft/shared/util/WorldUtil.java index 284af67d3..df99b66d1 100644 --- a/src/main/java/dan200/computercraft/shared/util/WorldUtil.java +++ b/src/main/java/dan200/computercraft/shared/util/WorldUtil.java @@ -40,7 +40,7 @@ public final class WorldUtil { .makeMap(); public static boolean isLiquidBlock(World world, BlockPos pos) { - if (!World.method_24794(pos)) { + if (!World.isValid(pos)) { return false; } return world.getBlockState(pos) diff --git a/src/main/resources/assets/computercraft/lang/pt_br.json b/src/main/resources/assets/computercraft/lang/pt_br.json index 0f7df3362..f5ec8017a 100644 --- a/src/main/resources/assets/computercraft/lang/pt_br.json +++ b/src/main/resources/assets/computercraft/lang/pt_br.json @@ -38,5 +38,21 @@ "upgrade.computercraft.speaker.adjective": "(Alto-Falante)", "chat.computercraft.wired_modem.peripheral_connected": "Periférico \"%s\" conectado à rede", "chat.computercraft.wired_modem.peripheral_disconnected": "Periférico \"%s\" desconectado da rede", - "gui.computercraft.tooltip.copy": "Copiar para a área de transferência" + "gui.computercraft.tooltip.copy": "Copiar para a área de transferência", + "commands.computercraft.tp.synopsis": "Teleprota para um computador específico.", + "commands.computercraft.turn_on.done": "Ligou %s/%s computadores", + "commands.computercraft.turn_on.desc": "Liga os computadores em escuta. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").", + "commands.computercraft.turn_on.synopsis": "Liga computadores remotamente.", + "commands.computercraft.shutdown.done": "Desliga %s/%s computadores", + "commands.computercraft.shutdown.desc": "Desliga os computadores em escuta ou todos caso não tenha sido especificado. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").", + "commands.computercraft.shutdown.synopsis": "Desliga computadores remotamente.", + "commands.computercraft.dump.action": "Ver mais informação sobre este computador", + "commands.computercraft.dump.desc": "Mostra o status de todos os computadores ou uma informação específica sobre um computador. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").", + "commands.computercraft.dump.synopsis": "Mostra status de computadores.", + "commands.computercraft.help.no_command": "Comando '%s' não existe", + "commands.computercraft.help.no_children": "%s não tem sub-comandos", + "commands.computercraft.help.desc": "Mostra essa mensagem de ajuda", + "commands.computercraft.help.synopsis": "Providencia ajuda para um comando específico", + "commands.computercraft.desc": "O comando /computercraft providencia várias ferramentas de depuração e administração para controle e interação com computadores.", + "commands.computercraft.synopsis": "Vários comandos para controlar computadores." } diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json b/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json index 7e55bfbab..6ca0e4761 100644 --- a/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json +++ b/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json @@ -1,11 +1,34 @@ { - "type": "minecraft:block", - "pools": [ + "type": "minecraft:block", + "pools": [ + { + "name": "main", + "rolls": 1, + "entries": [ { - "rolls": 1, - "entries": [ - { "type": "minecraft:dynamic", "name": "computercraft:computer" } - ] + "type": "minecraft:dynamic", + "name": "computercraft:computer" } - ] -} + ], + "conditions": [ + { + "condition": "minecraft:alternative", + "terms": [ + { + "condition": "computercraft:block_named" + }, + { + "condition": "computercraft:has_id" + }, + { + "condition": "minecraft:inverted", + "term": { + "condition": "computercraft:player_creative" + } + } + ] + } + ] + } + ] +} \ No newline at end of file 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 3be8906a5..8cbc63a78 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/gps.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/gps.lua @@ -83,9 +83,9 @@ end --- Tries to retrieve the computer or turtles own location. -- --- @tparam[opt] number timeout The maximum time taken to establish our --- position. Defaults to 2 seconds if not specified. --- @tparam[opt] boolean debug Print debugging messages +-- @tparam[opt=2] number timeout The maximum time in seconds taken to establish our +-- position. +-- @tparam[opt=false] boolean debug Print debugging messages -- @treturn[1] number This computer's `x` position. -- @treturn[1] number This computer's `y` position. -- @treturn[1] number This computer's `z` position. 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 5ecbdf652..4898df308 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/io.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/io.lua @@ -137,6 +137,15 @@ handleMetatable = { return handle.seek(whence, offset) end, + --[[- Sets the buffering mode for an output file. + + This has no effect under ComputerCraft, and exists with compatility + with base Lua. + @tparam string mode The buffering mode. + @tparam[opt] number size The size of the buffer. + @see file:setvbuf Lua's documentation for `setvbuf`. + @deprecated This has no effect in CC. + ]] setvbuf = function(self, mode, size) end, --- Write one or more values to the file 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 b920341c3..f6f1efaac 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/paintutils.lua @@ -132,7 +132,17 @@ function drawLine(startX, startY, endX, endY, colour) return end - local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY) + local minX = math.min(startX, endX) + local maxX, minY, maxY + if minX == startX then + minY = startY + maxX = endX + maxY = endY + else + minY = endY + maxX = startX + maxY = startY + end -- TODO: clip to screen rectangle? diff --git a/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua b/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua index c284d0691..0f3478046 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/peripheral.lua @@ -161,7 +161,9 @@ end -- @tparam string name The name of the peripheral to wrap. -- @treturn table|nil The table containing the peripheral's methods, or `nil` if -- there is no peripheral present with the given name. --- @usage peripheral.wrap("top").open(1) +-- @usage Open the modem on the top of this computer. +-- +-- peripheral.wrap("top").open(1) function wrap(name) expect(1, name, "string") @@ -183,16 +185,25 @@ function wrap(name) return result end ---- Find all peripherals of a specific type, and return the --- @{peripheral.wrap|wrapped} peripherals. --- --- @tparam string ty The type of peripheral to look for. --- @tparam[opt] function(name:string, wrapped:table):boolean filter A --- filter function, which takes the peripheral's name and wrapped table --- and returns if it should be included in the result. --- @treturn table... 0 or more wrapped peripherals matching the given filters. --- @usage { peripheral.find("monitor") } --- @usage peripheral.find("modem", rednet.open) +--[[- Find all peripherals of a specific type, and return the +@{peripheral.wrap|wrapped} peripherals. + +@tparam string ty The type of peripheral to look for. +@tparam[opt] function(name:string, wrapped:table):boolean filter A +filter function, which takes the peripheral's name and wrapped table +and returns if it should be included in the result. +@treturn table... 0 or more wrapped peripherals matching the given filters. +@usage Find all monitors and store them in a table, writing "Hello" on each one. + + local monitors = { peripheral.find("monitor") } + for _, monitor in pairs(monitors) do + monitor.write("Hello") + end + +@usage This abuses the `filter` argument to call @{rednet.open} on every modem. + + peripheral.find("modem", rednet.open) +]] function find(ty, filter) expect(1, ty, "string") expect(2, filter, "function", "nil") 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 c80f8c3c9..d6b4c5705 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -381,6 +381,7 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle) local sArrayResult = "[" local nObjectSize = 0 local nArraySize = 0 + local largestArrayIndex = 0 for k, v in pairs(t) do if type(k) == "string" then local sEntry @@ -395,10 +396,17 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle) sObjectResult = sObjectResult .. "," .. sEntry end nObjectSize = nObjectSize + 1 + elseif type(k) == "number" and k > largestArrayIndex then --the largest index is kept to avoid losing half the array if there is any single nil in that array + largestArrayIndex = k end end - for _, v in ipairs(t) do - local sEntry = serializeJSONImpl(v, tTracking, bNBTStyle) + for k = 1, largestArrayIndex, 1 do --the array is read up to the very last valid array index, ipairs() would stop at the first nil value and we would lose any data after. + local sEntry + if t[k] == nil then --if the array is nil at index k the value is "null" as to keep the unused indexes in between used ones. + sEntry = "null" + else -- if the array index does not point to a nil we serialise it's content. + sEntry = serializeJSONImpl(t[k], tTracking, bNBTStyle) + end if nArraySize == 0 then sArrayResult = sArrayResult .. sEntry else diff --git a/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua b/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua index c9d57bf12..0a66add16 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/turtle/turtle.lua @@ -10,6 +10,7 @@ end -- -- Generally you should not need to use this table - it only exists for -- backwards compatibility reasons. +-- @deprecated native = turtle.native or turtle local function addCraftMethod(object) diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt index 0c19ff76c..bcad65f44 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt @@ -1,3 +1,28 @@ +New features in CC: Restitched 1.95.3 + +Several bug fixes: +* Correctly serialise sparse arrays into JSON (livegamer999) +* Fix rs.getBundledInput returning the output instead (SkyTheCodeMaster) +* Programs run via edit are now a little better behaved (Wojbie) +* Add User-Agent to a websocket's headers. + +# New features in CC: Restitched 1.95.2 + +* Add `isReadOnly` to `fs.attributes` (Lupus590) +* Many more programs now support numpad enter (Wojbie) + +Several bug fixes: +* Fix some commands failing to parse on dedicated servers. +* Hopefully improve edit's behaviour with AltGr on some European keyboards. +* Prevent files being usable after their mount was removed. +* Fix the `id` program crashing on non-disk items (Wojbie). + +# New features in CC: Restitched 1.95.1 + +Several bug fixes: +* Command computers now drop items again. +* Restore crafting of disks with dyes. + # New features in CC: Restitched 1.95.0 * Optimise the paint program's initial render. diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt index 802edff6f..982911f36 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt @@ -1,23 +1,9 @@ -New features in CC: Restitched 1.95.0 +New features in CC: Restitched 1.95.3 -* Optimise the paint program's initial render. -* Several documentation improvments (Gibbo3771, MCJack123). -* `fs.combine` now accepts multiple arguments. -* Add a setting (`bios.strict_globals`) to error when accidentally declaring a global. (Lupus590). -* Add an improved help viewer which allows scrolling up and down (MCJack123). -* Add `cc.strings` module, with utilities for wrapping text (Lupus590). -* The `clear` program now allows resetting the palette too (Luca0208). - -And several bug fixes: -* Fix memory leak in generic peripherals. -* Fix crash when a turtle is broken while being ticked. -* `textutils.*tabulate` now accepts strings _or_ numbers. -* We now deny _all_ local IPs, using the magic `$private` host. Previously the IPv6 loopback interface was not blocked. -* Fix crash when rendering monitors if the block has not yet been synced. You will need to regenerate the config file to apply this change. -* `read` now supports numpad enter (TheWireLord) -* Correctly handle HTTP redirects to URLs containing escape characters. -* Fix integer overflow in `os.epoch`. -* Allow using pickaxes (and other items) for turtle upgrades which have mod-specific NBT. -* Fix duplicate turtle/pocket upgrade recipes appearing in JEI. +Several bug fixes: +* Correctly serialise sparse arrays into JSON (livegamer999) +* Fix rs.getBundledInput returning the output instead (SkyTheCodeMaster) +* Programs run via edit are now a little better behaved (Wojbie) +* Add User-Agent to a websocket's headers. Type "help changelog" to see the full version history. 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 9a3aa7b0a..f202c204e 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 @@ -1,23 +1,28 @@ ---- Provides a "pretty printer", for rendering data structures in an --- aesthetically pleasing manner. --- --- In order to display something using @{cc.pretty}, you build up a series of --- @{Doc|documents}. These behave a little bit like strings; you can concatenate --- them together and then print them to the screen. --- --- However, documents also allow you to control how they should be printed. There --- are several functions (such as @{nest} and @{group}) which allow you to control --- the "layout" of the document. When you come to display the document, the 'best' --- (most compact) layout is used. --- --- @module cc.pretty --- @usage Print a table to the terminal --- local pretty = require "cc.pretty" --- pretty.print(pretty.pretty({ 1, 2, 3 })) --- --- @usage Build a custom document and display it --- local pretty = require "cc.pretty" --- pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) +--[[- Provides a "pretty printer", for rendering data structures in an +aesthetically pleasing manner. + +In order to display something using @{cc.pretty}, you build up a series of +@{Doc|documents}. These behave a little bit like strings; you can concatenate +them together and then print them to the screen. + +However, documents also allow you to control how they should be printed. There +are several functions (such as @{nest} and @{group}) which allow you to control +the "layout" of the document. When you come to display the document, the 'best' +(most compact) layout is used. + +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 +@usage Print a table to the terminal + local pretty = require "cc.pretty" + pretty.print(pretty.pretty({ 1, 2, 3 })) + +@usage Build a custom document and display it + local pretty = require "cc.pretty" + pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) +]] local expect = require "cc.expect" local expect, field = expect.expect, expect.field 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 49f5cd0d4..89d6e475c 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 @@ -5,17 +5,24 @@ local expect = require "cc.expect".expect ---- Wraps a block of text, so that each line fits within the given width. --- --- This may be useful if you want to wrap text before displaying it to a --- @{monitor} or @{printer} without using @{_G.print|print}. --- --- @tparam string text The string to wrap. --- @tparam[opt] number width The width to constrain to, defaults to the width of --- the terminal. --- --- @treturn { string... } The wrapped input string. --- @usage require "cc.strings".wrap("This is a long piece of text", 10) +--[[- Wraps a block of text, so that each line fits within the given width. + +This may be useful if you want to wrap text before displaying it to a +@{monitor} or @{printer} without using @{_G.print|print}. + +@tparam string text The string to wrap. +@tparam[opt] number width The width to constrain to, defaults to the width of +the terminal. +@treturn { string... } The wrapped input string as a list of lines. +@usage Wrap a string and write it to the terminal. + + term.clear() + local lines = require "cc.strings".wrap("This is a long piece of text", 10) + for i = 1, #lines do + term.setCursorPos(1, i) + term.write(lines[i]) + end +]] local function wrap(text, width) expect(1, text, "string") expect(2, width, "number", "nil") diff --git a/src/main/resources/data/computercraft/lua/rom/motd.txt b/src/main/resources/data/computercraft/lua/rom/motd.txt index d94437ab9..0c5db751b 100644 --- a/src/main/resources/data/computercraft/lua/rom/motd.txt +++ b/src/main/resources/data/computercraft/lua/rom/motd.txt @@ -1,4 +1,4 @@ -Please report bugs at https://github.com/Merith-TK/cc-restiched. Thanks! +Please report bugs at https://github.com/Merith-TK/cc-restitched/issues. Thanks! View the documentation at https://tweaked.cc Show off your programs or ask for help at our forum: https://forums.computercraft.cc You can disable these messages by running "set motd.enable false". diff --git a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua index 8f0af9356..c913aa234 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/edit.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/edit.lua @@ -47,6 +47,30 @@ else stringColour = colours.white end +local runHandler = [[multishell.setTitle(multishell.getCurrent(), %q) +local current = term.current() +local ok, err = load(%q, %q, nil, _ENV) +if ok then ok, err = pcall(ok, ...) end +term.redirect(current) +term.setTextColor(term.isColour() and colours.yellow or colours.white) +term.setBackgroundColor(colours.black) +term.setCursorBlink(false) +local _, y = term.getCursorPos() +local _, h = term.getSize() +if not ok then + printError(err) +end +if ok and y >= h then + term.scroll(1) +end +term.setCursorPos(1, h) +if ok then + write("Program finished. ") +end +write("Press any key to continue") +os.pullEvent('key') +]] + -- Menus local bMenu = false local nMenuItem = 1 @@ -89,7 +113,7 @@ local function load(_sPath) end end -local function save(_sPath) +local function save(_sPath, fWrite) -- Create intervening folder local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len()) if not fs.exists(sDir) then @@ -101,8 +125,8 @@ local function save(_sPath) local function innerSave() file, fileerr = fs.open(_sPath, "w") if file then - for _, sLine in ipairs(tLines) do - file.write(sLine .. "\n") + if file then + fWrite(file) end else error("Failed to open " .. _sPath) @@ -293,7 +317,11 @@ local tMenuFuncs = { if bReadOnly then sStatus = "Access denied" else - local ok, _, fileerr = save(sPath) + local ok, _, fileerr = save(sPath, function(file) + for _, sLine in ipairs(tLines) do + file.write(sLine .. "\n") + end + end) if ok then sStatus = "Saved to " .. sPath else @@ -390,8 +418,18 @@ local tMenuFuncs = { bRunning = false end, Run = function() - local sTempPath = "/.temp" - local ok = save(sTempPath) + local sTitle = fs.getName(sPath) + if sTitle:sub(-4) == ".lua" then + sTitle = sTitle:sub(1, -5) + end + local sTempPath = bReadOnly and ".temp." .. sTitle or fs.combine(fs.getDir(sPath), ".temp." .. sTitle) + if fs.exists(sTempPath) then + sStatus = "Error saving to " .. sTempPath + return + end + local ok = save(sTempPath, function(file) + file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@" .. fs.getName(sPath))) + end) if ok then local nTask = shell.openTab(sTempPath) if nTask then @@ -667,8 +705,8 @@ while bRunning do end end - elseif param == keys.enter then - -- Enter + elseif param == keys.enter or param == keys.numPadEnter then + -- Enter/Numpad Enter if not bMenu and not bReadOnly then -- Newline local sLine = tLines[y] @@ -687,7 +725,7 @@ while bRunning do end - elseif param == keys.leftCtrl or param == keys.rightCtrl or param == keys.rightAlt then + elseif param == keys.leftCtrl or param == keys.rightCtrl then -- Menu toggle bMenu = not bMenu if bMenu then @@ -696,7 +734,12 @@ while bRunning do term.setCursorBlink(true) end redrawMenu() - + elseif param == keys.rightAlt then + if bMenu then + bMenu = false + term.setCursorBlink(true) + redrawMenu() + end end elseif sEvent == "char" then @@ -758,6 +801,7 @@ while bRunning do end else bMenu = false + term.setCursorBlink(true) redrawMenu() end end diff --git a/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/paint.lua b/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/paint.lua index 9839d1077..91667464b 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/paint.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/paint.lua @@ -349,7 +349,7 @@ local function accessMenu() selection = #mChoices end - elseif key == keys.enter then + elseif key == keys.enter or key == keys.numPadEnter then -- Select an option return menu_choices[mChoices[selection]]() elseif key == keys.leftCtrl or keys == keys.rightCtrl then diff --git a/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua b/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua index eaaa54869..9d98ac2fd 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua @@ -199,8 +199,8 @@ while true do drawMenu() drawFrontend() end - elseif key == keys.enter then - -- Enter + elseif key == keys.enter or key == keys.numPadEnter then + -- Enter/Numpad Enter break end end diff --git a/src/main/resources/data/computercraft/lua/rom/programs/id.lua b/src/main/resources/data/computercraft/lua/rom/programs/id.lua index 964503e24..79aecf9e6 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/id.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/id.lua @@ -13,16 +13,30 @@ if sDrive == nil then end else - local bData = disk.hasData(sDrive) - if not bData then + if disk.hasAudio(sDrive) then + local title = disk.getAudioTitle(sDrive) + if title then + print("Has audio track \"" .. title .. "\"") + else + print("Has untitled audio") + end + return + end + + if not disk.hasData(sDrive) then print("No disk in drive " .. sDrive) return end - print("The disk is #" .. disk.getID(sDrive)) + local id = disk.getID(sDrive) + if id then + print("The disk is #" .. id) + else + print("Non-disk data source") + end local label = disk.getLabel(sDrive) if label then - print("The disk is labelled \"" .. label .. "\"") + print("Labelled \"" .. label .. "\"") end end diff --git a/src/main/resources/data/computercraft/lua/rom/programs/pocket/falling.lua b/src/main/resources/data/computercraft/lua/rom/programs/pocket/falling.lua index deb01c380..62e8e7eae 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/pocket/falling.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/pocket/falling.lua @@ -546,7 +546,7 @@ local function playGame() msgBox("Game Over!") while true do local _, k = os.pullEvent("key") - if k == keys.space or k == keys.enter then + if k == keys.space or k == keys.enter or k == keys.numPadEnter then break end end @@ -627,7 +627,7 @@ local function runMenu() elseif key == keys.down or key == keys.s then selected = selected % 2 + 1 drawMenu() - elseif key == keys.enter or key == keys.space then + elseif key == keys.enter or key == keys.numPadEnter or key == keys.space then break --begin play! end end diff --git a/src/main/resources/data/computercraft/recipes/disk_1.json b/src/main/resources/data/computercraft/recipes/disk_1.json index b365f90db..f58c4524d 100644 --- a/src/main/resources/data/computercraft/recipes/disk_1.json +++ b/src/main/resources/data/computercraft/recipes/disk_1.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:1118481}" + "nbt": "{Color:1118481}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_10.json b/src/main/resources/data/computercraft/recipes/disk_10.json index 7fdec5607..be3c706d6 100644 --- a/src/main/resources/data/computercraft/recipes/disk_10.json +++ b/src/main/resources/data/computercraft/recipes/disk_10.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:15905484}" + "nbt": "{Color:15905484}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_11.json b/src/main/resources/data/computercraft/recipes/disk_11.json index 5e20d91f3..b16467e7a 100644 --- a/src/main/resources/data/computercraft/recipes/disk_11.json +++ b/src/main/resources/data/computercraft/recipes/disk_11.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:8375321}" + "nbt": "{Color:8375321}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_12.json b/src/main/resources/data/computercraft/recipes/disk_12.json index 42f2962e6..c0ff791a8 100644 --- a/src/main/resources/data/computercraft/recipes/disk_12.json +++ b/src/main/resources/data/computercraft/recipes/disk_12.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:14605932}" + "nbt": "{Color:14605932}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_13.json b/src/main/resources/data/computercraft/recipes/disk_13.json index 02ce61e52..8cf914c0c 100644 --- a/src/main/resources/data/computercraft/recipes/disk_13.json +++ b/src/main/resources/data/computercraft/recipes/disk_13.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:10072818}" + "nbt": "{Color:10072818}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_14.json b/src/main/resources/data/computercraft/recipes/disk_14.json index f741e4faa..a63ed9a0c 100644 --- a/src/main/resources/data/computercraft/recipes/disk_14.json +++ b/src/main/resources/data/computercraft/recipes/disk_14.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:15040472}" + "nbt": "{Color:15040472}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_15.json b/src/main/resources/data/computercraft/recipes/disk_15.json index 07f131bb7..96e20a117 100644 --- a/src/main/resources/data/computercraft/recipes/disk_15.json +++ b/src/main/resources/data/computercraft/recipes/disk_15.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:15905331}" + "nbt": "{Color:15905331}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_16.json b/src/main/resources/data/computercraft/recipes/disk_16.json index cc80c50b6..b3e31354f 100644 --- a/src/main/resources/data/computercraft/recipes/disk_16.json +++ b/src/main/resources/data/computercraft/recipes/disk_16.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:15790320}" + "nbt": "{Color:15790320}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_2.json b/src/main/resources/data/computercraft/recipes/disk_2.json index f073b67b9..b211373ca 100644 --- a/src/main/resources/data/computercraft/recipes/disk_2.json +++ b/src/main/resources/data/computercraft/recipes/disk_2.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:13388876}" + "nbt": "{Color:13388876}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_3.json b/src/main/resources/data/computercraft/recipes/disk_3.json index 902f563ac..311e7fc28 100644 --- a/src/main/resources/data/computercraft/recipes/disk_3.json +++ b/src/main/resources/data/computercraft/recipes/disk_3.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:5744206}" + "nbt": "{Color:5744206}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_4.json b/src/main/resources/data/computercraft/recipes/disk_4.json index 21e0e4f63..dad728d09 100644 --- a/src/main/resources/data/computercraft/recipes/disk_4.json +++ b/src/main/resources/data/computercraft/recipes/disk_4.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:8349260}" + "nbt": "{Color:8349260}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_5.json b/src/main/resources/data/computercraft/recipes/disk_5.json index efcddcdd3..52eca6cf8 100644 --- a/src/main/resources/data/computercraft/recipes/disk_5.json +++ b/src/main/resources/data/computercraft/recipes/disk_5.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:3368652}" + "nbt": "{Color:3368652}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_6.json b/src/main/resources/data/computercraft/recipes/disk_6.json index e099b73e1..f21b7d509 100644 --- a/src/main/resources/data/computercraft/recipes/disk_6.json +++ b/src/main/resources/data/computercraft/recipes/disk_6.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:11691749}" + "nbt": "{Color:11691749}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_7.json b/src/main/resources/data/computercraft/recipes/disk_7.json index 2232c6305..6c5d9335f 100644 --- a/src/main/resources/data/computercraft/recipes/disk_7.json +++ b/src/main/resources/data/computercraft/recipes/disk_7.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:5020082}" + "nbt": "{Color:5020082}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_8.json b/src/main/resources/data/computercraft/recipes/disk_8.json index 1319f7fbf..5670d5fdb 100644 --- a/src/main/resources/data/computercraft/recipes/disk_8.json +++ b/src/main/resources/data/computercraft/recipes/disk_8.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:10066329}" + "nbt": "{Color:10066329}" } } diff --git a/src/main/resources/data/computercraft/recipes/disk_9.json b/src/main/resources/data/computercraft/recipes/disk_9.json index 3552f4418..b1b28a606 100644 --- a/src/main/resources/data/computercraft/recipes/disk_9.json +++ b/src/main/resources/data/computercraft/recipes/disk_9.json @@ -14,6 +14,6 @@ ], "result": { "item": "computercraft:disk", - "nbt": "{color:5000268}" + "nbt": "{Color:5000268}" } } diff --git a/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java b/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java index 17855cf4a..6d4e52278 100644 --- a/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java +++ b/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java @@ -18,10 +18,19 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class FileSystemTest { private static final File ROOT = new File( "test-files/filesystem" ); + private static final long CAPACITY = 1000000; + + private static FileSystem mkFs() throws FileSystemException + { + IWritableMount writableMount = new FileMount( ROOT, CAPACITY ); + return new FileSystem( "hdd", writableMount ); + + } /** * Ensures writing a file truncates it. @@ -33,8 +42,7 @@ public class FileSystemTest @Test public void testWriteTruncates() throws FileSystemException, LuaException, IOException { - IWritableMount writableMount = new FileMount( ROOT, 1000000 ); - FileSystem fs = new FileSystem( "hdd", writableMount ); + FileSystem fs = mkFs(); { FileSystemWrapper writer = fs.openForWrite( "out.txt", false, EncodedWritableHandle::openUtf8 ); @@ -54,4 +62,20 @@ public class FileSystemTest assertEquals( "Tiny line", Files.asCharSource( new File( ROOT, "out.txt" ), StandardCharsets.UTF_8 ).read() ); } + + @Test + public void testUnmountCloses() throws FileSystemException + { + FileSystem fs = mkFs(); + IWritableMount mount = new FileMount( new File( ROOT, "child" ), CAPACITY ); + fs.mountWritable( "disk", "disk", mount ); + + FileSystemWrapper writer = fs.openForWrite( "disk/out.txt", false, EncodedWritableHandle::openUtf8 ); + ObjectWrapper wrapper = new ObjectWrapper( new EncodedWritableHandle( writer.get(), writer ) ); + + fs.unmount( "disk" ); + + LuaException err = assertThrows( LuaException.class, () -> wrapper.call( "write", "Tiny line" ) ); + assertEquals( "attempt to use a closed file", err.getMessage() ); + } } diff --git a/src/test/java/dan200/computercraft/core/terminal/TextBufferTest.java b/src/test/java/dan200/computercraft/core/terminal/TextBufferTest.java new file mode 100644 index 000000000..286e62ad6 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/terminal/TextBufferTest.java @@ -0,0 +1,150 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core.terminal; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TextBufferTest +{ + @Test + void testStringConstructor() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + assertEquals( "test", textBuffer.toString() ); + } + + @Test + void testCharRepetitionConstructor() + { + TextBuffer textBuffer = new TextBuffer( 'a', 5 ); + assertEquals( "aaaaa", textBuffer.toString() ); + } + + @Test + void testLength() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + assertEquals( 4, textBuffer.length() ); + } + + @Test + void testWrite() + { + TextBuffer textBuffer = new TextBuffer( ' ', 4 ); + textBuffer.write( "test" ); + assertEquals( "test", textBuffer.toString() ); + } + + @Test + void testWriteTextBuffer() + { + TextBuffer source = new TextBuffer( "test" ); + TextBuffer target = new TextBuffer( " " ); + target.write( source ); + assertEquals( "test", target.toString() ); + } + + @Test + void testWriteFromPos() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.write( "il", 1 ); + assertEquals( "tilt", textBuffer.toString() ); + } + + @Test + void testWriteOutOfBounds() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.write( "abcdefghijklmnop", -5 ); + assertEquals( "fghi", textBuffer.toString() ); + } + + @Test + void testWriteOutOfBounds2() + { + TextBuffer textBuffer = new TextBuffer( " " ); + textBuffer.write( "Hello, world!", -3 ); + assertEquals( "lo, world! ", textBuffer.toString() ); + } + + @Test + void testFill() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.fill( 'c' ); + assertEquals( "cccc", textBuffer.toString() ); + } + + @Test + void testFillSubstring() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.fill( 'c', 1, 3 ); + assertEquals( "tcct", textBuffer.toString() ); + } + + @Test + void testFillOutOfBounds() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.fill( 'c', -5, 5 ); + assertEquals( "cccc", textBuffer.toString() ); + } + + @Test + void testCharAt() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + assertEquals( 'e', textBuffer.charAt( 1 ) ); + } + + @Test + void testSetChar() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.setChar( 2, 'n' ); + assertEquals( "tent", textBuffer.toString() ); + } + + @Test + void testSetCharWithNegativeIndex() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.setChar( -5, 'n' ); + assertEquals( "test", textBuffer.toString(), "Buffer should not change after setting char with negative index." ); + } + + @Test + void testSetCharWithIndexBeyondBufferEnd() + { + TextBuffer textBuffer = new TextBuffer( "test" ); + textBuffer.setChar( 10, 'n' ); + assertEquals( "test", textBuffer.toString(), "Buffer should not change after setting char beyond buffer end." ); + } + + @Test + void testMultipleOperations() + { + TextBuffer textBuffer = new TextBuffer( ' ', 5 ); + textBuffer.setChar( 0, 'H' ); + textBuffer.setChar( 1, 'e' ); + textBuffer.setChar( 2, 'l' ); + textBuffer.write( "lo", 3 ); + assertEquals( "Hello", textBuffer.toString(), "TextBuffer failed to persist over multiple operations." ); + } + + @Test + void testEmptyBuffer() + { + TextBuffer textBuffer = new TextBuffer( "" ); + // exception on writing to empty buffer would fail the test + textBuffer.write( "test" ); + assertEquals( "", textBuffer.toString() ); + } +} diff --git a/src/test/resources/test-rom/spec/apis/fs_spec.lua b/src/test/resources/test-rom/spec/apis/fs_spec.lua index 883b8c575..32503598e 100644 --- a/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -11,4 +11,212 @@ describe("The fs library", function() expect.error(fs.complete, "", "", true, 1):eq("bad argument #4 (expected boolean, got number)") end) end) + + describe("fs.isDriveRoot", function() + it("validates arguments", function() + fs.isDriveRoot("") + + expect.error(fs.isDriveRoot, nil):eq("bad argument #1 (expected string, got nil)") + end) + + it("correctly identifies drive roots", function() + expect(fs.isDriveRoot("/rom")):eq(true) + expect(fs.isDriveRoot("/")):eq(true) + expect(fs.isDriveRoot("/rom/startup.lua")):eq(false) + expect(fs.isDriveRoot("/rom/programs/delete.lua")):eq(false) + end) + end) + + describe("fs.list", function() + it("fails on files", function() + expect.error(fs.list, "rom/startup.lua"):eq("/rom/startup.lua: Not a directory") + expect.error(fs.list, "startup.lua"):eq("/startup.lua: Not a directory") + end) + + it("fails on non-existent nodes", function() + expect.error(fs.list, "rom/x"):eq("/rom/x: Not a directory") + expect.error(fs.list, "x"):eq("/x: Not a directory") + end) + end) + + describe("fs.combine", function() + it("removes . and ..", function() + expect(fs.combine("./a/b")):eq("a/b") + expect(fs.combine("a/b", "../c")):eq("a/c") + expect(fs.combine("a", "../c")):eq("c") + expect(fs.combine("a", "../../c")):eq("../c") + end) + + it("combines empty paths", function() + expect(fs.combine("a")):eq("a") + expect(fs.combine("a", "")):eq("a") + expect(fs.combine("", "a")):eq("a") + expect(fs.combine("a", "", "b", "c")):eq("a/b/c") + end) + end) + + describe("fs.getSize", function() + it("fails on non-existent nodes", function() + expect.error(fs.getSize, "rom/x"):eq("/rom/x: No such file") + expect.error(fs.getSize, "x"):eq("/x: No such file") + end) + end) + + describe("fs.open", function() + describe("reading", function() + it("fails on directories", function() + expect { fs.open("rom", "r") }:same { nil, "/rom: No such file" } + expect { fs.open("", "r") }:same { nil, "/: No such file" } + end) + + it("fails on non-existent nodes", function() + expect { fs.open("rom/x", "r") }:same { nil, "/rom/x: No such file" } + expect { fs.open("x", "r") }:same { nil, "/x: No such file" } + end) + + it("errors when closing twice", function() + local handle = fs.open("rom/startup.lua", "r") + handle.close() + expect.error(handle.close):eq("attempt to use a closed file") + end) + end) + + describe("reading in binary mode", function() + it("errors when closing twice", function() + local handle = fs.open("rom/startup.lua", "rb") + handle.close() + expect.error(handle.close):eq("attempt to use a closed file") + end) + end) + + describe("writing", function() + it("fails on directories", function() + expect { fs.open("", "w") }:same { nil, "/: Cannot write to directory" } + end) + + it("fails on read-only mounts", function() + expect { fs.open("rom/x", "w") }:same { nil, "/rom/x: Access denied" } + end) + + it("errors when closing twice", function() + local handle = fs.open("test-files/out.txt", "w") + handle.close() + expect.error(handle.close):eq("attempt to use a closed file") + end) + + it("fails gracefully when opening 'CON' on Windows", function() + local ok, err = fs.open("test-files/con", "w") + if ok then fs.delete("test-files/con") return end + + -- On my Windows/Java version the message appears to be "Incorrect function.". It may not be + -- consistent though, and honestly doesn't matter too much. + expect(err):str_match("^/test%-files/con: .*") + end) + end) + + describe("writing in binary mode", function() + it("errors when closing twice", function() + local handle = fs.open("test-files/out.txt", "wb") + handle.close() + expect.error(handle.close):eq("attempt to use a closed file") + end) + end) + + describe("appending", function() + it("fails on directories", function() + expect { fs.open("", "a") }:same { nil, "/: Cannot write to directory" } + end) + + it("fails on read-only mounts", function() + expect { fs.open("rom/x", "a") }:same { nil, "/rom/x: Access denied" } + end) + end) + end) + + describe("fs.makeDir", function() + it("fails on files", function() + expect.error(fs.makeDir, "startup.lua"):eq("/startup.lua: File exists") + end) + + it("fails on read-only mounts", function() + expect.error(fs.makeDir, "rom/x"):eq("/rom/x: Access denied") + end) + end) + + describe("fs.delete", function() + it("fails on read-only mounts", function() + expect.error(fs.delete, "rom/x"):eq("/rom/x: Access denied") + end) + end) + + describe("fs.copy", function() + it("fails on read-only mounts", function() + expect.error(fs.copy, "rom", "rom/startup"):eq("/rom/startup: Access denied") + end) + + it("fails to copy a folder inside itself", function() + fs.makeDir("some-folder") + expect.error(fs.copy, "some-folder", "some-folder/x"):eq("/some-folder: Can't copy a directory inside itself") + expect.error(fs.copy, "some-folder", "Some-Folder/x"):eq("/some-folder: Can't copy a directory inside itself") + end) + + it("copies folders", function() + fs.delete("some-folder") + fs.delete("another-folder") + + fs.makeDir("some-folder") + fs.copy("some-folder", "another-folder") + expect(fs.isDir("another-folder")):eq(true) + end) + end) + + describe("fs.move", function() + it("fails on read-only mounts", function() + expect.error(fs.move, "rom", "rom/move"):eq("Access denied") + expect.error(fs.move, "test-files", "rom/move"):eq("Access denied") + expect.error(fs.move, "rom", "test-files"):eq("Access denied") + end) + end) + + describe("fs.getCapacity", function() + it("returns nil on read-only mounts", function() + expect(fs.getCapacity("rom")):eq(nil) + end) + + it("returns the capacity on the root mount", function() + expect(fs.getCapacity("")):eq(10000000) + end) + end) + + describe("fs.attributes", function() + it("errors on non-existent files", function() + expect.error(fs.attributes, "xuxu_nao_existe"):eq("/xuxu_nao_existe: No such file") + end) + + it("returns information about read-only mounts", function() + expect(fs.attributes("rom")):matches { isDir = true, size = 0, isReadOnly = true } + end) + + it("returns information about files", function() + local now = os.epoch("utc") + + fs.delete("/tmp/basic-file") + local h = fs.open("/tmp/basic-file", "w") + h.write("A reasonably sized string") + h.close() + + local attributes = fs.attributes("tmp/basic-file") + expect(attributes):matches { isDir = false, size = 25, isReadOnly = false } + + if attributes.created - now >= 1000 then + fail(("Expected created time (%d) to be within 1000ms of now (%d"):format(attributes.created, now)) + end + + if attributes.modified - now >= 1000 then + fail(("Expected modified time (%d) to be within 1000ms of now (%d"):format(attributes.modified, now)) + end + + expect(attributes.modification):eq(attributes.modified) + end) + end) end) diff --git a/src/test/resources/test-rom/spec/apis/paintutils_spec.lua b/src/test/resources/test-rom/spec/apis/paintutils_spec.lua index fc2b6008c..f13bbcfd2 100644 --- a/src/test/resources/test-rom/spec/apis/paintutils_spec.lua +++ b/src/test/resources/test-rom/spec/apis/paintutils_spec.lua @@ -67,6 +67,19 @@ describe("The paintutils library", function() { " ", "000", "ffe" }, }) end) + + it("draws a line going diagonally from bottom left", function() + local w = with_window(3, 3, function() + term.setBackgroundColour(colours.red) + paintutils.drawLine(1, 3, 3, 1) + end) + + window_eq(w, { + { " ", "000", "ffe" }, + { " ", "000", "fef" }, + { " ", "000", "eff" }, + }) + end) end) describe("paintutils.drawBox", function() 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 ab881d60b..6e1ba2bb7 100644 --- a/src/test/resources/test-rom/spec/apis/textutils_spec.lua +++ b/src/test/resources/test-rom/spec/apis/textutils_spec.lua @@ -70,6 +70,105 @@ describe("The textutils library", function() expect.error(textutils.serialiseJSON, nil):eq("bad argument #1 (expected table, string, number or boolean, got nil)") expect.error(textutils.serialiseJSON, "", 1):eq("bad argument #2 (expected boolean, got number)") end) + + it("serializes empty arrays", function() + expect(textutils.serializeJSON(textutils.empty_json_array)):eq("[]") + end) + + it("serializes null", function() + expect(textutils.serializeJSON(textutils.json_null)):eq("null") + end) + + it("serializes strings", function() + expect(textutils.serializeJSON('a')):eq('"a"') + expect(textutils.serializeJSON('"')):eq('"\\""') + expect(textutils.serializeJSON('\\')):eq('"\\\\"') + expect(textutils.serializeJSON('/')):eq('"/"') + expect(textutils.serializeJSON('\b')):eq('"\\b"') + expect(textutils.serializeJSON('\n')):eq('"\\n"') + expect(textutils.serializeJSON(string.char(0))):eq('"\\u0000"') + expect(textutils.serializeJSON(string.char(0x0A))):eq('"\\n"') + expect(textutils.serializeJSON(string.char(0x1D))):eq('"\\u001D"') + expect(textutils.serializeJSON(string.char(0x81))):eq('"\\u0081"') + expect(textutils.serializeJSON(string.char(0xFF))):eq('"\\u00FF"') + end) + + it("serializes arrays until the last index with content", function() + expect(textutils.serializeJSON({ 5, "test", nil, nil, 7 })):eq('[5,"test",null,null,7]') + expect(textutils.serializeJSON({ 5, "test", nil, nil, textutils.json_null })):eq('[5,"test",null,null,null]') + expect(textutils.serializeJSON({ nil, nil, nil, nil, "text" })):eq('[null,null,null,null,"text"]') + end) + end) + + describe("textutils.unserializeJSON", function() + describe("parses", function() + it("a list of primitives", function() + expect(textutils.unserializeJSON('[1, true, false, "hello"]')):same { 1, true, false, "hello" } + end) + + it("null when parse_null is true", function() + expect(textutils.unserializeJSON("null", { parse_null = true })):eq(textutils.json_null) + end) + + it("null when parse_null is false", function() + expect(textutils.unserializeJSON("null", { parse_null = false })):eq(nil) + end) + + it("an empty array", function() + expect(textutils.unserializeJSON("[]", { parse_null = false })):eq(textutils.empty_json_array) + end) + + it("basic objects", function() + expect(textutils.unserializeJSON([[{ "a": 1, "b":2 }]])):same { a = 1, b = 2 } + end) + end) + + describe("parses using NBT-style syntax", function() + local function exp(x) + local res, err = textutils.unserializeJSON(x, { nbt_style = true }) + if not res then error(err, 2) end + return expect(res) + end + it("basic objects", function() + exp([[{ a: 1, b:2 }]]):same { a = 1, b = 2 } + end) + + it("suffixed numbers", function() + exp("1b"):eq(1) + exp("1.1d"):eq(1.1) + end) + + it("strings", function() + exp("'123'"):eq("123") + exp("\"123\""):eq("123") + end) + + it("typed arrays", function() + exp("[B; 1, 2, 3]"):same { 1, 2, 3 } + exp("[B;]"):same {} + end) + end) + + describe("passes nst/JSONTestSuite", function() + local search_path = "test-rom/data/json-parsing" + local skip = dofile(search_path .. "/skip.lua") + for _, file in pairs(fs.find(search_path .. "/*.json")) do + local name = fs.getName(file):sub(1, -6); + (skip[name] and pending or it)(name, function() + local h = io.open(file, "r") + local contents = h:read("*a") + h:close() + + local res, err = textutils.unserializeJSON(contents) + local kind = fs.getName(file):sub(1, 1) + if kind == "n" then + expect(res):eq(nil) + elseif kind == "y" then + if err ~= nil then fail("Expected test to pass, but failed with " .. err) end + end + end) + end + end) end) describe("textutils.urlEncode", function()