1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-08-28 08:12:18 +00:00

Merge pull request #29 from Jummit/port-9

Brings us to CC:T 8494ba8ce29cd8d7b9105eef497fe3fe3f89d350 so... who is gonna complain?
This commit is contained in:
Merith 2021-05-16 09:21:31 -07:00 committed by GitHub
commit b0782ec38b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 3360 additions and 1631 deletions

View File

@ -16,7 +16,7 @@ jobs:
java-version: 8 java-version: 8
- name: Cache gradle dependencies - name: Cache gradle dependencies
uses: actions/cache@v1 uses: actions/cache@v2
with: with:
path: ~/.gradle/caches path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('gradle.properties') }} key: ${{ runner.os }}-gradle-${{ hashFiles('gradle.properties') }}

View File

@ -55,8 +55,8 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine: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-api:5.8.9"
modRuntime "me.shedaniel:RoughlyEnoughItems:5.2.10" modRuntime "me.shedaniel:RoughlyEnoughItems:5.8.9"
} }
sourceSets { sourceSets {

21
doc/events/alarm.md Normal file
View File

@ -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")
```

24
doc/events/char.md Normal file
View File

@ -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. <kbd>Ctrl</kbd>) 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
```

View File

@ -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
```

19
doc/events/disk.md Normal file
View File

@ -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
```

19
doc/events/disk_eject.md Normal file
View File

@ -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
```

14
doc/events/http_check.md Normal file
View File

@ -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.

View File

@ -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()
```

View File

@ -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()
```

26
doc/events/key.md Normal file
View File

@ -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, <kbd>F1</kbd> 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
```

24
doc/events/key_up.md Normal file
View File

@ -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, <kbd>F1</kbd> 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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -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
```

34
doc/events/mouse_click.md Normal file
View File

@ -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.
<table class="pretty-table">
<!-- Our markdown parser doesn't work on tables!? Guess I'll have to roll my own soonish :/. -->
<tr><th>Button code</th><th>Mouse button</th></tr>
<tr><td align="right">1</td><td>Left button</td></tr>
<tr><td align="right">2</td><td>Middle button</td></tr>
<tr><td align="right">3</td><td>Right button</td></tr>
</table>
## 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
```

22
doc/events/mouse_drag.md Normal file
View File

@ -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
```

View File

@ -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
```

21
doc/events/mouse_up.md Normal file
View File

@ -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
```

18
doc/events/paste.md Normal file
View File

@ -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
```

19
doc/events/peripheral.md Normal file
View File

@ -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
```

View File

@ -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
```

View File

@ -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
```

14
doc/events/redstone.md Normal file
View File

@ -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
```

View File

@ -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
```

15
doc/events/term_resize.md Normal file
View File

@ -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
```

25
doc/events/terminate.md Normal file
View File

@ -0,0 +1,25 @@
---
module: [kind=event] terminate
---
The @{terminate} event is fired when <kbd>Ctrl-T</kbd> 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
```

21
doc/events/timer.md Normal file
View File

@ -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")
```

View File

@ -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
```

View File

@ -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.")
```

View File

@ -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)
```

View File

@ -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()
```

View File

@ -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()
```

View File

@ -2,16 +2,16 @@
org.gradle.jvmargs=-Xmx1G org.gradle.jvmargs=-Xmx1G
# Mod properties # Mod properties
mod_version=1.95.0-beta mod_version=1.95.3-beta
# Minecraft properties # Minecraft properties
mc_version=1.16.2 mc_version=1.16.4
mappings_version=31 mappings_version=9
# Dependencies # Dependencies
cloth_config_version=4.8.1 cloth_config_version=4.8.1
fabric_api_version=0.19.0+build.398-1.16 fabric_api_version=0.34.2+1.16
fabric_loader_version=0.9.2+build.206 fabric_loader_version=0.11.3
jankson_version=1.2.0 jankson_version=1.2.0
modmenu_version=1.14.6+ modmenu_version=1.14.6+
cloth_api_version=1.4.5 cloth_api_version=1.4.5

View File

@ -536,4 +536,92 @@ e4b0a5b3ce035eb23feb4191432fc49af5772c5b
2020 -> 2021 2020 -> 2021
``` ```
A huge amount of changes. 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
```

View File

@ -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 * Use in conjunction with {@link IComputerAccess#mount} or {@link IComputerAccess#mountWritable} to mount a resource folder onto a computer's file
* system. * 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 domain The domain under which to look for resources. eg: "mymod".
* @param subPath The subPath under which to look for resources. eg: "lua/myfiles". * @param subPath The subPath under which to look for resources. eg: "lua/myfiles".

View File

@ -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; package dan200.computercraft.api;
import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.pocket.IPocketUpgrade;

View File

@ -48,7 +48,7 @@ import net.minecraft.util.Hand;
import net.minecraft.util.collection.DefaultedList; import net.minecraft.util.collection.DefaultedList;
import net.minecraft.util.math.ChunkPos; import net.minecraft.util.math.ChunkPos;
import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3d;
import net.minecraft.village.TraderOfferList; import net.minecraft.village.TradeOfferList;
import net.minecraft.world.GameMode; import net.minecraft.world.GameMode;
/** /**
@ -105,7 +105,7 @@ public class FakePlayer extends ServerPlayerEntity {
} }
@Override @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 @Override
public void openHorseInventory(HorseBaseEntity horse, Inventory inventory) { } public void openHorseInventory(HorseBaseEntity horse, Inventory inventory) { }
@ -251,10 +251,6 @@ public class FakePlayer extends ServerPlayerEntity {
public void disconnect(Text message) { public void disconnect(Text message) {
} }
@Override
public void setupEncryption(SecretKey key) {
}
@Override @Override
public void disableAutoRead() { public void disableAutoRead() {
} }

View File

@ -396,7 +396,8 @@ public class FSAPI implements ILuaAPI {
/** /**
* Get attributes about a specific file or folder. * 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 * 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. * convert it to more usable form.
@ -404,7 +405,7 @@ public class FSAPI implements ILuaAPI {
* @param path The path to get attributes for. * @param path The path to get attributes for.
* @return The resulting attributes. * @return The resulting attributes.
* @throws LuaException If the path does not exist. * @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 #getSize If you only care about the file's size.
* @see #isDir If you only care whether a path is a directory or not. * @see #isDir If you only care whether a path is a directory or not.
*/ */
@ -413,11 +414,12 @@ public class FSAPI implements ILuaAPI {
try { try {
BasicFileAttributes attributes = this.fileSystem.getAttributes(path); BasicFileAttributes attributes = this.fileSystem.getAttributes(path);
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("modification", getFileTime(attributes.lastModifiedTime())); result.put( "modification", getFileTime( attributes.lastModifiedTime() ) );
result.put("modified", getFileTime(attributes.lastModifiedTime())); result.put( "modified", getFileTime( attributes.lastModifiedTime() ) );
result.put("created", getFileTime(attributes.creationTime())); result.put( "created", getFileTime( attributes.creationTime() ) );
result.put("size", attributes.isDirectory() ? 0 : attributes.size()); result.put( "size", attributes.isDirectory() ? 0 : attributes.size() );
result.put("isDir", attributes.isDirectory()); result.put( "isDir", attributes.isDirectory() );
result.put( "isReadOnly", fileSystem.isReadOnly( path ) );
return result; return result;
} catch (FileSystemException e) { } catch (FileSystemException e) {
throw new LuaException(e.getMessage()); throw new LuaException(e.getMessage());

View File

@ -3,185 +3,206 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis; 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.ComputerCraft;
import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.http.CheckUrl; import dan200.computercraft.core.apis.http.*;
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.request.HttpRequest; import dan200.computercraft.core.apis.http.request.HttpRequest;
import dan200.computercraft.core.apis.http.websocket.Websocket; import dan200.computercraft.core.apis.http.websocket.Websocket;
import io.netty.handler.codec.http.DefaultHttpHeaders; 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.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod; 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. * The http library allows communicating with web servers, sending and receiving data from them.
* *
* @cc.module http * @cc.module http
* @hidden * @hidden
*/ */
public class HTTPAPI implements ILuaAPI { public class HTTPAPI implements ILuaAPI
private final IAPIEnvironment m_apiEnvironment; {
private final IAPIEnvironment apiEnvironment;
private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>(); private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>();
private final ResourceGroup<HttpRequest> requests = new ResourceQueue<>(() -> ComputerCraft.httpMaxRequests); private final ResourceGroup<HttpRequest> requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests );
private final ResourceGroup<Websocket> websockets = new ResourceGroup<>(() -> ComputerCraft.httpMaxWebsockets); private final ResourceGroup<Websocket> websockets = new ResourceGroup<>( () -> ComputerCraft.httpMaxWebsockets );
public HTTPAPI(IAPIEnvironment environment) { public HTTPAPI( IAPIEnvironment environment )
this.m_apiEnvironment = environment; {
apiEnvironment = environment;
} }
@Override @Override
public String[] getNames() { public String[] getNames()
return new String[] {"http"}; {
return new String[] { "http" };
} }
@Override @Override
public void startup() { public void startup()
this.checkUrls.startup(); {
this.requests.startup(); checkUrls.startup();
this.websockets.startup(); requests.startup();
websockets.startup();
} }
@Override @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 // It's rather ugly to run this here, but we need to clean up
// resources as often as possible to reduce blocking. // resources as often as possible to reduce blocking.
Resource.cleanup(); Resource.cleanup();
} }
@Override
public void shutdown() {
this.checkUrls.shutdown();
this.requests.shutdown();
this.websockets.shutdown();
}
@LuaFunction @LuaFunction
public final Object[] request(IArguments args) throws LuaException { public final Object[] request( IArguments args ) throws LuaException
{
String address, postString, requestMethod; String address, postString, requestMethod;
Map<?, ?> headerTable; Map<?, ?> headerTable;
boolean binary, redirect; boolean binary, redirect;
if (args.get(0) instanceof Map) { if( args.get( 0 ) instanceof Map )
Map<?, ?> options = args.getTable(0); {
address = getStringField(options, "url"); Map<?, ?> options = args.getTable( 0 );
postString = optStringField(options, "body", null); address = getStringField( options, "url" );
headerTable = optTableField(options, "headers", Collections.emptyMap()); postString = optStringField( options, "body", null );
binary = optBooleanField(options, "binary", false); headerTable = optTableField( options, "headers", Collections.emptyMap() );
requestMethod = optStringField(options, "method", null); binary = optBooleanField( options, "binary", false );
redirect = optBooleanField(options, "redirect", true); requestMethod = optStringField( options, "method", null );
redirect = optBooleanField( options, "redirect", true );
} else { }
else
{
// Get URL and post information // Get URL and post information
address = args.getString(0); address = args.getString( 0 );
postString = args.optString(1, null); postString = args.optString( 1, null );
headerTable = args.optTable(2, Collections.emptyMap()); headerTable = args.optTable( 2, Collections.emptyMap() );
binary = args.optBoolean(3, false); binary = args.optBoolean( 3, false );
requestMethod = null; requestMethod = null;
redirect = true; redirect = true;
} }
HttpHeaders headers = getHeaders(headerTable); HttpHeaders headers = getHeaders( headerTable );
HttpMethod httpMethod; HttpMethod httpMethod;
if (requestMethod == null) { if( requestMethod == null )
{
httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST; httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST;
} else { }
httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT)); else
if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) { {
throw new LuaException("Unsupported HTTP method"); httpMethod = HttpMethod.valueOf( requestMethod.toUpperCase( Locale.ROOT ) );
if( httpMethod == null || requestMethod.equalsIgnoreCase( "CONNECT" ) )
{
throw new LuaException( "Unsupported HTTP method" );
} }
} }
try { try
URI uri = HttpRequest.checkUri(address); {
HttpRequest request = new HttpRequest(this.requests, this.m_apiEnvironment, address, postString, headers, binary, redirect); URI uri = HttpRequest.checkUri( address );
HttpRequest request = new HttpRequest( requests, apiEnvironment, address, postString, headers, binary, redirect );
// Make the request // Make the request
request.queue(r -> r.request(uri, httpMethod)); request.queue( r -> r.request( uri, httpMethod ) );
return new Object[] {true}; return new Object[] { true };
} catch (HTTPRequestException e) { }
return new Object[] { catch( HTTPRequestException e )
false, {
e.getMessage() 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<Map<?, ?>> 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 @Nonnull
private static HttpHeaders getHeaders(@Nonnull Map<?, ?> headerTable) throws LuaException { private HttpHeaders getHeaders( @Nonnull Map<?, ?> headerTable ) throws LuaException
{
HttpHeaders headers = new DefaultHttpHeaders(); HttpHeaders headers = new DefaultHttpHeaders();
for (Map.Entry<?, ?> entry : headerTable.entrySet()) { for( Map.Entry<?, ?> entry : headerTable.entrySet() )
{
Object value = entry.getValue(); Object value = entry.getValue();
if (entry.getKey() instanceof String && value instanceof String) { if( entry.getKey() instanceof String && value instanceof String )
try { {
headers.add((String) entry.getKey(), value); try
} catch (IllegalArgumentException e) { {
throw new LuaException(e.getMessage()); 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; 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<Map<?, ?>> 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()
};
}
}
}

View File

@ -185,8 +185,9 @@ public class RedstoneAPI implements ILuaAPI {
* @see #testBundledInput To determine if a specific colour is set. * @see #testBundledInput To determine if a specific colour is set.
*/ */
@LuaFunction @LuaFunction
public final int getBundledInput(ComputerSide side) { public final int getBundledInput( ComputerSide side )
return this.environment.getBundledOutput(side); {
return environment.getBundledInput( side );
} }
/** /**

View File

@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; package dan200.computercraft.core.apis.handles;
import java.nio.ByteBuffer; 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. * A seekable, readable byte channel which is backed by a simple byte array.
*/ */
public class ArrayByteChannel implements SeekableByteChannel { public class ArrayByteChannel implements SeekableByteChannel
private final byte[] backing; {
private boolean closed = false; private boolean closed = false;
private int position = 0; private int position = 0;
public ArrayByteChannel(byte[] backing) { private final byte[] backing;
public ArrayByteChannel( byte[] backing )
{
this.backing = backing; this.backing = backing;
} }
@Override @Override
public int read(ByteBuffer destination) throws ClosedChannelException { public int read( ByteBuffer destination ) throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
} Objects.requireNonNull( destination, "destination" );
Objects.requireNonNull(destination, "destination");
if (this.position >= this.backing.length) { if( position >= backing.length ) return -1;
return -1;
}
int remaining = Math.min(this.backing.length - this.position, destination.remaining()); int remaining = Math.min( backing.length - position, destination.remaining() );
destination.put(this.backing, this.position, remaining); destination.put( backing, position, remaining );
this.position += remaining; position += remaining;
return remaining; return remaining;
} }
@Override @Override
public int write(ByteBuffer src) throws ClosedChannelException { public int write( ByteBuffer src ) throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
}
throw new NonWritableChannelException(); throw new NonWritableChannelException();
} }
@Override @Override
public long position() throws ClosedChannelException { public long position() throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
} return position;
return this.position;
} }
@Override @Override
public SeekableByteChannel position(long newPosition) throws ClosedChannelException { public SeekableByteChannel position( long newPosition ) throws ClosedChannelException
if (this.closed) { {
throw new 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) { position = (int) newPosition;
throw new IllegalArgumentException("Position out of bounds");
}
this.position = (int) newPosition;
return this; return this;
} }
@Override @Override
public long size() throws ClosedChannelException { public long size() throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
} return backing.length;
return this.backing.length;
} }
@Override @Override
public SeekableByteChannel truncate(long size) throws ClosedChannelException { public SeekableByteChannel truncate( long size ) throws ClosedChannelException
if (this.closed) { {
throw new ClosedChannelException(); if( closed ) throw new ClosedChannelException();
}
throw new NonWritableChannelException(); throw new NonWritableChannelException();
} }
@Override @Override
public boolean isOpen() { public boolean isOpen()
return !this.closed; {
return !closed;
} }
@Override @Override
public void close() { public void close()
this.closed = true; {
closed = true;
} }
} }

View File

@ -3,11 +3,13 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
@ -16,40 +18,43 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; 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 * @cc.module fs.BinaryReadHandle
*/ */
public class BinaryReadableHandle extends HandleGeneric { public class BinaryReadableHandle extends HandleGeneric
{
private static final int BUFFER_SIZE = 8192; 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) { private final ReadableByteChannel reader;
super(closeable); final SeekableByteChannel seekable;
private final ByteBuffer single = ByteBuffer.allocate( 1 );
BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable )
{
super( closeable );
this.reader = reader; this.reader = reader;
this.seekable = seekable; this.seekable = seekable;
} }
public static BinaryReadableHandle of(ReadableByteChannel channel) { public static BinaryReadableHandle of( ReadableByteChannel channel, TrackingCloseable closeable )
return of(channel, channel); {
SeekableByteChannel seekable = asSeekable( channel );
return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
} }
public static BinaryReadableHandle of(ReadableByteChannel channel, Closeable closeable) { public static BinaryReadableHandle of( ReadableByteChannel channel )
SeekableByteChannel seekable = asSeekable(channel); {
return seekable == null ? new BinaryReadableHandle(channel, null, closeable) : new Seekable(seekable, closeable); return of( channel, new TrackingCloseable.Impl( channel ) );
} }
/** /**
* Read a number of bytes from this file. * Read a number of bytes from this file.
* *
* @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This may be 0 to determine we are at * @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This
* the end of the file. * may be 0 to determine we are at the end of the file.
* @return The read bytes. * @return The read bytes.
* @throws LuaException When trying to read a negative number of bytes. * @throws LuaException When trying to read a negative number of bytes.
* @throws LuaException If the file has been closed. * @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. * @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
*/ */
@LuaFunction @LuaFunction
public final Object[] read(Optional<Integer> countArg) throws LuaException { public final Object[] read( Optional<Integer> countArg ) throws LuaException
this.checkOpen(); {
try { checkOpen();
if (countArg.isPresent()) { try
{
if( countArg.isPresent() )
{
int count = countArg.get(); int count = countArg.get();
if (count < 0) { if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" );
throw new LuaException("Cannot read a negative number of bytes"); if( count == 0 && seekable != null )
} {
if (count == 0 && this.seekable != null) { return seekable.position() >= seekable.size() ? null : new Object[] { "" };
return this.seekable.position() >= this.seekable.size() ? null : new Object[] {""};
} }
if (count <= BUFFER_SIZE) { if( count <= BUFFER_SIZE )
ByteBuffer buffer = ByteBuffer.allocate(count); {
ByteBuffer buffer = ByteBuffer.allocate( count );
int read = this.reader.read(buffer); int read = reader.read( buffer );
if (read < 0) { if( read < 0 ) return null;
return null;
}
buffer.flip(); buffer.flip();
return new Object[] {buffer}; return new Object[] { buffer };
} else { }
else
{
// Read the initial set of characters, failing if none are read. // Read the initial set of characters, failing if none are read.
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE );
int read = this.reader.read(buffer); int read = reader.read( buffer );
if (read < 0) { if( read < 0 ) return null;
return null;
}
// If we failed to read "enough" here, let's just abort // 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(); buffer.flip();
return new Object[] {buffer}; return new Object[] { buffer };
} }
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
// than doubling up the buffer each time. // than doubling up the buffer each time.
int totalRead = read; int totalRead = read;
List<ByteBuffer> parts = new ArrayList<>(4); List<ByteBuffer> parts = new ArrayList<>( 4 );
parts.add(buffer); parts.add( buffer );
while (read >= BUFFER_SIZE && totalRead < count) { while( read >= BUFFER_SIZE && totalRead < count )
buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead)); {
read = this.reader.read(buffer); buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) );
if (read < 0) { read = reader.read( buffer );
break; if( read < 0 ) break;
}
totalRead += read; totalRead += read;
parts.add(buffer); parts.add( buffer );
} }
// Now just copy all the bytes across! // Now just copy all the bytes across!
byte[] bytes = new byte[totalRead]; byte[] bytes = new byte[totalRead];
int pos = 0; int pos = 0;
for (ByteBuffer part : parts) { for( ByteBuffer part : parts )
System.arraycopy(part.array(), 0, bytes, pos, part.position()); {
System.arraycopy( part.array(), 0, bytes, pos, part.position() );
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; 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. * @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end.
*/ */
@LuaFunction @LuaFunction
public final Object[] readAll() throws LuaException { public final Object[] readAll() throws LuaException
this.checkOpen(); {
try { checkOpen();
try
{
int expected = 32; int expected = 32;
if (this.seekable != null) { if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) );
expected = Math.max(expected, (int) (this.seekable.size() - this.seekable.position())); ByteArrayOutputStream stream = new ByteArrayOutputStream( expected );
}
ByteArrayOutputStream stream = new ByteArrayOutputStream(expected);
ByteBuffer buf = ByteBuffer.allocate(8192); ByteBuffer buf = ByteBuffer.allocate( 8192 );
boolean readAnything = false; boolean readAnything = false;
while (true) { while( true )
{
buf.clear(); buf.clear();
int r = this.reader.read(buf); int r = reader.read( buf );
if (r == -1) { if( r == -1 ) break;
break;
}
readAnything = true; readAnything = true;
stream.write(buf.array(), 0, r); stream.write( buf.array(), 0, r );
} }
return readAnything ? new Object[] {stream.toByteArray()} : null; return readAnything ? new Object[] { stream.toByteArray() } : null;
} catch (IOException e) { }
catch( IOException e )
{
return null; 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. * @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
*/ */
@LuaFunction @LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException { public final Object[] readLine( Optional<Boolean> withTrailingArg ) throws LuaException
this.checkOpen(); {
boolean withTrailing = withTrailingArg.orElse(false); checkOpen();
try { boolean withTrailing = withTrailingArg.orElse( false );
try
{
ByteArrayOutputStream stream = new ByteArrayOutputStream(); ByteArrayOutputStream stream = new ByteArrayOutputStream();
boolean readAnything = false, readRc = false; boolean readAnything = false, readRc = false;
while (true) { while( true )
this.single.clear(); {
int read = this.reader.read(this.single); single.clear();
if (read <= 0) { 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 // Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it
// back. // back.
if (readRc) { if( readRc ) stream.write( '\r' );
stream.write('\r'); return readAnything ? new Object[] { stream.toByteArray() } : null;
}
return readAnything ? new Object[] {stream.toByteArray()} : null;
} }
readAnything = true; readAnything = true;
byte chr = this.single.get(0); byte chr = single.get( 0 );
if (chr == '\n') { if( chr == '\n' )
if (withTrailing) { {
if (readRc) { if( withTrailing )
stream.write('\r'); {
} if( readRc ) stream.write( '\r' );
stream.write(chr); stream.write( chr );
} }
return new Object[] {stream.toByteArray()}; return new Object[] { stream.toByteArray() };
} else { }
else
{
// We want to skip \r\n, but obviously need to include cases where \r is not followed by \n. // 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 // Note, this behaviour is non-standard compliant (strictly speaking we should have no
// special logic for \r), but we preserve compatibility with EncodedReadableHandle and // special logic for \r), but we preserve compatibility with EncodedReadableHandle and
// previous behaviour of the io library. // previous behaviour of the io library.
if (readRc) { if( readRc ) stream.write( '\r' );
stream.write('\r');
}
readRc = chr == '\r'; readRc = chr == '\r';
if (!readRc) { if( !readRc ) stream.write( chr );
stream.write(chr);
}
} }
} }
} catch (IOException e) { }
catch( IOException e )
{
return null; return null;
} }
} }
public static class Seekable extends BinaryReadableHandle { public static class Seekable extends BinaryReadableHandle
Seekable(SeekableByteChannel seekable, Closeable closeable) { {
super(seekable, seekable, closeable); 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 * Seek to a new position within the file, changing where bytes are written to. The new position is an offset
* start position determined by {@code whence}: * 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. * - {@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. * 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. * @cc.treturn string The reason seeking failed.
*/ */
@LuaFunction @LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException { public final Object[] seek( Optional<String> whence, Optional<Long> offset ) throws LuaException
this.checkOpen(); {
return handleSeek(this.seekable, whence, offset); checkOpen();
return handleSeek( seekable, whence, offset );
} }
} }
} }

View File

@ -3,10 +3,14 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
@ -14,34 +18,34 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.util.Optional; 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 * @cc.module fs.BinaryWriteHandle
*/ */
public class BinaryWritableHandle extends HandleGeneric { public class BinaryWritableHandle extends HandleGeneric
final SeekableByteChannel seekable; {
private final WritableByteChannel writer; 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) { protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, TrackingCloseable closeable )
super(closeable); {
super( closeable );
this.writer = writer; this.writer = writer;
this.seekable = seekable; this.seekable = seekable;
} }
public static BinaryWritableHandle of(WritableByteChannel channel) { public static BinaryWritableHandle of( WritableByteChannel channel, TrackingCloseable closeable )
return of(channel, channel); {
SeekableByteChannel seekable = asSeekable( channel );
return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
} }
public static BinaryWritableHandle of(WritableByteChannel channel, Closeable closeable) { public static BinaryWritableHandle of( WritableByteChannel channel )
SeekableByteChannel seekable = asSeekable(channel); {
return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable); return of( channel, new TrackingCloseable.Impl( channel ) );
} }
/** /**
@ -53,24 +57,33 @@ public class BinaryWritableHandle extends HandleGeneric {
* @cc.tparam [2] string The string to write. * @cc.tparam [2] string The string to write.
*/ */
@LuaFunction @LuaFunction
public final void write(IArguments arguments) throws LuaException { public final void write( IArguments arguments ) throws LuaException
this.checkOpen(); {
try { checkOpen();
Object arg = arguments.get(0); try
if (arg instanceof Number) { {
Object arg = arguments.get( 0 );
if( arg instanceof Number )
{
int number = ((Number) arg).intValue(); int number = ((Number) arg).intValue();
this.single.clear(); single.clear();
this.single.put((byte) number); single.put( (byte) number );
this.single.flip(); single.flip();
this.writer.write(this.single); writer.write( single );
} else if (arg instanceof String) {
this.writer.write(arguments.getBytes(0));
} else {
throw LuaValues.badArgumentOf(0, "string or number", arg);
} }
} catch (IOException e) { else if( arg instanceof String )
throw new LuaException(e.getMessage()); {
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. * @throws LuaException If the file has been closed.
*/ */
@LuaFunction @LuaFunction
public final void flush() throws LuaException { public final void flush() throws LuaException
this.checkOpen(); {
try { checkOpen();
try
{
// Technically this is not needed // Technically this is not needed
if (this.writer instanceof FileChannel) { if( writer instanceof FileChannel ) ((FileChannel) writer).force( false );
((FileChannel) this.writer).force(false); }
} catch( IOException ignored )
} catch (IOException ignored) { {
} }
} }
public static class Seekable extends BinaryWritableHandle { public static class Seekable extends BinaryWritableHandle
public Seekable(SeekableByteChannel seekable, Closeable closeable) { {
super(seekable, seekable, closeable); 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 * Seek to a new position within the file, changing where bytes are written to. The new position is an offset
* start position determined by {@code whence}: * 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. * - {@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. * 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. * @cc.treturn string The reason seeking failed.
*/ */
@LuaFunction @LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException { public final Object[] seek( Optional<String> whence, Optional<Long> offset ) throws LuaException
this.checkOpen(); {
return handleSeek(this.seekable, whence, offset); checkOpen();
return handleSeek( seekable, whence, offset );
} }
} }
} }

View File

@ -3,11 +3,14 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.BufferedReader;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
@ -17,41 +20,27 @@ import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Optional; 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 * @cc.module fs.ReadHandle
*/ */
public class EncodedReadableHandle extends HandleGeneric { public class EncodedReadableHandle extends HandleGeneric
{
private static final int BUFFER_SIZE = 8192; private static final int BUFFER_SIZE = 8192;
private final BufferedReader reader; private final BufferedReader reader;
public EncodedReadableHandle(@Nonnull BufferedReader reader) { public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull TrackingCloseable closable )
this(reader, reader); {
} super( closable );
public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull Closeable closable) {
super(closable);
this.reader = reader; this.reader = reader;
} }
public static BufferedReader openUtf8(ReadableByteChannel channel) { public EncodedReadableHandle( @Nonnull BufferedReader reader )
return open(channel, StandardCharsets.UTF_8); {
} this( reader, new TrackingCloseable.Impl( reader ) );
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));
} }
/** /**
@ -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. * @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
*/ */
@LuaFunction @LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException { public final Object[] readLine( Optional<Boolean> withTrailingArg ) throws LuaException
this.checkOpen(); {
boolean withTrailing = withTrailingArg.orElse(false); checkOpen();
try { boolean withTrailing = withTrailingArg.orElse( false );
String line = this.reader.readLine(); try
if (line != null) { {
String line = reader.readLine();
if( line != null )
{
// While this is technically inaccurate, it's better than nothing // While this is technically inaccurate, it's better than nothing
if (withTrailing) { if( withTrailing ) line += "\n";
line += "\n"; return new Object[] { line };
} }
return new Object[] {line}; else
} else { {
return null; return null;
} }
} catch (IOException e) { }
catch( IOException e )
{
return null; 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. * @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
*/ */
@LuaFunction @LuaFunction
public final Object[] readAll() throws LuaException { public final Object[] readAll() throws LuaException
this.checkOpen(); {
try { checkOpen();
try
{
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
String line = this.reader.readLine(); String line = reader.readLine();
while (line != null) { while( line != null )
result.append(line); {
line = this.reader.readLine(); result.append( line );
if (line != null) { line = reader.readLine();
result.append("\n"); if( line != null )
{
result.append( "\n" );
} }
} }
return new Object[] {result.toString()}; return new Object[] { result.toString() };
} catch (IOException e) { }
catch( IOException e )
{
return null; 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. * @cc.treturn string|nil The read characters, or {@code nil} if at the of the file.
*/ */
@LuaFunction @LuaFunction
public final Object[] read(Optional<Integer> countA) throws LuaException { public final Object[] read( Optional<Integer> countA ) throws LuaException
this.checkOpen(); {
try { checkOpen();
int count = countA.orElse(1); try
if (count < 0) { {
int count = countA.orElse( 1 );
if( count < 0 )
{
// Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so // Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so
// it seems best to remain somewhat consistent. // it seems best to remain somewhat consistent.
throw new LuaException("Cannot read a negative number of characters"); throw new LuaException( "Cannot read a negative number of characters" );
} else if (count <= BUFFER_SIZE) { }
else if( count <= BUFFER_SIZE )
{
// If we've got a small count, then allocate that and read it. // If we've got a small count, then allocate that and read it.
char[] chars = new char[count]; 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)}; return read < 0 ? null : new Object[] { new String( chars, 0, read ) };
} else { }
else
{
// If we've got a large count, read in bunches of 8192. // If we've got a large count, read in bunches of 8192.
char[] buffer = new char[BUFFER_SIZE]; char[] buffer = new char[BUFFER_SIZE];
// Read the initial set of characters, failing if none are read. // Read the initial set of characters, failing if none are read.
int read = this.reader.read(buffer, 0, Math.min(buffer.length, count)); int read = reader.read( buffer, 0, Math.min( buffer.length, count ) );
if (read < 0) { if( read < 0 ) return null;
return null;
}
StringBuilder out = new StringBuilder(read); StringBuilder out = new StringBuilder( read );
int totalRead = 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 // Otherwise read until we either reach the limit or we no longer consume
// the full buffer. // the full buffer.
while (read >= BUFFER_SIZE && totalRead < count) { while( read >= BUFFER_SIZE && totalRead < count )
read = this.reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead)); {
if (read < 0) { read = reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) );
break; if( read < 0 ) break;
}
totalRead += read; 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; 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 ) );
}
} }

View File

@ -3,11 +3,16 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.BufferedWriter;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
@ -16,39 +21,21 @@ import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction; import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets; 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. * A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes.
* *
* @cc.module fs.WriteHandle * @cc.module fs.WriteHandle
*/ */
public class EncodedWritableHandle extends HandleGeneric { public class EncodedWritableHandle extends HandleGeneric
{
private final BufferedWriter writer; private final BufferedWriter writer;
public EncodedWritableHandle(@Nonnull BufferedWriter writer, @Nonnull Closeable closable) { public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull TrackingCloseable closable )
super(closable); {
super( closable );
this.writer = writer; 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. * 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. * @cc.param value The value to write to the file.
*/ */
@LuaFunction @LuaFunction
public final void write(IArguments args) throws LuaException { public final void write( IArguments args ) throws LuaException
this.checkOpen(); {
String text = StringUtil.toString(args.get(0)); checkOpen();
try { String text = StringUtil.toString( args.get( 0 ) );
this.writer.write(text, 0, text.length()); try
} catch (IOException e) { {
throw new LuaException(e.getMessage()); 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. * @cc.param value The value to write to the file.
*/ */
@LuaFunction @LuaFunction
public final void writeLine(IArguments args) throws LuaException { public final void writeLine( IArguments args ) throws LuaException
this.checkOpen(); {
String text = StringUtil.toString(args.get(0)); checkOpen();
try { String text = StringUtil.toString( args.get( 0 ) );
this.writer.write(text, 0, text.length()); try
this.writer.newLine(); {
} catch (IOException e) { writer.write( text, 0, text.length() );
throw new LuaException(e.getMessage()); 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. * @throws LuaException If the file has been closed.
*/ */
@LuaFunction @LuaFunction
public final void flush() throws LuaException { public final void flush() throws LuaException
this.checkOpen(); {
try { checkOpen();
this.writer.flush(); try
} catch (IOException ignored) { {
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 ) );
}
} }

View File

@ -3,79 +3,38 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.handles; 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.io.IOException;
import java.nio.channels.Channel; import java.nio.channels.Channel;
import java.nio.channels.SeekableByteChannel; import java.nio.channels.SeekableByteChannel;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Nonnull; public abstract class HandleGeneric
{
private TrackingCloseable closeable;
import dan200.computercraft.api.lua.LuaException; protected HandleGeneric( @Nonnull TrackingCloseable closeable )
import dan200.computercraft.api.lua.LuaFunction; {
import dan200.computercraft.shared.util.IoUtil; this.closeable = closeable;
public abstract class HandleGeneric {
private Closeable closable;
private boolean open = true;
protected HandleGeneric(@Nonnull Closeable closable) {
this.closable = closable;
} }
/** protected void checkOpen() throws LuaException
* Shared implementation for various file handle types. {
* TrackingCloseable closeable = this.closeable;
* @param channel The channel to seek in if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" );
* @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 <a href="https://www.lua.org/manual/5.1/manual.html#pdf-file:seek">{@code file:seek} in the Lua manual.</a>
*/
protected static Object[] handleSeek(SeekableByteChannel channel, Optional<String> whence, Optional<Long> offset) throws LuaException {
long actualOffset = offset.orElse(0L);
try {
switch (whence.orElse("cur")) {
case "set":
channel.position(actualOffset);
break;
case "cur":
channel.position(channel.position() + actualOffset);
break;
case "end":
channel.position(channel.size() + actualOffset);
break;
default:
throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'");
}
return new Object[] {channel.position()};
} catch (IllegalArgumentException e) {
return new Object[] {
null,
"Position is negative"
};
} catch (IOException e) {
return null;
}
} }
protected static SeekableByteChannel asSeekable(Channel channel) { protected final void close()
if (!(channel instanceof SeekableByteChannel)) { {
return null; IoUtil.closeQuietly( closeable );
} closeable = null;
SeekableByteChannel seekable = (SeekableByteChannel) channel;
try {
seekable.position(seekable.position());
return seekable;
} catch (IOException | UnsupportedOperationException e) {
return null;
}
} }
/** /**
@ -85,22 +44,69 @@ public abstract class HandleGeneric {
* *
* @throws LuaException If the file has already been closed. * @throws LuaException If the file has already been closed.
*/ */
@LuaFunction ("close") @LuaFunction( "close" )
public final void doClose() throws LuaException { public final void doClose() throws LuaException
this.checkOpen(); {
this.close(); 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 <a href="https://www.lua.org/manual/5.1/manual.html#pdf-file:seek">{@code file:seek} in the Lua manual.</a>
*/
protected static Object[] handleSeek( SeekableByteChannel channel, Optional<String> whence, Optional<Long> 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() { protected static SeekableByteChannel asSeekable( Channel channel )
this.open = false; {
if( !(channel instanceof SeekableByteChannel) ) return null;
IoUtil.closeQuietly(this.closable); SeekableByteChannel seekable = (SeekableByteChannel) channel;
this.closable = null; try
{
seekable.position( seekable.position() );
return seekable;
}
catch( IOException | UnsupportedOperationException e )
{
return null;
}
} }
} }

View File

@ -3,19 +3,8 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.apis.http.request; 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.ComputerCraft;
import dan200.computercraft.core.apis.handles.ArrayByteChannel; import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle; import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
@ -29,22 +18,20 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.CompositeByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.*;
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;
public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpObject> 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<HttpObject> implements Closeable
{
/** /**
* Same as {@link io.netty.handler.codec.MessageAggregator}. * Same as {@link io.netty.handler.codec.MessageAggregator}.
*/ */
@ -53,16 +40,19 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
private static final byte[] EMPTY_BYTES = new byte[0]; private static final byte[] EMPTY_BYTES = new byte[0];
private final HttpRequest request; private final HttpRequest request;
private boolean closed = false;
private final URI uri; private final URI uri;
private final HttpMethod method; private final HttpMethod method;
private final Options options; private final Options options;
private final HttpHeaders responseHeaders = new DefaultHttpHeaders();
private boolean closed = false;
private Charset responseCharset; private Charset responseCharset;
private final HttpHeaders responseHeaders = new DefaultHttpHeaders();
private HttpResponseStatus responseStatus; private HttpResponseStatus responseStatus;
private CompositeByteBuf responseBody; private CompositeByteBuf responseBody;
HttpRequestHandler(HttpRequest request, URI uri, HttpMethod method, Options options) { HttpRequestHandler( HttpRequest request, URI uri, HttpMethod method, Options options )
{
this.request = request; this.request = request;
this.uri = uri; this.uri = uri;
@ -71,203 +61,199 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
} }
@Override @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { public void channelActive( ChannelHandlerContext ctx ) throws Exception
if (this.request.checkClosed()) { {
return; if( request.checkClosed() ) return;
}
ByteBuf body = this.request.body(); ByteBuf body = request.body();
body.resetReaderIndex() body.resetReaderIndex().retain();
.retain();
String requestUri = this.uri.getRawPath(); String requestUri = uri.getRawPath();
if (this.uri.getRawQuery() != null) { if( uri.getRawQuery() != null ) requestUri += "?" + uri.getRawQuery();
requestUri += "?" + this.uri.getRawQuery();
}
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, requestUri, body); FullHttpRequest request = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, requestUri, body );
request.setMethod(this.method); request.setMethod( method );
request.headers() request.headers().set( this.request.headers() );
.set(this.request.headers());
// We force some headers to be always applied // We force some headers to be always applied
if (!request.headers() if( !request.headers().contains( HttpHeaderNames.ACCEPT_CHARSET ) )
.contains(HttpHeaderNames.ACCEPT_CHARSET)) { {
request.headers() request.headers().set( HttpHeaderNames.ACCEPT_CHARSET, "UTF-8" );
.set(HttpHeaderNames.ACCEPT_CHARSET, "UTF-8");
} }
if (!request.headers() request.headers().set( HttpHeaderNames.HOST, uri.getPort() < 0 ? uri.getHost() : uri.getHost() + ":" + uri.getPort() );
.contains(HttpHeaderNames.USER_AGENT)) { request.headers().set( HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE );
request.headers()
.set(HttpHeaderNames.USER_AGENT,
this.request.environment()
.getComputerEnvironment()
.getUserAgent());
}
request.headers()
.set(HttpHeaderNames.HOST, this.uri.getPort() < 0 ? this.uri.getHost() : this.uri.getHost() + ":" + this.uri.getPort());
request.headers()
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
ctx.channel() ctx.channel().writeAndFlush( request );
.writeAndFlush(request);
super.channelActive(ctx); super.channelActive( ctx );
} }
@Override @Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception { public void channelInactive( ChannelHandlerContext ctx ) throws Exception
if (!this.closed) { {
this.request.failure("Could not connect"); if( !closed ) request.failure( "Could not connect" );
} super.channelInactive( ctx );
super.channelInactive(ctx);
} }
@Override @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void channelRead0( ChannelHandlerContext ctx, HttpObject message )
if (ComputerCraft.logPeripheralErrors) { {
ComputerCraft.log.error("Error handling HTTP response", cause); if( closed || request.checkClosed() ) return;
}
this.request.failure(cause);
}
@Override if( message instanceof HttpResponse )
public void channelRead0(ChannelHandlerContext ctx, HttpObject message) { {
if (this.closed || this.request.checkClosed()) {
return;
}
if (message instanceof HttpResponse) {
HttpResponse response = (HttpResponse) message; HttpResponse response = (HttpResponse) message;
if (this.request.redirects.get() > 0) { if( request.redirects.get() > 0 )
URI redirect = this.getRedirect(response.status(), response.headers()); {
if (redirect != null && !this.uri.equals(redirect) && this.request.redirects.getAndDecrement() > 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. // 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 // We mark ourselves as disposed first though, to avoid firing events when the channel
// becomes inactive or disposed. // becomes inactive or disposed.
this.closed = true; closed = true;
ctx.close(); ctx.close();
try { try
HttpRequest.checkUri(redirect); {
} catch (HTTPRequestException e) { HttpRequest.checkUri( redirect );
}
catch( HTTPRequestException e )
{
// If we cannot visit this uri, then fail. // If we cannot visit this uri, then fail.
this.request.failure(e.getMessage()); request.failure( e.getMessage() );
return; return;
} }
this.request.request(redirect, request.request( redirect, response.status().code() == 303 ? HttpMethod.GET : method );
response.status()
.code() == 303 ? HttpMethod.GET : this.method);
return; return;
} }
} }
this.responseCharset = HttpUtil.getCharset(response, StandardCharsets.UTF_8); responseCharset = HttpUtil.getCharset( response, StandardCharsets.UTF_8 );
this.responseStatus = response.status(); responseStatus = response.status();
this.responseHeaders.add(response.headers()); responseHeaders.add( response.headers() );
} }
if (message instanceof HttpContent) { if( message instanceof HttpContent )
{
HttpContent content = (HttpContent) message; HttpContent content = (HttpContent) message;
if (this.responseBody == null) { if( responseBody == null )
this.responseBody = ctx.alloc() {
.compositeBuffer(DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS); responseBody = ctx.alloc().compositeBuffer( DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS );
} }
ByteBuf partial = content.content(); 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 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) { if( options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload )
this.closed = true; {
closed = true;
ctx.close(); ctx.close();
this.request.failure("Response is too large"); request.failure( "Response is too large" );
return; return;
} }
this.responseBody.addComponent(true, partial.retain()); responseBody.addComponent( true, partial.retain() );
} }
if (message instanceof LastHttpContent) { if( message instanceof LastHttpContent )
{
LastHttpContent last = (LastHttpContent) message; LastHttpContent last = (LastHttpContent) message;
this.responseHeaders.add(last.trailingHeaders()); responseHeaders.add( last.trailingHeaders() );
// Set the content length, if not already given. // Set the content length, if not already given.
if (this.responseHeaders.contains(HttpHeaderNames.CONTENT_LENGTH)) { if( responseHeaders.contains( HttpHeaderNames.CONTENT_LENGTH ) )
this.responseHeaders.set(HttpHeaderNames.CONTENT_LENGTH, this.responseBody.readableBytes()); {
responseHeaders.set( HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes() );
} }
ctx.close(); 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<String, String> headers = new HashMap<>();
for( Map.Entry<String, String> 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. * 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. * @param headers The headers of the HTTP response.
* @return The URI to redirect to, or {@code null} if no redirect should occur. * @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(); 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; 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<String, String> headers = new HashMap<>();
for (Map.Entry<String, String> 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 @Override
public void close() { public void close()
this.closed = true; {
if (this.responseBody != null) { closed = true;
this.responseBody.release(); if( responseBody != null )
this.responseBody = null; {
responseBody.release();
responseBody = null;
} }
} }
} }

View File

@ -64,9 +64,9 @@ public final class Generator<T> {
private final Function<T, T> wrap; private final Function<T, T> wrap;
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder.newBuilder() private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder.newBuilder()
.build(CacheLoader.from(this::build)); .build(CacheLoader.from(catching(this::build, Optional.empty())));
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder.newBuilder() private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder.newBuilder()
.build(CacheLoader.from(this::build)); .build(CacheLoader.from(catching(this::build, Collections.emptyList())));
Generator(Class<T> base, List<Class<?>> context, Function<T, T> wrap) { Generator(Class<T> base, List<Class<?>> context, Function<T, T> wrap) {
this.base = base; this.base = base;
@ -374,4 +374,22 @@ public final class Generator<T> {
method.getName()); method.getName());
return null; return null;
} }
@SuppressWarnings( "Guava" )
private static <T, U> com.google.common.base.Function<T, U> catching( Function<T, U> 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;
}
};
}
} }

View File

@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.filesystem; package dan200.computercraft.core.filesystem;
import java.io.Closeable; 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. * 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, * When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown
* 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. * 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 <T> The type of the closeable object to write. * @param <T> The type of the closeable object to write.
*/ */
class ChannelWrapper<T extends Closeable> implements Closeable { class ChannelWrapper<T extends Closeable> implements Closeable
{
private final T wrapper; private final T wrapper;
private final Channel channel; private final Channel channel;
ChannelWrapper(T wrapper, Channel channel) { ChannelWrapper( T wrapper, Channel channel )
{
this.wrapper = wrapper; this.wrapper = wrapper;
this.channel = channel; this.channel = channel;
} }
@Override @Override
public void close() throws IOException { public void close() throws IOException
try { {
this.wrapper.close(); try
} finally { {
this.channel.close(); wrapper.close();
}
finally
{
channel.close();
} }
} }
public T get() { T get()
return this.wrapper; {
return wrapper;
} }
} }

View File

@ -3,48 +3,68 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.filesystem; package dan200.computercraft.core.filesystem;
import dan200.computercraft.shared.util.IoUtil;
import javax.annotation.Nonnull;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.ReferenceQueue; import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import javax.annotation.Nonnull;
/** /**
* An alternative closeable implementation that will free up resources in the filesystem. * 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 * The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of
* gone), then the wrapped object will be closed by the filesystem. * (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. * 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 * In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks
* numerous instances. * on the stream, it's not really possible as it'd require numerous instances.
* *
* @param <T> The type of writer or channel to wrap. * @param <T> The type of writer or channel to wrap.
*/ */
public class FileSystemWrapper<T extends Closeable> implements Closeable { public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable
final WeakReference<FileSystemWrapper<?>> self; {
private final FileSystem fileSystem; private final FileSystem fileSystem;
final MountWrapper mount;
private final ChannelWrapper<T> closeable; private final ChannelWrapper<T> closeable;
final WeakReference<FileSystemWrapper<?>> self;
private boolean isOpen = true;
FileSystemWrapper(FileSystem fileSystem, ChannelWrapper<T> closeable, ReferenceQueue<FileSystemWrapper<?>> queue) { FileSystemWrapper( FileSystem fileSystem, MountWrapper mount, ChannelWrapper<T> closeable, ReferenceQueue<FileSystemWrapper<?>> queue )
{
this.fileSystem = fileSystem; this.fileSystem = fileSystem;
this.mount = mount;
this.closeable = closeable; this.closeable = closeable;
this.self = new WeakReference<>(this, queue); self = new WeakReference<>( this, queue );
} }
@Override @Override
public void close() throws IOException { public void close() throws IOException
this.fileSystem.removeFile(this); {
this.closeable.close(); isOpen = false;
fileSystem.removeFile( this );
closeable.close();
}
void closeExternally()
{
isOpen = false;
IoUtil.closeQuietly( closeable );
}
@Override
public boolean isOpen()
{
return isOpen;
} }
@Nonnull @Nonnull
public T get() { public T get()
return this.closeable.get(); {
return closeable.get();
} }
} }

View File

@ -90,19 +90,30 @@ public final class ResourceMount implements IMount {
private void load() { private void load() {
boolean hasAny = false; boolean hasAny = false;
FileEntry newRoot = new FileEntry(new Identifier(this.namespace, this.subPath)); String existingNamespace = null;
for (Identifier file : this.manager.findResources(this.subPath, s -> true)) {
if (!file.getNamespace() FileEntry newRoot = new FileEntry( new Identifier( namespace, subPath ) );
.equals(this.namespace)) { for( Identifier file : manager.findResources( subPath, s -> true ) )
continue; {
} existingNamespace = file.getNamespace();
if( !file.getNamespace().equals( namespace ) ) continue;
String localPath = FileSystem.toLocal(file.getPath(), this.subPath); String localPath = FileSystem.toLocal(file.getPath(), this.subPath);
this.create(newRoot, localPath); this.create(newRoot, localPath);
hasAny = true; 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) { private void create(FileEntry lastEntry, String path) {

View File

@ -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();
}
}
}

View File

@ -3,168 +3,84 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.core.terminal; package dan200.computercraft.core.terminal;
public class TextBuffer { public class TextBuffer
private final char[] m_text; {
private final char[] text;
public TextBuffer(char c, int length) { public TextBuffer( char c, int length )
this.m_text = new char[length]; {
for (int i = 0; i < length; i++) { text = new char[length];
this.m_text[i] = c; this.fill( c );
}
} }
public TextBuffer(String text) { public TextBuffer( String text )
this(text, 1); {
this.text = text.toCharArray();
} }
public TextBuffer(String text, int repetitions) { public int length()
int textLength = text.length(); {
this.m_text = new char[textLength * repetitions]; return text.length;
for (int i = 0; i < repetitions; i++) {
for (int j = 0; j < textLength; j++) {
this.m_text[j + i * textLength] = text.charAt(j);
}
}
} }
public TextBuffer(TextBuffer text) { public void write( String text )
this(text, 1); {
write( text, 0 );
} }
public TextBuffer(TextBuffer text, int repetitions) { public void write( String text, int start )
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) {
int pos = start; int pos = start;
start = Math.max(start, 0); start = Math.max( start, 0 );
end = Math.min(end, pos + text.length()); int end = Math.min( start + text.length(), pos + text.length() );
end = Math.min(end, this.m_text.length); end = Math.min( end, this.text.length );
for (int i = start; i < end; i++) { for( int i = start; i < end; i++ )
this.m_text[i] = text.charAt(i - pos); {
this.text[i] = text.charAt( i - pos );
} }
} }
public void write(String text, int start) { public void write( TextBuffer text )
this.write(text, start, start + text.length()); {
} int end = Math.min( text.length(), this.text.length );
for( int i = 0; i < end; i++ )
public void write(TextBuffer text) { {
this.write(text, 0, text.length()); this.text[i] = text.charAt( i );
}
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 start) { public void fill( char c )
this.write(text, start, start + text.length()); {
fill( c, 0, text.length );
} }
public void fill(char c) { public void fill( char c, int start, int end )
this.fill(c, 0, this.m_text.length); {
} start = Math.max( start, 0 );
end = Math.min( end, text.length );
public void fill(char c, int start, int end) { for( int i = start; i < end; i++ )
start = Math.max(start, 0); {
end = Math.min(end, this.m_text.length); text[i] = c;
for (int i = start; i < end; i++) {
this.m_text[i] = c;
} }
} }
public void fill(char c, int start) { public char charAt( int i )
this.fill(c, start, this.m_text.length); {
return text[i];
} }
public void fill(String text) { public void setChar( int i, char c )
this.fill(text, 0, this.m_text.length); {
} if( i >= 0 && i < text.length )
{
public void fill(String text, int start, int end) { text[i] = c;
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(String text, int start) { public String toString()
this.fill(text, start, this.m_text.length); {
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);
}
}

View File

@ -32,7 +32,7 @@ public class MixinWorld {
@Inject (method = "setBlockEntity", at = @At ("HEAD")) @Inject (method = "setBlockEntity", at = @At ("HEAD"))
public void setBlockEntity(BlockPos pos, @Nullable BlockEntity entity, CallbackInfo info) { 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); setWorld(entity, this);
} }
} }

View File

@ -31,7 +31,7 @@ public final class BundledRedstone {
} }
public static int getDefaultOutput(@Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side) { 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) { 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) { private static int getUnmaskedOutput(World world, BlockPos pos, Direction side) {
if (!World.method_24794(pos)) { if (!World.isValid(pos)) {
return -1; return -1;
} }

View File

@ -35,13 +35,11 @@ public final class Peripherals {
@Nullable @Nullable
public static IPeripheral getPeripheral(World world, BlockPos pos, Direction side) { 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 @Nullable
private static IPeripheral getPeripheralAt(World world, BlockPos pos, Direction side) { private static IPeripheral getPeripheralAt(World world, BlockPos pos, Direction side) {
BlockEntity block = world.getBlockEntity(pos);
// Try the handlers in order: // Try the handlers in order:
for (IPeripheralProvider peripheralProvider : providers) { for (IPeripheralProvider peripheralProvider : providers) {
try { try {

View File

@ -3,61 +3,59 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.shared; package dan200.computercraft.shared;
import java.util.ArrayList; import dan200.computercraft.api.pocket.IPocketUpgrade;
import java.util.Collections; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap;
import java.util.HashMap; import net.fabricmc.loader.api.FabricLoader;
import java.util.IdentityHashMap; import net.fabricmc.loader.api.ModContainer;
import java.util.List; import net.minecraft.item.ItemStack;
import java.util.Map; import net.minecraft.util.Util;
import java.util.Objects;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.*;
import dan200.computercraft.ComputerCraft; public final class PocketUpgrades
import dan200.computercraft.api.pocket.IPocketUpgrade; {
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.item.ItemStack;
public final class PocketUpgrades {
private static final Map<String, IPocketUpgrade> upgrades = new HashMap<>(); private static final Map<String, IPocketUpgrade> upgrades = new HashMap<>();
private static final IdentityHashMap<IPocketUpgrade, String> upgradeOwners = new IdentityHashMap<>(); private static final Map<IPocketUpgrade, String> upgradeOwners = new Object2ObjectLinkedOpenCustomHashMap<>( Util.identityHashStrategy() );
private PocketUpgrades() {} private PocketUpgrades() {}
public static synchronized void register(@Nonnull IPocketUpgrade upgrade) { public static synchronized void register( @Nonnull IPocketUpgrade upgrade )
Objects.requireNonNull(upgrade, "upgrade cannot be null"); {
Objects.requireNonNull( upgrade, "upgrade cannot be null" );
String id = upgrade.getUpgradeID() String id = upgrade.getUpgradeID().toString();
.toString(); IPocketUpgrade existing = upgrades.get( id );
IPocketUpgrade existing = upgrades.get(id); if( existing != null )
if (existing != null) { {
throw new IllegalStateException("Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is " + throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " pocket computer'" );
"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. // Fix a typo in the advanced modem upgrade's name. I'm sorry, I realise this is horrible.
if (id.equals("computercraft:advanved_modem")) { if( id.equals( "computercraft:advanved_modem" ) ) id = "computercraft:advanced_modem";
id = "computercraft:advanced_modem";
}
return upgrades.get(id); return upgrades.get( id );
} }
public static IPocketUpgrade get(@Nonnull ItemStack stack) { public static IPocketUpgrade get( @Nonnull ItemStack stack )
if (stack.isEmpty()) { {
return null; if( stack.isEmpty() ) return null;
}
for (IPocketUpgrade upgrade : upgrades.values()) { for( IPocketUpgrade upgrade : upgrades.values() )
{
ItemStack craftingStack = upgrade.getCraftingItem(); ItemStack craftingStack = upgrade.getCraftingItem();
if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && upgrade.isItemSuitable( stack ) ) if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && upgrade.isItemSuitable( stack ) )
{ {
@ -69,19 +67,22 @@ public final class PocketUpgrades {
} }
@Nullable @Nullable
public static String getOwner(IPocketUpgrade upgrade) { public static String getOwner( IPocketUpgrade upgrade )
return upgradeOwners.get(upgrade); {
return upgradeOwners.get( upgrade );
} }
public static Iterable<IPocketUpgrade> getVanillaUpgrades() { public static Iterable<IPocketUpgrade> getVanillaUpgrades()
{
List<IPocketUpgrade> vanilla = new ArrayList<>(); List<IPocketUpgrade> vanilla = new ArrayList<>();
vanilla.add(ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal); vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal );
vanilla.add(ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced); vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced );
vanilla.add(ComputerCraftRegistry.PocketUpgrades.speaker); vanilla.add( ComputerCraftRegistry.PocketUpgrades.speaker );
return vanilla; return vanilla;
} }
public static Iterable<IPocketUpgrade> getUpgrades() { public static Iterable<IPocketUpgrade> getUpgrades()
return Collections.unmodifiableCollection(upgrades.values()); {
return Collections.unmodifiableCollection( upgrades.values() );
} }
} }

View File

@ -3,9 +3,15 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com * Send enquiries to dratcliffe@gmail.com
*/ */
package dan200.computercraft.shared; 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.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
@ -13,86 +19,72 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.annotation.Nonnull; public final class TurtleUpgrades
import javax.annotation.Nullable; {
private static class Wrapper
{
final ITurtleUpgrade upgrade;
final String id;
final String modId;
boolean enabled;
import dan200.computercraft.ComputerCraft; Wrapper( ITurtleUpgrade upgrade )
import dan200.computercraft.api.turtle.ITurtleUpgrade; {
import dan200.computercraft.shared.computer.core.ComputerFamily; 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<String, ITurtleUpgrade> upgrades = new HashMap<>(); private static final Map<String, ITurtleUpgrade> upgrades = new HashMap<>();
private static final IdentityHashMap<ITurtleUpgrade, Wrapper> wrappers = new IdentityHashMap<>(); private static final IdentityHashMap<ITurtleUpgrade, Wrapper> wrappers = new IdentityHashMap<>();
private static ITurtleUpgrade[] vanilla;
private static boolean needsRebuild; private static boolean needsRebuild;
private TurtleUpgrades() {} private TurtleUpgrades() {}
public static void register(@Nonnull ITurtleUpgrade upgrade) { public static void register( @Nonnull ITurtleUpgrade upgrade )
Objects.requireNonNull(upgrade, "upgrade cannot be null"); {
Objects.requireNonNull( upgrade, "upgrade cannot be null" );
rebuild(); rebuild();
Wrapper wrapper = new Wrapper(upgrade); Wrapper wrapper = new Wrapper( upgrade );
String id = wrapper.id; String id = wrapper.id;
ITurtleUpgrade existing = upgrades.get(id); ITurtleUpgrade existing = upgrades.get( id );
if (existing != null) { if( existing != null )
throw new IllegalStateException("Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already " + {
"registered by '" + existing.getUnlocalisedAdjective() + " Turtle'"); throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" );
} }
upgrades.put(id, upgrade); upgrades.put( id, upgrade );
wrappers.put(upgrade, wrapper); 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;
} }
@Nullable @Nullable
public static ITurtleUpgrade get(String id) { public static ITurtleUpgrade get( String id )
{
rebuild(); rebuild();
return upgrades.get(id); return upgrades.get( id );
} }
@Nullable @Nullable
public static String getOwner(@Nonnull ITurtleUpgrade upgrade) { public static String getOwner( @Nonnull ITurtleUpgrade upgrade )
Wrapper wrapper = wrappers.get(upgrade); {
Wrapper wrapper = wrappers.get( upgrade );
return wrapper != null ? wrapper.modId : null; return wrapper != null ? wrapper.modId : null;
} }
public static ITurtleUpgrade get(@Nonnull ItemStack stack) { public static ITurtleUpgrade get( @Nonnull ItemStack stack )
if (stack.isEmpty()) { {
return null; if( stack.isEmpty() ) return null;
}
for (Wrapper wrapper : wrappers.values()) { for( Wrapper wrapper : wrappers.values() )
if (!wrapper.enabled) { {
continue; if( !wrapper.enabled ) continue;
}
ItemStack craftingStack = wrapper.upgrade.getCraftingItem(); ItemStack craftingStack = wrapper.upgrade.getCraftingItem();
if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade.isItemSuitable( stack ) ) if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade.isItemSuitable( stack ) )
@ -104,8 +96,10 @@ public final class TurtleUpgrades {
return null; return null;
} }
public static Stream<ITurtleUpgrade> getVanillaUpgrades() { public static Stream<ITurtleUpgrade> getVanillaUpgrades()
if (vanilla == null) { {
if( vanilla == null )
{
vanilla = new ITurtleUpgrade[] { vanilla = new ITurtleUpgrade[] {
// ComputerCraft upgrades // ComputerCraft upgrades
ComputerCraftRegistry.TurtleUpgrades.wirelessModemNormal, ComputerCraftRegistry.TurtleUpgrades.wirelessModemNormal,
@ -119,64 +113,69 @@ public final class TurtleUpgrades {
ComputerCraftRegistry.TurtleUpgrades.diamondShovel, ComputerCraftRegistry.TurtleUpgrades.diamondShovel,
ComputerCraftRegistry.TurtleUpgrades.diamondHoe, ComputerCraftRegistry.TurtleUpgrades.diamondHoe,
ComputerCraftRegistry.TurtleUpgrades.craftingTable, ComputerCraftRegistry.TurtleUpgrades.craftingTable,
ComputerCraftRegistry.TurtleUpgrades.netheritePickaxe,
}; };
} }
return Arrays.stream(vanilla) return Arrays.stream( vanilla ).filter( x -> x != null && wrappers.get( x ).enabled );
.filter(x -> x != null && wrappers.get(x).enabled);
} }
public static Stream<ITurtleUpgrade> getUpgrades() { public static Stream<ITurtleUpgrade> getUpgrades()
return wrappers.values() {
.stream() return wrappers.values().stream().filter( x -> x.enabled ).map( x -> x.upgrade );
.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; return true;
} }
public static void enable(ITurtleUpgrade upgrade) { /**
Wrapper wrapper = wrappers.get(upgrade); * Rebuild the cache of turtle upgrades. This is done before querying the cache or registering new upgrades.
if (wrapper.enabled) { */
return; 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; wrapper.enabled = true;
needsRebuild = true; needsRebuild = true;
} }
public static void disable(ITurtleUpgrade upgrade) { public static void disable( ITurtleUpgrade upgrade )
Wrapper wrapper = wrappers.get(upgrade); {
if (!wrapper.enabled) { Wrapper wrapper = wrappers.get( upgrade );
return; if( !wrapper.enabled ) return;
}
wrapper.enabled = false; wrapper.enabled = false;
upgrades.remove(wrapper.id); upgrades.remove( wrapper.id );
} }
public static void remove(ITurtleUpgrade upgrade) { public static void remove( ITurtleUpgrade upgrade )
wrappers.remove(upgrade); {
wrappers.remove( upgrade );
needsRebuild = true; 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;
}
}
}

View File

@ -17,7 +17,7 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import dan200.computercraft.api.turtle.FakePlayer; import dan200.computercraft.api.turtle.FakePlayer;
import net.minecraft.entity.Entity; 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.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;

View File

@ -55,8 +55,9 @@ public final class RepeatArgumentType<T, U> implements ArgumentType<List<T>> {
this.some = some; this.some = some;
} }
public static <T> RepeatArgumentType<T, T> some(ArgumentType<T> appender, SimpleCommandExceptionType missing) { public static <T> RepeatArgumentType<T, T> some( ArgumentType<T> appender, SimpleCommandExceptionType missing )
return new RepeatArgumentType<>(appender, List::add, true, missing); {
return new RepeatArgumentType<>( appender, List::add, false, missing );
} }
public static <T> RepeatArgumentType<T, List<T>> someFlat(ArgumentType<List<T>> appender, SimpleCommandExceptionType missing) { public static <T> RepeatArgumentType<T, List<T>> someFlat(ArgumentType<List<T>> appender, SimpleCommandExceptionType missing) {

View File

@ -8,7 +8,6 @@ package dan200.computercraft.shared.common;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.shared.util.ColourTracker; import dan200.computercraft.shared.util.ColourTracker;
import dan200.computercraft.shared.util.ColourUtils; import dan200.computercraft.shared.util.ColourUtils;
@ -66,17 +65,10 @@ public final class ColourableRecipe extends SpecialCraftingRecipe {
if (stack.isEmpty()) { if (stack.isEmpty()) {
continue; continue;
} }
else
if (stack.getItem() instanceof IColouredItem) { {
colourable = stack; DyeColor dye = ColourUtils.getStackColour( stack );
} else { if( dye != null ) tracker.addColour( dye );
DyeColor dye = ColourUtils.getStackColour(stack);
if (dye == null) {
continue;
}
Colour colour = Colour.fromInt(15 - dye.getId());
tracker.addColour(colour.getR(), colour.getG(), colour.getB());
} }
} }

View File

@ -207,7 +207,7 @@ public class CommandAPI implements ILuaAPI {
World world = this.computer.getWorld(); World world = this.computer.getWorld();
BlockPos min = new BlockPos(Math.min(minX, maxX), Math.min(minY, maxY), Math.min(minZ, maxZ)); 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)); 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"); throw new LuaException("Co-ordinates out of range");
} }
@ -284,7 +284,7 @@ public class CommandAPI implements ILuaAPI {
// Get the details of the block // Get the details of the block
World world = this.computer.getWorld(); World world = this.computer.getWorld();
BlockPos position = new BlockPos(x, y, z); BlockPos position = new BlockPos(x, y, z);
if (World.method_24794(position)) { if (World.isValid(position)) {
return getBlockInfo(world, position); return getBlockInfo(world, position);
} else { } else {
throw new LuaException("Co-ordinates out of range"); throw new LuaException("Co-ordinates out of range");

View File

@ -316,7 +316,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput
@Nonnull @Nonnull
@Override @Override
public String getHostString() { 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 @Nonnull

View File

@ -53,7 +53,9 @@ public class DiskRecipe extends SpecialCraftingRecipe {
return false; return false;
} }
redstoneFound = true; redstoneFound = true;
} else if (ColourUtils.getStackColour(stack) != null) { }
else if( ColourUtils.getStackColour( stack ) == null )
{
return false; return false;
} }
} }
@ -74,14 +76,10 @@ public class DiskRecipe extends SpecialCraftingRecipe {
continue; continue;
} }
if (!this.paper.test(stack) && !this.redstone.test(stack)) { if( !paper.test( stack ) && !redstone.test( stack ) )
DyeColor dye = ColourUtils.getStackColour(stack); {
if (dye == null) { DyeColor dye = ColourUtils.getStackColour( stack );
continue; if( dye != null ) tracker.addColour( dye );
}
Colour colour = Colour.VALUES[dye.getId()];
tracker.addColour(colour.getR(), colour.getG(), colour.getB());
} }
} }

View File

@ -85,8 +85,8 @@ public class InventoryMethods implements GenericSource
* @link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail includes. More information can be fetched * @link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail includes. More information can be fetched
* with {@link #getItemDetail}. * with {@link #getItemDetail}.
* *
* The table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs` rather than * The returned table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs`
* `ipairs`. * rather than `ipairs`.
* *
* @param inventory The current inventory. * @param inventory The current inventory.
* @return All items in this inventory. * @return All items in this inventory.

View File

@ -191,6 +191,10 @@ public class TurtleAPI implements ILuaAPI {
/** /**
* Place a block or item into the world in front of the turtle. * 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. * @param args Arguments to place.
* @return The turtle command result. * @return The turtle command result.
* @cc.tparam [opt] string text When placing a sign, set its contents to this text. * @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.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 boolean Whether the block could be placed.
* @cc.treturn string|nil The reason the block was not placed. * @cc.treturn string|nil The reason the block was not placed.
* @see #place For more information about placing items.
*/ */
@LuaFunction @LuaFunction
public final MethodResult placeUp(IArguments args) { 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.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 boolean Whether the block could be placed.
* @cc.treturn string|nil The reason the block was not placed. * @cc.treturn string|nil The reason the block was not placed.
* @see #place For more information about placing items.
*/ */
@LuaFunction @LuaFunction
public final MethodResult placeDown(IArguments args) { public final MethodResult placeDown(IArguments args) {
@ -380,16 +386,34 @@ public class TurtleAPI implements ILuaAPI {
return this.trackCommand(new TurtleDetectCommand(InteractDirection.DOWN)); 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 @LuaFunction
public final MethodResult compare() { public final MethodResult compare() {
return this.trackCommand(new TurtleCompareCommand(InteractDirection.FORWARD)); 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 @LuaFunction
public final MethodResult compareUp() { public final MethodResult compareUp() {
return this.trackCommand(new TurtleCompareCommand(InteractDirection.UP)); 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 @LuaFunction
public final MethodResult compareDown() { public final MethodResult compareDown() {
return this.trackCommand(new TurtleCompareCommand(InteractDirection.DOWN)); 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))); 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 @LuaFunction
public final Object getFuelLevel() { public final Object getFuelLevel() {
return this.turtle.isFuelNeeded() ? this.turtle.getFuelLevel() : "unlimited"; 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.
* <pre>{@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}</pre>
* @cc.usage Check if the current item is a valid fuel source.
* <pre>{@code
* local is_fuel, reason = turtle.refuel(0)
* if not is_fuel then printError(reason) end
* }</pre>
* @see #getFuelLevel()
* @see #getFuelLimit()
*/
@LuaFunction @LuaFunction
public final MethodResult refuel(Optional<Integer> countA) throws LuaException { public final MethodResult refuel(Optional<Integer> countA) throws LuaException {
int count = countA.orElse(Integer.MAX_VALUE); int count = countA.orElse(Integer.MAX_VALUE);
@ -492,11 +561,29 @@ public class TurtleAPI implements ILuaAPI {
return this.trackCommand(new TurtleRefuelCommand(count)); 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 @LuaFunction
public final MethodResult compareTo(int slot) throws LuaException { public final MethodResult compareTo(int slot) throws LuaException {
return this.trackCommand(new TurtleCompareToCommand(checkSlot(slot))); 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 @LuaFunction
public final MethodResult transferTo(int slotArg, Optional<Integer> countArg) throws LuaException { public final MethodResult transferTo(int slotArg, Optional<Integer> countArg) throws LuaException {
int slot = checkSlot(slotArg); int slot = checkSlot(slotArg);
@ -515,16 +602,53 @@ public class TurtleAPI implements ILuaAPI {
return this.turtle.getSelectedSlot() + 1; 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 @LuaFunction
public final Object getFuelLimit() { public final Object getFuelLimit() {
return this.turtle.isFuelNeeded() ? this.turtle.getFuelLimit() : "unlimited"; 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 @LuaFunction
public final MethodResult equipLeft() { public final MethodResult equipLeft() {
return this.trackCommand(new TurtleEquipCommand(TurtleSide.LEFT)); 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 @LuaFunction
public final MethodResult equipRight() { public final MethodResult equipRight() {
return this.trackCommand(new TurtleEquipCommand(TurtleSide.RIGHT)); return this.trackCommand(new TurtleEquipCommand(TurtleSide.RIGHT));

View File

@ -119,10 +119,10 @@ public class TurtleMoveCommand implements ITurtleCommand {
} }
private static TurtleCommandResult canEnter(TurtlePlayer turtlePlayer, World world, BlockPos position) { 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"); 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"); return TurtleCommandResult.failure("Cannot leave the world");
} }

View File

@ -364,7 +364,7 @@ public class TurtlePlaceCommand implements ITurtleCommand {
private static boolean canDeployOnBlock(@Nonnull ItemPlacementContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position, private static boolean canDeployOnBlock(@Nonnull ItemPlacementContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position,
Direction side, boolean allowReplaceable, String[] outErrorMessage) { Direction side, boolean allowReplaceable, String[] outErrorMessage) {
World world = turtle.getWorld(); 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, .getItem() instanceof BlockItem && WorldUtil.isLiquidBlock(world,
position))) { position))) {
return false; return false;

View File

@ -77,7 +77,7 @@ public final class TurtlePlayer extends FakePlayer {
private void setState(ITurtleAccess turtle) { private void setState(ITurtleAccess turtle) {
if (this.currentScreenHandler != playerScreenHandler) { if (this.currentScreenHandler != playerScreenHandler) {
ComputerCraft.log.warn("Turtle has open container ({})", this.currentScreenHandler); ComputerCraft.log.warn("Turtle has open container ({})", this.currentScreenHandler);
closeCurrentScreen(); closeHandledScreen();
} }
BlockPos position = turtle.getPosition(); BlockPos position = turtle.getPosition();
@ -91,13 +91,8 @@ public final class TurtlePlayer extends FakePlayer {
} }
public static TurtlePlayer get(ITurtleAccess access) { public static TurtlePlayer get(ITurtleAccess access) {
ServerWorld world = (ServerWorld) access.getWorld();
if( !(access instanceof TurtleBrain) ) return create( access ); if( !(access instanceof TurtleBrain) ) return create( access );
/*if (!(access instanceof TurtleBrain)) {
return new TurtlePlayer(world, access.getOwningPlayer());
}*/
TurtleBrain brain = (TurtleBrain) access; TurtleBrain brain = (TurtleBrain) access;
TurtlePlayer player = brain.m_cachedPlayer; TurtlePlayer player = brain.m_cachedPlayer;
if (player == null || player.getGameProfile() != getProfile(access.getOwningPlayer()) || player.getEntityWorld() != access.getWorld()) { if (player == null || player.getGameProfile() != getProfile(access.getOwningPlayer()) || player.getEntityWorld() != access.getWorld()) {

View File

@ -6,6 +6,8 @@
package dan200.computercraft.shared.util; 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. * A reimplementation of the colour system in {@link ArmorDyeRecipe}, but bundled together as an object.
*/ */
@ -28,8 +30,15 @@ public class ColourTracker {
this.count++; this.count++;
} }
public boolean hasColour() { public void addColour( DyeColor dye )
return this.count > 0; {
Colour colour = Colour.VALUES[15 - dye.getId()];
addColour( colour.getR(), colour.getG(), colour.getB() );
}
public boolean hasColour()
{
return count > 0;
} }
public int getColour() { public int getColour() {

View File

@ -14,7 +14,6 @@ import dan200.computercraft.api.turtle.FakePlayer;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener; import io.netty.util.concurrent.GenericFutureListener;
import net.minecraft.network.ClientConnection; import net.minecraft.network.ClientConnection;
import net.minecraft.network.NetworkSide; import net.minecraft.network.NetworkSide;
import net.minecraft.network.NetworkState; 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.BookUpdateC2SPacket;
import net.minecraft.network.packet.c2s.play.ButtonClickC2SPacket; import net.minecraft.network.packet.c2s.play.ButtonClickC2SPacket;
import net.minecraft.network.packet.c2s.play.ChatMessageC2SPacket; 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.ClientCommandC2SPacket;
import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket; import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket;
import net.minecraft.network.packet.c2s.play.ClientStatusC2SPacket; 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.CraftRequestC2SPacket;
import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket;
import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; 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.HandSwingC2SPacket;
import net.minecraft.network.packet.c2s.play.KeepAliveC2SPacket; import net.minecraft.network.packet.c2s.play.KeepAliveC2SPacket;
import net.minecraft.network.packet.c2s.play.PickFromInventoryC2SPacket; 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.RenameItemC2SPacket;
import net.minecraft.network.packet.c2s.play.RequestCommandCompletionsC2SPacket; import net.minecraft.network.packet.c2s.play.RequestCommandCompletionsC2SPacket;
import net.minecraft.network.packet.c2s.play.ResourcePackStatusC2SPacket; 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.SpectatorTeleportC2SPacket;
import net.minecraft.network.packet.c2s.play.TeleportConfirmC2SPacket; import net.minecraft.network.packet.c2s.play.TeleportConfirmC2SPacket;
import net.minecraft.network.packet.c2s.play.UpdateBeaconC2SPacket; import net.minecraft.network.packet.c2s.play.UpdateBeaconC2SPacket;
@ -127,10 +122,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler {
public void onJigsawUpdate(@Nonnull UpdateJigsawC2SPacket packet) { public void onJigsawUpdate(@Nonnull UpdateJigsawC2SPacket packet) {
} }
@Override
public void onVillagerTradeSelect(@Nonnull SelectVillagerTradeC2SPacket packet) {
}
@Override @Override
public void onBookUpdate(@Nonnull BookUpdateC2SPacket packet) { public void onBookUpdate(@Nonnull BookUpdateC2SPacket packet) {
} }
@ -207,14 +198,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler {
public void onClientStatus(@Nonnull ClientStatusC2SPacket packet) { public void onClientStatus(@Nonnull ClientStatusC2SPacket packet) {
} }
@Override
public void onGuiClose(@Nonnull GuiCloseC2SPacket packet) {
}
@Override
public void onClickWindow(@Nonnull ClickWindowC2SPacket packet) {
}
@Override @Override
public void onCraftRequest(@Nonnull CraftRequestC2SPacket packet) { public void onCraftRequest(@Nonnull CraftRequestC2SPacket packet) {
} }
@ -227,10 +210,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler {
public void onCreativeInventoryAction(@Nonnull CreativeInventoryActionC2SPacket packet) { public void onCreativeInventoryAction(@Nonnull CreativeInventoryActionC2SPacket packet) {
} }
@Override
public void onConfirmTransaction(@Nonnull ConfirmGuiActionC2SPacket packet) {
}
@Override @Override
public void onSignUpdate(@Nonnull UpdateSignC2SPacket packet) { public void onSignUpdate(@Nonnull UpdateSignC2SPacket packet) {
} }
@ -309,10 +288,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler {
this.closeReason = message; this.closeReason = message;
} }
@Override
public void setupEncryption(@Nonnull SecretKey key) {
}
@Nonnull @Nonnull
@Override @Override
public PacketListener getPacketListener() { public PacketListener getPacketListener() {

View File

@ -40,7 +40,7 @@ public final class WorldUtil {
.makeMap(); .makeMap();
public static boolean isLiquidBlock(World world, BlockPos pos) { public static boolean isLiquidBlock(World world, BlockPos pos) {
if (!World.method_24794(pos)) { if (!World.isValid(pos)) {
return false; return false;
} }
return world.getBlockState(pos) return world.getBlockState(pos)

View File

@ -38,5 +38,21 @@
"upgrade.computercraft.speaker.adjective": "(Alto-Falante)", "upgrade.computercraft.speaker.adjective": "(Alto-Falante)",
"chat.computercraft.wired_modem.peripheral_connected": "Periférico \"%s\" conectado à rede", "chat.computercraft.wired_modem.peripheral_connected": "Periférico \"%s\" conectado à rede",
"chat.computercraft.wired_modem.peripheral_disconnected": "Periférico \"%s\" desconectado da 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."
} }

View File

@ -1,11 +1,34 @@
{ {
"type": "minecraft:block", "type": "minecraft:block",
"pools": [ "pools": [
{
"name": "main",
"rolls": 1,
"entries": [
{ {
"rolls": 1, "type": "minecraft:dynamic",
"entries": [ "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"
}
}
]
}
]
}
]
}

View File

@ -83,9 +83,9 @@ end
--- Tries to retrieve the computer or turtles own location. --- Tries to retrieve the computer or turtles own location.
-- --
-- @tparam[opt] number timeout The maximum time taken to establish our -- @tparam[opt=2] number timeout The maximum time in seconds taken to establish our
-- position. Defaults to 2 seconds if not specified. -- position.
-- @tparam[opt] boolean debug Print debugging messages -- @tparam[opt=false] boolean debug Print debugging messages
-- @treturn[1] number This computer's `x` position. -- @treturn[1] number This computer's `x` position.
-- @treturn[1] number This computer's `y` position. -- @treturn[1] number This computer's `y` position.
-- @treturn[1] number This computer's `z` position. -- @treturn[1] number This computer's `z` position.

View File

@ -137,6 +137,15 @@ handleMetatable = {
return handle.seek(whence, offset) return handle.seek(whence, offset)
end, 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, setvbuf = function(self, mode, size) end,
--- Write one or more values to the file --- Write one or more values to the file

View File

@ -132,7 +132,17 @@ function drawLine(startX, startY, endX, endY, colour)
return return
end 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? -- TODO: clip to screen rectangle?

View File

@ -161,7 +161,9 @@ end
-- @tparam string name The name of the peripheral to wrap. -- @tparam string name The name of the peripheral to wrap.
-- @treturn table|nil The table containing the peripheral's methods, or `nil` if -- @treturn table|nil The table containing the peripheral's methods, or `nil` if
-- there is no peripheral present with the given name. -- 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) function wrap(name)
expect(1, name, "string") expect(1, name, "string")
@ -183,16 +185,25 @@ function wrap(name)
return result return result
end end
--- Find all peripherals of a specific type, and return the --[[- Find all peripherals of a specific type, and return the
-- @{peripheral.wrap|wrapped} peripherals. @{peripheral.wrap|wrapped} peripherals.
--
-- @tparam string ty The type of peripheral to look for. @tparam string ty The type of peripheral to look for.
-- @tparam[opt] function(name:string, wrapped:table):boolean filter A @tparam[opt] function(name:string, wrapped:table):boolean filter A
-- filter function, which takes the peripheral's name and wrapped table filter function, which takes the peripheral's name and wrapped table
-- and returns if it should be included in the result. and returns if it should be included in the result.
-- @treturn table... 0 or more wrapped peripherals matching the given filters. @treturn table... 0 or more wrapped peripherals matching the given filters.
-- @usage { peripheral.find("monitor") } @usage Find all monitors and store them in a table, writing "Hello" on each one.
-- @usage peripheral.find("modem", rednet.open)
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) function find(ty, filter)
expect(1, ty, "string") expect(1, ty, "string")
expect(2, filter, "function", "nil") expect(2, filter, "function", "nil")

View File

@ -381,6 +381,7 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle)
local sArrayResult = "[" local sArrayResult = "["
local nObjectSize = 0 local nObjectSize = 0
local nArraySize = 0 local nArraySize = 0
local largestArrayIndex = 0
for k, v in pairs(t) do for k, v in pairs(t) do
if type(k) == "string" then if type(k) == "string" then
local sEntry local sEntry
@ -395,10 +396,17 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle)
sObjectResult = sObjectResult .. "," .. sEntry sObjectResult = sObjectResult .. "," .. sEntry
end end
nObjectSize = nObjectSize + 1 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
end end
for _, v in ipairs(t) do 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 = serializeJSONImpl(v, tTracking, bNBTStyle) 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 if nArraySize == 0 then
sArrayResult = sArrayResult .. sEntry sArrayResult = sArrayResult .. sEntry
else else

View File

@ -10,6 +10,7 @@ end
-- --
-- Generally you should not need to use this table - it only exists for -- Generally you should not need to use this table - it only exists for
-- backwards compatibility reasons. -- backwards compatibility reasons.
-- @deprecated
native = turtle.native or turtle native = turtle.native or turtle
local function addCraftMethod(object) local function addCraftMethod(object)

View File

@ -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 # New features in CC: Restitched 1.95.0
* Optimise the paint program's initial render. * Optimise the paint program's initial render.

View File

@ -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 bug fixes:
* Several documentation improvments (Gibbo3771, MCJack123). * Correctly serialise sparse arrays into JSON (livegamer999)
* `fs.combine` now accepts multiple arguments. * Fix rs.getBundledInput returning the output instead (SkyTheCodeMaster)
* Add a setting (`bios.strict_globals`) to error when accidentally declaring a global. (Lupus590). * Programs run via edit are now a little better behaved (Wojbie)
* Add an improved help viewer which allows scrolling up and down (MCJack123). * Add User-Agent to a websocket's headers.
* 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.
Type "help changelog" to see the full version history. Type "help changelog" to see the full version history.

View File

@ -1,23 +1,28 @@
--- Provides a "pretty printer", for rendering data structures in an --[[- Provides a "pretty printer", for rendering data structures in an
-- aesthetically pleasing manner. aesthetically pleasing manner.
--
-- In order to display something using @{cc.pretty}, you build up a series of 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 @{Doc|documents}. These behave a little bit like strings; you can concatenate
-- them together and then print them to the screen. them together and then print them to the screen.
--
-- However, documents also allow you to control how they should be printed. There 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 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' the "layout" of the document. When you come to display the document, the 'best'
-- (most compact) layout is used. (most compact) layout is used.
--
-- @module cc.pretty The structure of this module is based on [A Prettier Printer][prettier].
-- @usage Print a table to the terminal
-- local pretty = require "cc.pretty" [prettier]: https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf "A Prettier Printer"
-- pretty.print(pretty.pretty({ 1, 2, 3 }))
-- @module cc.pretty
-- @usage Build a custom document and display it @usage Print a table to the terminal
-- local pretty = require "cc.pretty" local pretty = require "cc.pretty"
-- pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) 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 = require "cc.expect"
local expect, field = expect.expect, expect.field local expect, field = expect.expect, expect.field

View File

@ -5,17 +5,24 @@
local expect = require "cc.expect".expect local expect = require "cc.expect".expect
--- Wraps a block of text, so that each line fits within the given width. --[[- 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 This may be useful if you want to wrap text before displaying it to a
-- @{monitor} or @{printer} without using @{_G.print|print}. @{monitor} or @{printer} without using @{_G.print|print}.
--
-- @tparam string text The string to wrap. @tparam string text The string to wrap.
-- @tparam[opt] number width The width to constrain to, defaults to the width of @tparam[opt] number width The width to constrain to, defaults to the width of
-- the terminal. the terminal.
-- @treturn { string... } The wrapped input string as a list of lines.
-- @treturn { string... } The wrapped input string. @usage Wrap a string and write it to the terminal.
-- @usage require "cc.strings".wrap("This is a long piece of text", 10)
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) local function wrap(text, width)
expect(1, text, "string") expect(1, text, "string")
expect(2, width, "number", "nil") expect(2, width, "number", "nil")

View File

@ -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 View the documentation at https://tweaked.cc
Show off your programs or ask for help at our forum: https://forums.computercraft.cc Show off your programs or ask for help at our forum: https://forums.computercraft.cc
You can disable these messages by running "set motd.enable false". You can disable these messages by running "set motd.enable false".

View File

@ -47,6 +47,30 @@ else
stringColour = colours.white stringColour = colours.white
end 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 -- Menus
local bMenu = false local bMenu = false
local nMenuItem = 1 local nMenuItem = 1
@ -89,7 +113,7 @@ local function load(_sPath)
end end
end end
local function save(_sPath) local function save(_sPath, fWrite)
-- Create intervening folder -- Create intervening folder
local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len()) local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len())
if not fs.exists(sDir) then if not fs.exists(sDir) then
@ -101,8 +125,8 @@ local function save(_sPath)
local function innerSave() local function innerSave()
file, fileerr = fs.open(_sPath, "w") file, fileerr = fs.open(_sPath, "w")
if file then if file then
for _, sLine in ipairs(tLines) do if file then
file.write(sLine .. "\n") fWrite(file)
end end
else else
error("Failed to open " .. _sPath) error("Failed to open " .. _sPath)
@ -293,7 +317,11 @@ local tMenuFuncs = {
if bReadOnly then if bReadOnly then
sStatus = "Access denied" sStatus = "Access denied"
else 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 if ok then
sStatus = "Saved to " .. sPath sStatus = "Saved to " .. sPath
else else
@ -390,8 +418,18 @@ local tMenuFuncs = {
bRunning = false bRunning = false
end, end,
Run = function() Run = function()
local sTempPath = "/.temp" local sTitle = fs.getName(sPath)
local ok = save(sTempPath) 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 if ok then
local nTask = shell.openTab(sTempPath) local nTask = shell.openTab(sTempPath)
if nTask then if nTask then
@ -667,8 +705,8 @@ while bRunning do
end end
end end
elseif param == keys.enter then elseif param == keys.enter or param == keys.numPadEnter then
-- Enter -- Enter/Numpad Enter
if not bMenu and not bReadOnly then if not bMenu and not bReadOnly then
-- Newline -- Newline
local sLine = tLines[y] local sLine = tLines[y]
@ -687,7 +725,7 @@ while bRunning do
end 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 -- Menu toggle
bMenu = not bMenu bMenu = not bMenu
if bMenu then if bMenu then
@ -696,7 +734,12 @@ while bRunning do
term.setCursorBlink(true) term.setCursorBlink(true)
end end
redrawMenu() redrawMenu()
elseif param == keys.rightAlt then
if bMenu then
bMenu = false
term.setCursorBlink(true)
redrawMenu()
end
end end
elseif sEvent == "char" then elseif sEvent == "char" then
@ -758,6 +801,7 @@ while bRunning do
end end
else else
bMenu = false bMenu = false
term.setCursorBlink(true)
redrawMenu() redrawMenu()
end end
end end

View File

@ -349,7 +349,7 @@ local function accessMenu()
selection = #mChoices selection = #mChoices
end end
elseif key == keys.enter then elseif key == keys.enter or key == keys.numPadEnter then
-- Select an option -- Select an option
return menu_choices[mChoices[selection]]() return menu_choices[mChoices[selection]]()
elseif key == keys.leftCtrl or keys == keys.rightCtrl then elseif key == keys.leftCtrl or keys == keys.rightCtrl then

View File

@ -199,8 +199,8 @@ while true do
drawMenu() drawMenu()
drawFrontend() drawFrontend()
end end
elseif key == keys.enter then elseif key == keys.enter or key == keys.numPadEnter then
-- Enter -- Enter/Numpad Enter
break break
end end
end end

View File

@ -13,16 +13,30 @@ if sDrive == nil then
end end
else else
local bData = disk.hasData(sDrive) if disk.hasAudio(sDrive) then
if not bData 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) print("No disk in drive " .. sDrive)
return return
end 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) local label = disk.getLabel(sDrive)
if label then if label then
print("The disk is labelled \"" .. label .. "\"") print("Labelled \"" .. label .. "\"")
end end
end end

View File

@ -546,7 +546,7 @@ local function playGame()
msgBox("Game Over!") msgBox("Game Over!")
while true do while true do
local _, k = os.pullEvent("key") 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 break
end end
end end
@ -627,7 +627,7 @@ local function runMenu()
elseif key == keys.down or key == keys.s then elseif key == keys.down or key == keys.s then
selected = selected % 2 + 1 selected = selected % 2 + 1
drawMenu() 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! break --begin play!
end end
end end

View File

@ -14,6 +14,6 @@
], ],
"result": { "result": {
"item": "computercraft:disk", "item": "computercraft:disk",
"nbt": "{color:1118481}" "nbt": "{Color:1118481}"
} }
} }

View File

@ -14,6 +14,6 @@
], ],
"result": { "result": {
"item": "computercraft:disk", "item": "computercraft:disk",
"nbt": "{color:15905484}" "nbt": "{Color:15905484}"
} }
} }

View File

@ -14,6 +14,6 @@
], ],
"result": { "result": {
"item": "computercraft:disk", "item": "computercraft:disk",
"nbt": "{color:8375321}" "nbt": "{Color:8375321}"
} }
} }

View File

@ -14,6 +14,6 @@
], ],
"result": { "result": {
"item": "computercraft:disk", "item": "computercraft:disk",
"nbt": "{color:14605932}" "nbt": "{Color:14605932}"
} }
} }

View File

@ -14,6 +14,6 @@
], ],
"result": { "result": {
"item": "computercraft:disk", "item": "computercraft:disk",
"nbt": "{color:10072818}" "nbt": "{Color:10072818}"
} }
} }

View File

@ -14,6 +14,6 @@
], ],
"result": { "result": {
"item": "computercraft:disk", "item": "computercraft:disk",
"nbt": "{color:15040472}" "nbt": "{Color:15040472}"
} }
} }

View File

@ -14,6 +14,6 @@
], ],
"result": { "result": {
"item": "computercraft:disk", "item": "computercraft:disk",
"nbt": "{color:15905331}" "nbt": "{Color:15905331}"
} }
} }

View File

@ -14,6 +14,6 @@
], ],
"result": { "result": {
"item": "computercraft:disk", "item": "computercraft:disk",
"nbt": "{color:15790320}" "nbt": "{Color:15790320}"
} }
} }

Some files were not shown because too many files have changed in this diff Show More