mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-08-26 23:42: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:
commit
b0782ec38b
2
.github/workflows/main-ci.yml
vendored
2
.github/workflows/main-ci.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
java-version: 8
|
||||
|
||||
- name: Cache gradle dependencies
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('gradle.properties') }}
|
||||
|
@ -55,8 +55,8 @@ dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
|
||||
|
||||
modRuntime "me.shedaniel:RoughlyEnoughItems-api:5.2.10"
|
||||
modRuntime "me.shedaniel:RoughlyEnoughItems:5.2.10"
|
||||
modRuntime "me.shedaniel:RoughlyEnoughItems-api:5.8.9"
|
||||
modRuntime "me.shedaniel:RoughlyEnoughItems:5.8.9"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
21
doc/events/alarm.md
Normal file
21
doc/events/alarm.md
Normal 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
24
doc/events/char.md
Normal 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
|
||||
```
|
18
doc/events/computer_command.md
Normal file
18
doc/events/computer_command.md
Normal 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
19
doc/events/disk.md
Normal 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
19
doc/events/disk_eject.md
Normal 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
14
doc/events/http_check.md
Normal 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.
|
39
doc/events/http_failure.md
Normal file
39
doc/events/http_failure.md
Normal 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()
|
||||
```
|
27
doc/events/http_success.md
Normal file
27
doc/events/http_success.md
Normal 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
26
doc/events/key.md
Normal 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
24
doc/events/key_up.md
Normal 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
|
||||
```
|
22
doc/events/modem_message.md
Normal file
22
doc/events/modem_message.md
Normal 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
|
||||
```
|
18
doc/events/monitor_resize.md
Normal file
18
doc/events/monitor_resize.md
Normal 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
|
||||
```
|
20
doc/events/monitor_touch.md
Normal file
20
doc/events/monitor_touch.md
Normal 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
34
doc/events/mouse_click.md
Normal 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
22
doc/events/mouse_drag.md
Normal 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
|
||||
```
|
21
doc/events/mouse_scroll.md
Normal file
21
doc/events/mouse_scroll.md
Normal 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
21
doc/events/mouse_up.md
Normal 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
18
doc/events/paste.md
Normal 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
19
doc/events/peripheral.md
Normal 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
|
||||
```
|
19
doc/events/peripheral_detach.md
Normal file
19
doc/events/peripheral_detach.md
Normal 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
|
||||
```
|
30
doc/events/rednet_message.md
Normal file
30
doc/events/rednet_message.md
Normal 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
14
doc/events/redstone.md
Normal 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
|
||||
```
|
28
doc/events/task_complete.md
Normal file
28
doc/events/task_complete.md
Normal 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
15
doc/events/term_resize.md
Normal 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
25
doc/events/terminate.md
Normal 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
21
doc/events/timer.md
Normal 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")
|
||||
```
|
14
doc/events/turtle_inventory.md
Normal file
14
doc/events/turtle_inventory.md
Normal 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
|
||||
```
|
21
doc/events/websocket_closed.md
Normal file
21
doc/events/websocket_closed.md
Normal 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.")
|
||||
```
|
25
doc/events/websocket_failure.md
Normal file
25
doc/events/websocket_failure.md
Normal 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)
|
||||
```
|
26
doc/events/websocket_message.md
Normal file
26
doc/events/websocket_message.md
Normal 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()
|
||||
```
|
28
doc/events/websocket_success.md
Normal file
28
doc/events/websocket_success.md
Normal 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()
|
||||
```
|
@ -2,16 +2,16 @@
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
|
||||
# Mod properties
|
||||
mod_version=1.95.0-beta
|
||||
mod_version=1.95.3-beta
|
||||
|
||||
# Minecraft properties
|
||||
mc_version=1.16.2
|
||||
mappings_version=31
|
||||
mc_version=1.16.4
|
||||
mappings_version=9
|
||||
|
||||
# Dependencies
|
||||
cloth_config_version=4.8.1
|
||||
fabric_api_version=0.19.0+build.398-1.16
|
||||
fabric_loader_version=0.9.2+build.206
|
||||
fabric_api_version=0.34.2+1.16
|
||||
fabric_loader_version=0.11.3
|
||||
jankson_version=1.2.0
|
||||
modmenu_version=1.14.6+
|
||||
cloth_api_version=1.4.5
|
||||
|
90
patchwork.md
90
patchwork.md
@ -536,4 +536,92 @@ e4b0a5b3ce035eb23feb4191432fc49af5772c5b
|
||||
|
||||
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
|
||||
```
|
||||
|
@ -108,7 +108,10 @@ public final class ComputerCraftAPI {
|
||||
* Use in conjunction with {@link IComputerAccess#mount} or {@link IComputerAccess#mountWritable} to mount a resource folder onto a computer's file
|
||||
* system.
|
||||
*
|
||||
* The files in this mount will be a combination of files in all mod jar, and data packs that contain resources with the same domain and path.
|
||||
* The files in this mount will be a combination of files in all mod jar, and data packs that contain
|
||||
* resources with the same domain and path. For instance, ComputerCraft's resources are stored in
|
||||
* "/data/computercraft/lua/rom". We construct a mount for that with
|
||||
* {@code createResourceMount("computercraft", "lua/rom")}.
|
||||
*
|
||||
* @param domain The domain under which to look for resources. eg: "mymod".
|
||||
* @param subPath The subPath under which to look for resources. eg: "lua/myfiles".
|
||||
|
@ -1,3 +1,8 @@
|
||||
/*
|
||||
* This file is part of the public ComputerCraft API - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only.
|
||||
* For help using the API, and posting your mods, visit the forums at computercraft.info.
|
||||
*/
|
||||
package dan200.computercraft.api;
|
||||
|
||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
|
@ -48,7 +48,7 @@ import net.minecraft.util.Hand;
|
||||
import net.minecraft.util.collection.DefaultedList;
|
||||
import net.minecraft.util.math.ChunkPos;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.village.TraderOfferList;
|
||||
import net.minecraft.village.TradeOfferList;
|
||||
import net.minecraft.world.GameMode;
|
||||
|
||||
/**
|
||||
@ -105,7 +105,7 @@ public class FakePlayer extends ServerPlayerEntity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendTradeOffers(int id, TraderOfferList list, int level, int experience, boolean levelled, boolean refreshable) { }
|
||||
public void sendTradeOffers(int id, TradeOfferList list, int level, int experience, boolean levelled, boolean refreshable) { }
|
||||
|
||||
@Override
|
||||
public void openHorseInventory(HorseBaseEntity horse, Inventory inventory) { }
|
||||
@ -251,10 +251,6 @@ public class FakePlayer extends ServerPlayerEntity {
|
||||
public void disconnect(Text message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupEncryption(SecretKey key) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableAutoRead() {
|
||||
}
|
||||
|
@ -396,7 +396,8 @@ public class FSAPI implements ILuaAPI {
|
||||
/**
|
||||
* Get attributes about a specific file or folder.
|
||||
*
|
||||
* The returned attributes table contains information about the size of the file, whether it is a directory, and when it was created and last modified.
|
||||
* The returned attributes table contains information about the size of the file, whether it is a directory,
|
||||
* when it was created and last modified, and whether it is read only.
|
||||
*
|
||||
* The creation and modification times are given as the number of milliseconds since the UNIX epoch. This may be given to {@link OSAPI#date} in order to
|
||||
* convert it to more usable form.
|
||||
@ -404,7 +405,7 @@ public class FSAPI implements ILuaAPI {
|
||||
* @param path The path to get attributes for.
|
||||
* @return The resulting attributes.
|
||||
* @throws LuaException If the path does not exist.
|
||||
* @cc.treturn { size = number, isDir = boolean, created = number, modified = number } The resulting attributes.
|
||||
* @cc.treturn { size = number, isDir = boolean, isReadOnly = boolean, created = number, modified = number } The resulting attributes.
|
||||
* @see #getSize If you only care about the file's size.
|
||||
* @see #isDir If you only care whether a path is a directory or not.
|
||||
*/
|
||||
@ -413,11 +414,12 @@ public class FSAPI implements ILuaAPI {
|
||||
try {
|
||||
BasicFileAttributes attributes = this.fileSystem.getAttributes(path);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("modification", getFileTime(attributes.lastModifiedTime()));
|
||||
result.put("modified", getFileTime(attributes.lastModifiedTime()));
|
||||
result.put("created", getFileTime(attributes.creationTime()));
|
||||
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
|
||||
result.put("isDir", attributes.isDirectory());
|
||||
result.put( "modification", getFileTime( attributes.lastModifiedTime() ) );
|
||||
result.put( "modified", getFileTime( attributes.lastModifiedTime() ) );
|
||||
result.put( "created", getFileTime( attributes.creationTime() ) );
|
||||
result.put( "size", attributes.isDirectory() ? 0 : attributes.size() );
|
||||
result.put( "isDir", attributes.isDirectory() );
|
||||
result.put( "isReadOnly", fileSystem.isReadOnly( path ) );
|
||||
return result;
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
|
@ -3,185 +3,206 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import static dan200.computercraft.core.apis.TableHelper.getStringField;
|
||||
import static dan200.computercraft.core.apis.TableHelper.optBooleanField;
|
||||
import static dan200.computercraft.core.apis.TableHelper.optStringField;
|
||||
import static dan200.computercraft.core.apis.TableHelper.optTableField;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.http.CheckUrl;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||
import dan200.computercraft.core.apis.http.ResourceQueue;
|
||||
import dan200.computercraft.core.apis.http.*;
|
||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
|
||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static dan200.computercraft.core.apis.TableHelper.*;
|
||||
|
||||
/**
|
||||
* The http library allows communicating with web servers, sending and receiving data from them.
|
||||
*
|
||||
* @cc.module http
|
||||
* @hidden
|
||||
*/
|
||||
public class HTTPAPI implements ILuaAPI {
|
||||
private final IAPIEnvironment m_apiEnvironment;
|
||||
public class HTTPAPI implements ILuaAPI
|
||||
{
|
||||
private final IAPIEnvironment apiEnvironment;
|
||||
|
||||
private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>();
|
||||
private final ResourceGroup<HttpRequest> requests = new ResourceQueue<>(() -> ComputerCraft.httpMaxRequests);
|
||||
private final ResourceGroup<Websocket> websockets = new ResourceGroup<>(() -> ComputerCraft.httpMaxWebsockets);
|
||||
private final ResourceGroup<HttpRequest> requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests );
|
||||
private final ResourceGroup<Websocket> websockets = new ResourceGroup<>( () -> ComputerCraft.httpMaxWebsockets );
|
||||
|
||||
public HTTPAPI(IAPIEnvironment environment) {
|
||||
this.m_apiEnvironment = environment;
|
||||
public HTTPAPI( IAPIEnvironment environment )
|
||||
{
|
||||
apiEnvironment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[] {"http"};
|
||||
public String[] getNames()
|
||||
{
|
||||
return new String[] { "http" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startup() {
|
||||
this.checkUrls.startup();
|
||||
this.requests.startup();
|
||||
this.websockets.startup();
|
||||
public void startup()
|
||||
{
|
||||
checkUrls.startup();
|
||||
requests.startup();
|
||||
websockets.startup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
public void shutdown()
|
||||
{
|
||||
checkUrls.shutdown();
|
||||
requests.shutdown();
|
||||
websockets.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update()
|
||||
{
|
||||
// It's rather ugly to run this here, but we need to clean up
|
||||
// resources as often as possible to reduce blocking.
|
||||
Resource.cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
this.checkUrls.shutdown();
|
||||
this.requests.shutdown();
|
||||
this.websockets.shutdown();
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] request(IArguments args) throws LuaException {
|
||||
public final Object[] request( IArguments args ) throws LuaException
|
||||
{
|
||||
String address, postString, requestMethod;
|
||||
Map<?, ?> headerTable;
|
||||
boolean binary, redirect;
|
||||
|
||||
if (args.get(0) instanceof Map) {
|
||||
Map<?, ?> options = args.getTable(0);
|
||||
address = getStringField(options, "url");
|
||||
postString = optStringField(options, "body", null);
|
||||
headerTable = optTableField(options, "headers", Collections.emptyMap());
|
||||
binary = optBooleanField(options, "binary", false);
|
||||
requestMethod = optStringField(options, "method", null);
|
||||
redirect = optBooleanField(options, "redirect", true);
|
||||
if( args.get( 0 ) instanceof Map )
|
||||
{
|
||||
Map<?, ?> options = args.getTable( 0 );
|
||||
address = getStringField( options, "url" );
|
||||
postString = optStringField( options, "body", null );
|
||||
headerTable = optTableField( options, "headers", Collections.emptyMap() );
|
||||
binary = optBooleanField( options, "binary", false );
|
||||
requestMethod = optStringField( options, "method", null );
|
||||
redirect = optBooleanField( options, "redirect", true );
|
||||
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get URL and post information
|
||||
address = args.getString(0);
|
||||
postString = args.optString(1, null);
|
||||
headerTable = args.optTable(2, Collections.emptyMap());
|
||||
binary = args.optBoolean(3, false);
|
||||
address = args.getString( 0 );
|
||||
postString = args.optString( 1, null );
|
||||
headerTable = args.optTable( 2, Collections.emptyMap() );
|
||||
binary = args.optBoolean( 3, false );
|
||||
requestMethod = null;
|
||||
redirect = true;
|
||||
}
|
||||
|
||||
HttpHeaders headers = getHeaders(headerTable);
|
||||
HttpHeaders headers = getHeaders( headerTable );
|
||||
|
||||
HttpMethod httpMethod;
|
||||
if (requestMethod == null) {
|
||||
if( requestMethod == null )
|
||||
{
|
||||
httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST;
|
||||
} else {
|
||||
httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT));
|
||||
if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) {
|
||||
throw new LuaException("Unsupported HTTP method");
|
||||
}
|
||||
else
|
||||
{
|
||||
httpMethod = HttpMethod.valueOf( requestMethod.toUpperCase( Locale.ROOT ) );
|
||||
if( httpMethod == null || requestMethod.equalsIgnoreCase( "CONNECT" ) )
|
||||
{
|
||||
throw new LuaException( "Unsupported HTTP method" );
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
URI uri = HttpRequest.checkUri(address);
|
||||
HttpRequest request = new HttpRequest(this.requests, this.m_apiEnvironment, address, postString, headers, binary, redirect);
|
||||
try
|
||||
{
|
||||
URI uri = HttpRequest.checkUri( address );
|
||||
HttpRequest request = new HttpRequest( requests, apiEnvironment, address, postString, headers, binary, redirect );
|
||||
|
||||
// Make the request
|
||||
request.queue(r -> r.request(uri, httpMethod));
|
||||
request.queue( r -> r.request( uri, httpMethod ) );
|
||||
|
||||
return new Object[] {true};
|
||||
} catch (HTTPRequestException e) {
|
||||
return new Object[] {
|
||||
false,
|
||||
e.getMessage()
|
||||
};
|
||||
return new Object[] { true };
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
return new Object[] { false, e.getMessage() };
|
||||
}
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] checkURL( String address )
|
||||
{
|
||||
try
|
||||
{
|
||||
URI uri = HttpRequest.checkUri( address );
|
||||
new CheckUrl( checkUrls, apiEnvironment, address, uri ).queue( CheckUrl::run );
|
||||
|
||||
return new Object[] { true };
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
return new Object[] { false, e.getMessage() };
|
||||
}
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] websocket( String address, Optional<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
|
||||
private static HttpHeaders getHeaders(@Nonnull Map<?, ?> headerTable) throws LuaException {
|
||||
private HttpHeaders getHeaders( @Nonnull Map<?, ?> headerTable ) throws LuaException
|
||||
{
|
||||
HttpHeaders headers = new DefaultHttpHeaders();
|
||||
for (Map.Entry<?, ?> entry : headerTable.entrySet()) {
|
||||
for( Map.Entry<?, ?> entry : headerTable.entrySet() )
|
||||
{
|
||||
Object value = entry.getValue();
|
||||
if (entry.getKey() instanceof String && value instanceof String) {
|
||||
try {
|
||||
headers.add((String) entry.getKey(), value);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
if( entry.getKey() instanceof String && value instanceof String )
|
||||
{
|
||||
try
|
||||
{
|
||||
headers.add( (String) entry.getKey(), value );
|
||||
}
|
||||
catch( IllegalArgumentException e )
|
||||
{
|
||||
throw new LuaException( e.getMessage() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( !headers.contains( HttpHeaderNames.USER_AGENT ) )
|
||||
{
|
||||
headers.set( HttpHeaderNames.USER_AGENT, apiEnvironment.getComputerEnvironment().getUserAgent() );
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] checkURL(String address) {
|
||||
try {
|
||||
URI uri = HttpRequest.checkUri(address);
|
||||
new CheckUrl(this.checkUrls, this.m_apiEnvironment, address, uri).queue(CheckUrl::run);
|
||||
|
||||
return new Object[] {true};
|
||||
} catch (HTTPRequestException e) {
|
||||
return new Object[] {
|
||||
false,
|
||||
e.getMessage()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] websocket(String address, Optional<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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -185,8 +185,9 @@ public class RedstoneAPI implements ILuaAPI {
|
||||
* @see #testBundledInput To determine if a specific colour is set.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final int getBundledInput(ComputerSide side) {
|
||||
return this.environment.getBundledOutput(side);
|
||||
public final int getBundledInput( ComputerSide side )
|
||||
{
|
||||
return environment.getBundledInput( side );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,6 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@ -15,83 +14,81 @@ import java.util.Objects;
|
||||
/**
|
||||
* A seekable, readable byte channel which is backed by a simple byte array.
|
||||
*/
|
||||
public class ArrayByteChannel implements SeekableByteChannel {
|
||||
private final byte[] backing;
|
||||
public class ArrayByteChannel implements SeekableByteChannel
|
||||
{
|
||||
private boolean closed = false;
|
||||
private int position = 0;
|
||||
|
||||
public ArrayByteChannel(byte[] backing) {
|
||||
private final byte[] backing;
|
||||
|
||||
public ArrayByteChannel( byte[] backing )
|
||||
{
|
||||
this.backing = backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer destination) throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
Objects.requireNonNull(destination, "destination");
|
||||
public int read( ByteBuffer destination ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
Objects.requireNonNull( destination, "destination" );
|
||||
|
||||
if (this.position >= this.backing.length) {
|
||||
return -1;
|
||||
}
|
||||
if( position >= backing.length ) return -1;
|
||||
|
||||
int remaining = Math.min(this.backing.length - this.position, destination.remaining());
|
||||
destination.put(this.backing, this.position, remaining);
|
||||
this.position += remaining;
|
||||
int remaining = Math.min( backing.length - position, destination.remaining() );
|
||||
destination.put( backing, position, remaining );
|
||||
position += remaining;
|
||||
return remaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src) throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
public int write( ByteBuffer src ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
return this.position;
|
||||
public long position() throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
public SeekableByteChannel position( long newPosition ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
if( newPosition < 0 || newPosition > Integer.MAX_VALUE )
|
||||
{
|
||||
throw new IllegalArgumentException( "Position out of bounds" );
|
||||
}
|
||||
if (newPosition < 0 || newPosition > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Position out of bounds");
|
||||
}
|
||||
this.position = (int) newPosition;
|
||||
position = (int) newPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
return this.backing.length;
|
||||
public long size() throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
return backing.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) throws ClosedChannelException {
|
||||
if (this.closed) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
public SeekableByteChannel truncate( long size ) throws ClosedChannelException
|
||||
{
|
||||
if( closed ) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return !this.closed;
|
||||
public boolean isOpen()
|
||||
{
|
||||
return !closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.closed = true;
|
||||
public void close()
|
||||
{
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,13 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
@ -16,40 +18,43 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
|
||||
/**
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"} mode.
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"}
|
||||
* mode.
|
||||
*
|
||||
* @cc.module fs.BinaryReadHandle
|
||||
*/
|
||||
public class BinaryReadableHandle extends HandleGeneric {
|
||||
public class BinaryReadableHandle extends HandleGeneric
|
||||
{
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
final SeekableByteChannel seekable;
|
||||
private final ReadableByteChannel reader;
|
||||
private final ByteBuffer single = ByteBuffer.allocate(1);
|
||||
|
||||
BinaryReadableHandle(ReadableByteChannel reader, SeekableByteChannel seekable, Closeable closeable) {
|
||||
super(closeable);
|
||||
private final ReadableByteChannel reader;
|
||||
final SeekableByteChannel seekable;
|
||||
private final ByteBuffer single = ByteBuffer.allocate( 1 );
|
||||
|
||||
BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable )
|
||||
{
|
||||
super( closeable );
|
||||
this.reader = reader;
|
||||
this.seekable = seekable;
|
||||
}
|
||||
|
||||
public static BinaryReadableHandle of(ReadableByteChannel channel) {
|
||||
return of(channel, channel);
|
||||
public static BinaryReadableHandle of( ReadableByteChannel channel, TrackingCloseable closeable )
|
||||
{
|
||||
SeekableByteChannel seekable = asSeekable( channel );
|
||||
return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
|
||||
}
|
||||
|
||||
public static BinaryReadableHandle of(ReadableByteChannel channel, Closeable closeable) {
|
||||
SeekableByteChannel seekable = asSeekable(channel);
|
||||
return seekable == null ? new BinaryReadableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
|
||||
public static BinaryReadableHandle of( ReadableByteChannel channel )
|
||||
{
|
||||
return of( channel, new TrackingCloseable.Impl( channel ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a number of bytes from this file.
|
||||
*
|
||||
* @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This may be 0 to determine we are at
|
||||
* the end of the file.
|
||||
* @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This
|
||||
* may be 0 to determine we are at the end of the file.
|
||||
* @return The read bytes.
|
||||
* @throws LuaException When trying to read a negative number of bytes.
|
||||
* @throws LuaException If the file has been closed.
|
||||
@ -58,72 +63,78 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] read(Optional<Integer> countArg) throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
if (countArg.isPresent()) {
|
||||
public final Object[] read( Optional<Integer> countArg ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
if( countArg.isPresent() )
|
||||
{
|
||||
int count = countArg.get();
|
||||
if (count < 0) {
|
||||
throw new LuaException("Cannot read a negative number of bytes");
|
||||
}
|
||||
if (count == 0 && this.seekable != null) {
|
||||
return this.seekable.position() >= this.seekable.size() ? null : new Object[] {""};
|
||||
if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" );
|
||||
if( count == 0 && seekable != null )
|
||||
{
|
||||
return seekable.position() >= seekable.size() ? null : new Object[] { "" };
|
||||
}
|
||||
|
||||
if (count <= BUFFER_SIZE) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(count);
|
||||
if( count <= BUFFER_SIZE )
|
||||
{
|
||||
ByteBuffer buffer = ByteBuffer.allocate( count );
|
||||
|
||||
int read = this.reader.read(buffer);
|
||||
if (read < 0) {
|
||||
return null;
|
||||
}
|
||||
int read = reader.read( buffer );
|
||||
if( read < 0 ) return null;
|
||||
buffer.flip();
|
||||
return new Object[] {buffer};
|
||||
} else {
|
||||
return new Object[] { buffer };
|
||||
}
|
||||
else
|
||||
{
|
||||
// Read the initial set of characters, failing if none are read.
|
||||
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||
int read = this.reader.read(buffer);
|
||||
if (read < 0) {
|
||||
return null;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE );
|
||||
int read = reader.read( buffer );
|
||||
if( read < 0 ) return null;
|
||||
|
||||
// If we failed to read "enough" here, let's just abort
|
||||
if (read >= count || read < BUFFER_SIZE) {
|
||||
if( read >= count || read < BUFFER_SIZE )
|
||||
{
|
||||
buffer.flip();
|
||||
return new Object[] {buffer};
|
||||
return new Object[] { buffer };
|
||||
}
|
||||
|
||||
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
||||
// than doubling up the buffer each time.
|
||||
int totalRead = read;
|
||||
List<ByteBuffer> parts = new ArrayList<>(4);
|
||||
parts.add(buffer);
|
||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
||||
buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead));
|
||||
read = this.reader.read(buffer);
|
||||
if (read < 0) {
|
||||
break;
|
||||
}
|
||||
List<ByteBuffer> parts = new ArrayList<>( 4 );
|
||||
parts.add( buffer );
|
||||
while( read >= BUFFER_SIZE && totalRead < count )
|
||||
{
|
||||
buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) );
|
||||
read = reader.read( buffer );
|
||||
if( read < 0 ) break;
|
||||
|
||||
totalRead += read;
|
||||
parts.add(buffer);
|
||||
parts.add( buffer );
|
||||
}
|
||||
|
||||
// Now just copy all the bytes across!
|
||||
byte[] bytes = new byte[totalRead];
|
||||
int pos = 0;
|
||||
for (ByteBuffer part : parts) {
|
||||
System.arraycopy(part.array(), 0, bytes, pos, part.position());
|
||||
for( ByteBuffer part : parts )
|
||||
{
|
||||
System.arraycopy( part.array(), 0, bytes, pos, part.position() );
|
||||
pos += part.position();
|
||||
}
|
||||
return new Object[] {bytes};
|
||||
return new Object[] { bytes };
|
||||
}
|
||||
} else {
|
||||
this.single.clear();
|
||||
int b = this.reader.read(this.single);
|
||||
return b == -1 ? null : new Object[] {this.single.get(0) & 0xFF};
|
||||
}
|
||||
} catch (IOException e) {
|
||||
else
|
||||
{
|
||||
single.clear();
|
||||
int b = reader.read( single );
|
||||
return b == -1 ? null : new Object[] { single.get( 0 ) & 0xFF };
|
||||
}
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -136,29 +147,30 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readAll() throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
public final Object[] readAll() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
int expected = 32;
|
||||
if (this.seekable != null) {
|
||||
expected = Math.max(expected, (int) (this.seekable.size() - this.seekable.position()));
|
||||
}
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream(expected);
|
||||
if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) );
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream( expected );
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(8192);
|
||||
ByteBuffer buf = ByteBuffer.allocate( 8192 );
|
||||
boolean readAnything = false;
|
||||
while (true) {
|
||||
while( true )
|
||||
{
|
||||
buf.clear();
|
||||
int r = this.reader.read(buf);
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
int r = reader.read( buf );
|
||||
if( r == -1 ) break;
|
||||
|
||||
readAnything = true;
|
||||
stream.write(buf.array(), 0, r);
|
||||
stream.write( buf.array(), 0, r );
|
||||
}
|
||||
return readAnything ? new Object[] {stream.toByteArray()} : null;
|
||||
} catch (IOException e) {
|
||||
return readAnything ? new Object[] { stream.toByteArray() } : null;
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -172,65 +184,70 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||
this.checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse(false);
|
||||
try {
|
||||
public final Object[] readLine( Optional<Boolean> withTrailingArg ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse( false );
|
||||
try
|
||||
{
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
|
||||
boolean readAnything = false, readRc = false;
|
||||
while (true) {
|
||||
this.single.clear();
|
||||
int read = this.reader.read(this.single);
|
||||
if (read <= 0) {
|
||||
while( true )
|
||||
{
|
||||
single.clear();
|
||||
int read = reader.read( single );
|
||||
if( read <= 0 )
|
||||
{
|
||||
// Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it
|
||||
// back.
|
||||
if (readRc) {
|
||||
stream.write('\r');
|
||||
}
|
||||
return readAnything ? new Object[] {stream.toByteArray()} : null;
|
||||
if( readRc ) stream.write( '\r' );
|
||||
return readAnything ? new Object[] { stream.toByteArray() } : null;
|
||||
}
|
||||
|
||||
readAnything = true;
|
||||
|
||||
byte chr = this.single.get(0);
|
||||
if (chr == '\n') {
|
||||
if (withTrailing) {
|
||||
if (readRc) {
|
||||
stream.write('\r');
|
||||
}
|
||||
stream.write(chr);
|
||||
byte chr = single.get( 0 );
|
||||
if( chr == '\n' )
|
||||
{
|
||||
if( withTrailing )
|
||||
{
|
||||
if( readRc ) stream.write( '\r' );
|
||||
stream.write( chr );
|
||||
}
|
||||
return new Object[] {stream.toByteArray()};
|
||||
} else {
|
||||
return new Object[] { stream.toByteArray() };
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want to skip \r\n, but obviously need to include cases where \r is not followed by \n.
|
||||
// Note, this behaviour is non-standard compliant (strictly speaking we should have no
|
||||
// special logic for \r), but we preserve compatibility with EncodedReadableHandle and
|
||||
// previous behaviour of the io library.
|
||||
if (readRc) {
|
||||
stream.write('\r');
|
||||
}
|
||||
if( readRc ) stream.write( '\r' );
|
||||
readRc = chr == '\r';
|
||||
if (!readRc) {
|
||||
stream.write(chr);
|
||||
}
|
||||
if( !readRc ) stream.write( chr );
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Seekable extends BinaryReadableHandle {
|
||||
Seekable(SeekableByteChannel seekable, Closeable closeable) {
|
||||
super(seekable, seekable, closeable);
|
||||
public static class Seekable extends BinaryReadableHandle
|
||||
{
|
||||
Seekable( SeekableByteChannel seekable, TrackingCloseable closeable )
|
||||
{
|
||||
super( seekable, seekable, closeable );
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset given by {@code offset}, relative to a
|
||||
* start position determined by {@code whence}:
|
||||
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
|
||||
* given by {@code offset}, relative to a start position determined by {@code whence}:
|
||||
*
|
||||
* - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default.
|
||||
* - {@code "set"}: {@code offset} is relative to the beginning of the file.
|
||||
* - {@code "cur"}: Relative to the current position. This is the default.
|
||||
* - {@code "end"}: Relative to the end of the file.
|
||||
*
|
||||
* In case of success, {@code seek} returns the new file position from the beginning of the file.
|
||||
@ -244,9 +261,10 @@ public class BinaryReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string The reason seeking failed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
|
||||
this.checkOpen();
|
||||
return handleSeek(this.seekable, whence, offset);
|
||||
public final Object[] seek( Optional<String> whence, Optional<Long> offset ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
return handleSeek( seekable, whence, offset );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,14 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.io.Closeable;
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
@ -14,34 +18,34 @@ import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.Optional;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
|
||||
/**
|
||||
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"} modes.
|
||||
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"}
|
||||
* modes.
|
||||
*
|
||||
* @cc.module fs.BinaryWriteHandle
|
||||
*/
|
||||
public class BinaryWritableHandle extends HandleGeneric {
|
||||
final SeekableByteChannel seekable;
|
||||
public class BinaryWritableHandle extends HandleGeneric
|
||||
{
|
||||
private final WritableByteChannel writer;
|
||||
private final ByteBuffer single = ByteBuffer.allocate(1);
|
||||
final SeekableByteChannel seekable;
|
||||
private final ByteBuffer single = ByteBuffer.allocate( 1 );
|
||||
|
||||
protected BinaryWritableHandle(WritableByteChannel writer, SeekableByteChannel seekable, Closeable closeable) {
|
||||
super(closeable);
|
||||
protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, TrackingCloseable closeable )
|
||||
{
|
||||
super( closeable );
|
||||
this.writer = writer;
|
||||
this.seekable = seekable;
|
||||
}
|
||||
|
||||
public static BinaryWritableHandle of(WritableByteChannel channel) {
|
||||
return of(channel, channel);
|
||||
public static BinaryWritableHandle of( WritableByteChannel channel, TrackingCloseable closeable )
|
||||
{
|
||||
SeekableByteChannel seekable = asSeekable( channel );
|
||||
return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable );
|
||||
}
|
||||
|
||||
public static BinaryWritableHandle of(WritableByteChannel channel, Closeable closeable) {
|
||||
SeekableByteChannel seekable = asSeekable(channel);
|
||||
return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
|
||||
public static BinaryWritableHandle of( WritableByteChannel channel )
|
||||
{
|
||||
return of( channel, new TrackingCloseable.Impl( channel ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,24 +57,33 @@ public class BinaryWritableHandle extends HandleGeneric {
|
||||
* @cc.tparam [2] string The string to write.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void write(IArguments arguments) throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
Object arg = arguments.get(0);
|
||||
if (arg instanceof Number) {
|
||||
public final void write( IArguments arguments ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
Object arg = arguments.get( 0 );
|
||||
if( arg instanceof Number )
|
||||
{
|
||||
int number = ((Number) arg).intValue();
|
||||
this.single.clear();
|
||||
this.single.put((byte) number);
|
||||
this.single.flip();
|
||||
single.clear();
|
||||
single.put( (byte) number );
|
||||
single.flip();
|
||||
|
||||
this.writer.write(this.single);
|
||||
} else if (arg instanceof String) {
|
||||
this.writer.write(arguments.getBytes(0));
|
||||
} else {
|
||||
throw LuaValues.badArgumentOf(0, "string or number", arg);
|
||||
writer.write( single );
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
else if( arg instanceof String )
|
||||
{
|
||||
writer.write( arguments.getBytes( 0 ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw LuaValues.badArgumentOf( 0, "string or number", arg );
|
||||
}
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new LuaException( e.getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,27 +93,32 @@ public class BinaryWritableHandle extends HandleGeneric {
|
||||
* @throws LuaException If the file has been closed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void flush() throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
public final void flush() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
// Technically this is not needed
|
||||
if (this.writer instanceof FileChannel) {
|
||||
((FileChannel) this.writer).force(false);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
if( writer instanceof FileChannel ) ((FileChannel) writer).force( false );
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class Seekable extends BinaryWritableHandle {
|
||||
public Seekable(SeekableByteChannel seekable, Closeable closeable) {
|
||||
super(seekable, seekable, closeable);
|
||||
public static class Seekable extends BinaryWritableHandle
|
||||
{
|
||||
public Seekable( SeekableByteChannel seekable, TrackingCloseable closeable )
|
||||
{
|
||||
super( seekable, seekable, closeable );
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset given by {@code offset}, relative to a
|
||||
* start position determined by {@code whence}:
|
||||
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
|
||||
* given by {@code offset}, relative to a start position determined by {@code whence}:
|
||||
*
|
||||
* - {@code "set"}: {@code offset} is relative to the beginning of the file. - {@code "cur"}: Relative to the current position. This is the default.
|
||||
* - {@code "set"}: {@code offset} is relative to the beginning of the file.
|
||||
* - {@code "cur"}: Relative to the current position. This is the default.
|
||||
* - {@code "end"}: Relative to the end of the file.
|
||||
*
|
||||
* In case of success, {@code seek} returns the new file position from the beginning of the file.
|
||||
@ -114,9 +132,10 @@ public class BinaryWritableHandle extends HandleGeneric {
|
||||
* @cc.treturn string The reason seeking failed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
|
||||
this.checkOpen();
|
||||
return handleSeek(this.seekable, whence, offset);
|
||||
public final Object[] seek( Optional<String> whence, Optional<Long> offset ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
return handleSeek( seekable, whence, offset );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,14 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
@ -17,41 +20,27 @@ import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
|
||||
/**
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"} mode.
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"}
|
||||
* mode.
|
||||
*
|
||||
* @cc.module fs.ReadHandle
|
||||
*/
|
||||
public class EncodedReadableHandle extends HandleGeneric {
|
||||
public class EncodedReadableHandle extends HandleGeneric
|
||||
{
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
private final BufferedReader reader;
|
||||
|
||||
public EncodedReadableHandle(@Nonnull BufferedReader reader) {
|
||||
this(reader, reader);
|
||||
}
|
||||
|
||||
public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull Closeable closable) {
|
||||
super(closable);
|
||||
public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull TrackingCloseable closable )
|
||||
{
|
||||
super( closable );
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public static BufferedReader openUtf8(ReadableByteChannel channel) {
|
||||
return open(channel, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static BufferedReader open(ReadableByteChannel channel, Charset charset) {
|
||||
// Create a charset decoder with the same properties as StreamDecoder does for
|
||||
// InputStreams: namely, replace everything instead of erroring.
|
||||
CharsetDecoder decoder = charset.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
return new BufferedReader(Channels.newReader(channel, decoder, -1));
|
||||
public EncodedReadableHandle( @Nonnull BufferedReader reader )
|
||||
{
|
||||
this( reader, new TrackingCloseable.Impl( reader ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,21 +52,26 @@ public class EncodedReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||
this.checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse(false);
|
||||
try {
|
||||
String line = this.reader.readLine();
|
||||
if (line != null) {
|
||||
public final Object[] readLine( Optional<Boolean> withTrailingArg ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse( false );
|
||||
try
|
||||
{
|
||||
String line = reader.readLine();
|
||||
if( line != null )
|
||||
{
|
||||
// While this is technically inaccurate, it's better than nothing
|
||||
if (withTrailing) {
|
||||
line += "\n";
|
||||
}
|
||||
return new Object[] {line};
|
||||
} else {
|
||||
if( withTrailing ) line += "\n";
|
||||
return new Object[] { line };
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -90,20 +84,26 @@ public class EncodedReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readAll() throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
public final Object[] readAll() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
String line = this.reader.readLine();
|
||||
while (line != null) {
|
||||
result.append(line);
|
||||
line = this.reader.readLine();
|
||||
if (line != null) {
|
||||
result.append("\n");
|
||||
String line = reader.readLine();
|
||||
while( line != null )
|
||||
{
|
||||
result.append( line );
|
||||
line = reader.readLine();
|
||||
if( line != null )
|
||||
{
|
||||
result.append( "\n" );
|
||||
}
|
||||
}
|
||||
return new Object[] {result.toString()};
|
||||
} catch (IOException e) {
|
||||
return new Object[] { result.toString() };
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -118,50 +118,71 @@ public class EncodedReadableHandle extends HandleGeneric {
|
||||
* @cc.treturn string|nil The read characters, or {@code nil} if at the of the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] read(Optional<Integer> countA) throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
int count = countA.orElse(1);
|
||||
if (count < 0) {
|
||||
public final Object[] read( Optional<Integer> countA ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
int count = countA.orElse( 1 );
|
||||
if( count < 0 )
|
||||
{
|
||||
// Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so
|
||||
// it seems best to remain somewhat consistent.
|
||||
throw new LuaException("Cannot read a negative number of characters");
|
||||
} else if (count <= BUFFER_SIZE) {
|
||||
throw new LuaException( "Cannot read a negative number of characters" );
|
||||
}
|
||||
else if( count <= BUFFER_SIZE )
|
||||
{
|
||||
// If we've got a small count, then allocate that and read it.
|
||||
char[] chars = new char[count];
|
||||
int read = this.reader.read(chars);
|
||||
int read = reader.read( chars );
|
||||
|
||||
return read < 0 ? null : new Object[] {new String(chars, 0, read)};
|
||||
} else {
|
||||
return read < 0 ? null : new Object[] { new String( chars, 0, read ) };
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we've got a large count, read in bunches of 8192.
|
||||
char[] buffer = new char[BUFFER_SIZE];
|
||||
|
||||
// Read the initial set of characters, failing if none are read.
|
||||
int read = this.reader.read(buffer, 0, Math.min(buffer.length, count));
|
||||
if (read < 0) {
|
||||
return null;
|
||||
}
|
||||
int read = reader.read( buffer, 0, Math.min( buffer.length, count ) );
|
||||
if( read < 0 ) return null;
|
||||
|
||||
StringBuilder out = new StringBuilder(read);
|
||||
StringBuilder out = new StringBuilder( read );
|
||||
int totalRead = read;
|
||||
out.append(buffer, 0, read);
|
||||
out.append( buffer, 0, read );
|
||||
|
||||
// Otherwise read until we either reach the limit or we no longer consume
|
||||
// the full buffer.
|
||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
||||
read = this.reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead));
|
||||
if (read < 0) {
|
||||
break;
|
||||
}
|
||||
while( read >= BUFFER_SIZE && totalRead < count )
|
||||
{
|
||||
read = reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) );
|
||||
if( read < 0 ) break;
|
||||
|
||||
totalRead += read;
|
||||
out.append(buffer, 0, read);
|
||||
out.append( buffer, 0, read );
|
||||
}
|
||||
|
||||
return new Object[] {out.toString()};
|
||||
return new Object[] { out.toString() };
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static BufferedReader openUtf8( ReadableByteChannel channel )
|
||||
{
|
||||
return open( channel, StandardCharsets.UTF_8 );
|
||||
}
|
||||
|
||||
public static BufferedReader open( ReadableByteChannel channel, Charset charset )
|
||||
{
|
||||
// Create a charset decoder with the same properties as StreamDecoder does for
|
||||
// InputStreams: namely, replace everything instead of erroring.
|
||||
CharsetDecoder decoder = charset.newDecoder()
|
||||
.onMalformedInput( CodingErrorAction.REPLACE )
|
||||
.onUnmappableCharacter( CodingErrorAction.REPLACE );
|
||||
return new BufferedReader( Channels.newReader( channel, decoder, -1 ) );
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,16 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
import dan200.computercraft.shared.util.StringUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
@ -16,39 +21,21 @@ import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.shared.util.StringUtil;
|
||||
|
||||
/**
|
||||
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes.
|
||||
*
|
||||
* @cc.module fs.WriteHandle
|
||||
*/
|
||||
public class EncodedWritableHandle extends HandleGeneric {
|
||||
public class EncodedWritableHandle extends HandleGeneric
|
||||
{
|
||||
private final BufferedWriter writer;
|
||||
|
||||
public EncodedWritableHandle(@Nonnull BufferedWriter writer, @Nonnull Closeable closable) {
|
||||
super(closable);
|
||||
public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull TrackingCloseable closable )
|
||||
{
|
||||
super( closable );
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
public static BufferedWriter openUtf8(WritableByteChannel channel) {
|
||||
return open(channel, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static BufferedWriter open(WritableByteChannel channel, Charset charset) {
|
||||
// Create a charset encoder with the same properties as StreamEncoder does for
|
||||
// OutputStreams: namely, replace everything instead of erroring.
|
||||
CharsetEncoder encoder = charset.newEncoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
return new BufferedWriter(Channels.newWriter(channel, encoder, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a string of characters to the file.
|
||||
*
|
||||
@ -57,13 +44,17 @@ public class EncodedWritableHandle extends HandleGeneric {
|
||||
* @cc.param value The value to write to the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void write(IArguments args) throws LuaException {
|
||||
this.checkOpen();
|
||||
String text = StringUtil.toString(args.get(0));
|
||||
try {
|
||||
this.writer.write(text, 0, text.length());
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
public final void write( IArguments args ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
String text = StringUtil.toString( args.get( 0 ) );
|
||||
try
|
||||
{
|
||||
writer.write( text, 0, text.length() );
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new LuaException( e.getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,14 +66,18 @@ public class EncodedWritableHandle extends HandleGeneric {
|
||||
* @cc.param value The value to write to the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void writeLine(IArguments args) throws LuaException {
|
||||
this.checkOpen();
|
||||
String text = StringUtil.toString(args.get(0));
|
||||
try {
|
||||
this.writer.write(text, 0, text.length());
|
||||
this.writer.newLine();
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
public final void writeLine( IArguments args ) throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
String text = StringUtil.toString( args.get( 0 ) );
|
||||
try
|
||||
{
|
||||
writer.write( text, 0, text.length() );
|
||||
writer.newLine();
|
||||
}
|
||||
catch( IOException e )
|
||||
{
|
||||
throw new LuaException( e.getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,11 +87,30 @@ public class EncodedWritableHandle extends HandleGeneric {
|
||||
* @throws LuaException If the file has been closed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void flush() throws LuaException {
|
||||
this.checkOpen();
|
||||
try {
|
||||
this.writer.flush();
|
||||
} catch (IOException ignored) {
|
||||
public final void flush() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
try
|
||||
{
|
||||
writer.flush();
|
||||
}
|
||||
catch( IOException ignored )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static BufferedWriter openUtf8( WritableByteChannel channel )
|
||||
{
|
||||
return open( channel, StandardCharsets.UTF_8 );
|
||||
}
|
||||
|
||||
public static BufferedWriter open( WritableByteChannel channel, Charset charset )
|
||||
{
|
||||
// Create a charset encoder with the same properties as StreamEncoder does for
|
||||
// OutputStreams: namely, replace everything instead of erroring.
|
||||
CharsetEncoder encoder = charset.newEncoder()
|
||||
.onMalformedInput( CodingErrorAction.REPLACE )
|
||||
.onUnmappableCharacter( CodingErrorAction.REPLACE );
|
||||
return new BufferedWriter( Channels.newWriter( channel, encoder, -1 ) );
|
||||
}
|
||||
}
|
||||
|
@ -3,79 +3,38 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.io.Closeable;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
public abstract class HandleGeneric
|
||||
{
|
||||
private TrackingCloseable closeable;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
|
||||
public abstract class HandleGeneric {
|
||||
private Closeable closable;
|
||||
private boolean open = true;
|
||||
|
||||
protected HandleGeneric(@Nonnull Closeable closable) {
|
||||
this.closable = closable;
|
||||
protected HandleGeneric( @Nonnull TrackingCloseable closeable )
|
||||
{
|
||||
this.closeable = closeable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared implementation for various file handle types.
|
||||
*
|
||||
* @param channel The channel to seek in
|
||||
* @param whence The seeking mode.
|
||||
* @param offset The offset to seek to.
|
||||
* @return The new position of the file, or null if some error occurred.
|
||||
* @throws LuaException If the arguments were invalid
|
||||
* @see <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 void checkOpen() throws LuaException
|
||||
{
|
||||
TrackingCloseable closeable = this.closeable;
|
||||
if( closeable == null || !closeable.isOpen() ) throw new LuaException( "attempt to use a closed file" );
|
||||
}
|
||||
|
||||
protected static SeekableByteChannel asSeekable(Channel channel) {
|
||||
if (!(channel instanceof SeekableByteChannel)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SeekableByteChannel seekable = (SeekableByteChannel) channel;
|
||||
try {
|
||||
seekable.position(seekable.position());
|
||||
return seekable;
|
||||
} catch (IOException | UnsupportedOperationException e) {
|
||||
return null;
|
||||
}
|
||||
protected final void close()
|
||||
{
|
||||
IoUtil.closeQuietly( closeable );
|
||||
closeable = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,22 +44,69 @@ public abstract class HandleGeneric {
|
||||
*
|
||||
* @throws LuaException If the file has already been closed.
|
||||
*/
|
||||
@LuaFunction ("close")
|
||||
public final void doClose() throws LuaException {
|
||||
this.checkOpen();
|
||||
this.close();
|
||||
@LuaFunction( "close" )
|
||||
public final void doClose() throws LuaException
|
||||
{
|
||||
checkOpen();
|
||||
close();
|
||||
}
|
||||
|
||||
protected void checkOpen() throws LuaException {
|
||||
if (!this.open) {
|
||||
throw new LuaException("attempt to use a closed file");
|
||||
|
||||
/**
|
||||
* Shared implementation for various file handle types.
|
||||
*
|
||||
* @param channel The channel to seek in
|
||||
* @param whence The seeking mode.
|
||||
* @param offset The offset to seek to.
|
||||
* @return The new position of the file, or null if some error occurred.
|
||||
* @throws LuaException If the arguments were invalid
|
||||
* @see <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() {
|
||||
this.open = false;
|
||||
protected static SeekableByteChannel asSeekable( Channel channel )
|
||||
{
|
||||
if( !(channel instanceof SeekableByteChannel) ) return null;
|
||||
|
||||
IoUtil.closeQuietly(this.closable);
|
||||
this.closable = null;
|
||||
SeekableByteChannel seekable = (SeekableByteChannel) channel;
|
||||
try
|
||||
{
|
||||
seekable.position( seekable.position() );
|
||||
return seekable;
|
||||
}
|
||||
catch( IOException | UnsupportedOperationException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,19 +3,8 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import static dan200.computercraft.core.apis.http.request.HttpRequest.getHeaderSize;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
@ -29,22 +18,20 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.CompositeByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpObject;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpUtil;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.handler.codec.http.*;
|
||||
|
||||
public final class HttpRequestHandler extends SimpleChannelInboundHandler<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}.
|
||||
*/
|
||||
@ -53,16 +40,19 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
||||
private static final byte[] EMPTY_BYTES = new byte[0];
|
||||
|
||||
private final HttpRequest request;
|
||||
private boolean closed = false;
|
||||
|
||||
private final URI uri;
|
||||
private final HttpMethod method;
|
||||
private final Options options;
|
||||
private final HttpHeaders responseHeaders = new DefaultHttpHeaders();
|
||||
private boolean closed = false;
|
||||
|
||||
private Charset responseCharset;
|
||||
private final HttpHeaders responseHeaders = new DefaultHttpHeaders();
|
||||
private HttpResponseStatus responseStatus;
|
||||
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.uri = uri;
|
||||
@ -71,203 +61,199 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
if (this.request.checkClosed()) {
|
||||
return;
|
||||
}
|
||||
public void channelActive( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
if( request.checkClosed() ) return;
|
||||
|
||||
ByteBuf body = this.request.body();
|
||||
body.resetReaderIndex()
|
||||
.retain();
|
||||
ByteBuf body = request.body();
|
||||
body.resetReaderIndex().retain();
|
||||
|
||||
String requestUri = this.uri.getRawPath();
|
||||
if (this.uri.getRawQuery() != null) {
|
||||
requestUri += "?" + this.uri.getRawQuery();
|
||||
}
|
||||
String requestUri = uri.getRawPath();
|
||||
if( uri.getRawQuery() != null ) requestUri += "?" + uri.getRawQuery();
|
||||
|
||||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, requestUri, body);
|
||||
request.setMethod(this.method);
|
||||
request.headers()
|
||||
.set(this.request.headers());
|
||||
FullHttpRequest request = new DefaultFullHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.GET, requestUri, body );
|
||||
request.setMethod( method );
|
||||
request.headers().set( this.request.headers() );
|
||||
|
||||
// We force some headers to be always applied
|
||||
if (!request.headers()
|
||||
.contains(HttpHeaderNames.ACCEPT_CHARSET)) {
|
||||
request.headers()
|
||||
.set(HttpHeaderNames.ACCEPT_CHARSET, "UTF-8");
|
||||
if( !request.headers().contains( HttpHeaderNames.ACCEPT_CHARSET ) )
|
||||
{
|
||||
request.headers().set( HttpHeaderNames.ACCEPT_CHARSET, "UTF-8" );
|
||||
}
|
||||
if (!request.headers()
|
||||
.contains(HttpHeaderNames.USER_AGENT)) {
|
||||
request.headers()
|
||||
.set(HttpHeaderNames.USER_AGENT,
|
||||
this.request.environment()
|
||||
.getComputerEnvironment()
|
||||
.getUserAgent());
|
||||
}
|
||||
request.headers()
|
||||
.set(HttpHeaderNames.HOST, this.uri.getPort() < 0 ? this.uri.getHost() : this.uri.getHost() + ":" + this.uri.getPort());
|
||||
request.headers()
|
||||
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
request.headers().set( HttpHeaderNames.HOST, uri.getPort() < 0 ? uri.getHost() : uri.getHost() + ":" + uri.getPort() );
|
||||
request.headers().set( HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE );
|
||||
|
||||
ctx.channel()
|
||||
.writeAndFlush(request);
|
||||
ctx.channel().writeAndFlush( request );
|
||||
|
||||
super.channelActive(ctx);
|
||||
super.channelActive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
if (!this.closed) {
|
||||
this.request.failure("Could not connect");
|
||||
}
|
||||
super.channelInactive(ctx);
|
||||
public void channelInactive( ChannelHandlerContext ctx ) throws Exception
|
||||
{
|
||||
if( !closed ) request.failure( "Could not connect" );
|
||||
super.channelInactive( ctx );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
if (ComputerCraft.logPeripheralErrors) {
|
||||
ComputerCraft.log.error("Error handling HTTP response", cause);
|
||||
}
|
||||
this.request.failure(cause);
|
||||
}
|
||||
public void channelRead0( ChannelHandlerContext ctx, HttpObject message )
|
||||
{
|
||||
if( closed || request.checkClosed() ) return;
|
||||
|
||||
@Override
|
||||
public void channelRead0(ChannelHandlerContext ctx, HttpObject message) {
|
||||
if (this.closed || this.request.checkClosed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message instanceof HttpResponse) {
|
||||
if( message instanceof HttpResponse )
|
||||
{
|
||||
HttpResponse response = (HttpResponse) message;
|
||||
|
||||
if (this.request.redirects.get() > 0) {
|
||||
URI redirect = this.getRedirect(response.status(), response.headers());
|
||||
if (redirect != null && !this.uri.equals(redirect) && this.request.redirects.getAndDecrement() > 0) {
|
||||
if( request.redirects.get() > 0 )
|
||||
{
|
||||
URI redirect = getRedirect( response.status(), response.headers() );
|
||||
if( redirect != null && !uri.equals( redirect ) && request.redirects.getAndDecrement() > 0 )
|
||||
{
|
||||
// If we have a redirect, and don't end up at the same place, then follow it.
|
||||
|
||||
// We mark ourselves as disposed first though, to avoid firing events when the channel
|
||||
// becomes inactive or disposed.
|
||||
this.closed = true;
|
||||
closed = true;
|
||||
ctx.close();
|
||||
|
||||
try {
|
||||
HttpRequest.checkUri(redirect);
|
||||
} catch (HTTPRequestException e) {
|
||||
try
|
||||
{
|
||||
HttpRequest.checkUri( redirect );
|
||||
}
|
||||
catch( HTTPRequestException e )
|
||||
{
|
||||
// If we cannot visit this uri, then fail.
|
||||
this.request.failure(e.getMessage());
|
||||
request.failure( e.getMessage() );
|
||||
return;
|
||||
}
|
||||
|
||||
this.request.request(redirect,
|
||||
response.status()
|
||||
.code() == 303 ? HttpMethod.GET : this.method);
|
||||
request.request( redirect, response.status().code() == 303 ? HttpMethod.GET : method );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.responseCharset = HttpUtil.getCharset(response, StandardCharsets.UTF_8);
|
||||
this.responseStatus = response.status();
|
||||
this.responseHeaders.add(response.headers());
|
||||
responseCharset = HttpUtil.getCharset( response, StandardCharsets.UTF_8 );
|
||||
responseStatus = response.status();
|
||||
responseHeaders.add( response.headers() );
|
||||
}
|
||||
|
||||
if (message instanceof HttpContent) {
|
||||
if( message instanceof HttpContent )
|
||||
{
|
||||
HttpContent content = (HttpContent) message;
|
||||
|
||||
if (this.responseBody == null) {
|
||||
this.responseBody = ctx.alloc()
|
||||
.compositeBuffer(DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS);
|
||||
if( responseBody == null )
|
||||
{
|
||||
responseBody = ctx.alloc().compositeBuffer( DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS );
|
||||
}
|
||||
|
||||
ByteBuf partial = content.content();
|
||||
if (partial.isReadable()) {
|
||||
if( partial.isReadable() )
|
||||
{
|
||||
// If we've read more than we're allowed to handle, abort as soon as possible.
|
||||
if (this.options.maxDownload != 0 && this.responseBody.readableBytes() + partial.readableBytes() > this.options.maxDownload) {
|
||||
this.closed = true;
|
||||
if( options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload )
|
||||
{
|
||||
closed = true;
|
||||
ctx.close();
|
||||
|
||||
this.request.failure("Response is too large");
|
||||
request.failure( "Response is too large" );
|
||||
return;
|
||||
}
|
||||
|
||||
this.responseBody.addComponent(true, partial.retain());
|
||||
responseBody.addComponent( true, partial.retain() );
|
||||
}
|
||||
|
||||
if (message instanceof LastHttpContent) {
|
||||
if( message instanceof LastHttpContent )
|
||||
{
|
||||
LastHttpContent last = (LastHttpContent) message;
|
||||
this.responseHeaders.add(last.trailingHeaders());
|
||||
responseHeaders.add( last.trailingHeaders() );
|
||||
|
||||
// Set the content length, if not already given.
|
||||
if (this.responseHeaders.contains(HttpHeaderNames.CONTENT_LENGTH)) {
|
||||
this.responseHeaders.set(HttpHeaderNames.CONTENT_LENGTH, this.responseBody.readableBytes());
|
||||
if( responseHeaders.contains( HttpHeaderNames.CONTENT_LENGTH ) )
|
||||
{
|
||||
responseHeaders.set( HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes() );
|
||||
}
|
||||
|
||||
ctx.close();
|
||||
this.sendResponse();
|
||||
sendResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause )
|
||||
{
|
||||
if( ComputerCraft.logPeripheralErrors ) ComputerCraft.log.error( "Error handling HTTP response", cause );
|
||||
request.failure( cause );
|
||||
}
|
||||
|
||||
private void sendResponse()
|
||||
{
|
||||
// Read the ByteBuf into a channel.
|
||||
CompositeByteBuf body = responseBody;
|
||||
byte[] bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes( body );
|
||||
|
||||
// Decode the headers
|
||||
HttpResponseStatus status = responseStatus;
|
||||
Map<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.
|
||||
*
|
||||
* @param status The status of the HTTP response.
|
||||
* @param status The status of the HTTP response.
|
||||
* @param headers The headers of the HTTP response.
|
||||
* @return The URI to redirect to, or {@code null} if no redirect should occur.
|
||||
*/
|
||||
private URI getRedirect(HttpResponseStatus status, HttpHeaders headers) {
|
||||
private URI getRedirect( HttpResponseStatus status, HttpHeaders headers )
|
||||
{
|
||||
int code = status.code();
|
||||
if (code < 300 || code > 307 || code == 304 || code == 306) {
|
||||
if( code < 300 || code > 307 || code == 304 || code == 306 ) return null;
|
||||
|
||||
String location = headers.get( HttpHeaderNames.LOCATION );
|
||||
if( location == null ) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return uri.resolve( new URI( location ) );
|
||||
}
|
||||
catch( IllegalArgumentException | URISyntaxException e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
String location = headers.get(HttpHeaderNames.LOCATION);
|
||||
if (location == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return this.uri.resolve(new URI( location ));
|
||||
} catch( IllegalArgumentException | URISyntaxException e ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendResponse() {
|
||||
// Read the ByteBuf into a channel.
|
||||
CompositeByteBuf body = this.responseBody;
|
||||
byte[] bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes(body);
|
||||
|
||||
// Decode the headers
|
||||
HttpResponseStatus status = this.responseStatus;
|
||||
Map<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
|
||||
public void close() {
|
||||
this.closed = true;
|
||||
if (this.responseBody != null) {
|
||||
this.responseBody.release();
|
||||
this.responseBody = null;
|
||||
public void close()
|
||||
{
|
||||
closed = true;
|
||||
if( responseBody != null )
|
||||
{
|
||||
responseBody.release();
|
||||
responseBody = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -64,9 +64,9 @@ public final class Generator<T> {
|
||||
|
||||
private final Function<T, T> wrap;
|
||||
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()
|
||||
.build(CacheLoader.from(this::build));
|
||||
.build(CacheLoader.from(catching(this::build, Collections.emptyList())));
|
||||
|
||||
Generator(Class<T> base, List<Class<?>> context, Function<T, T> wrap) {
|
||||
this.base = base;
|
||||
@ -374,4 +374,22 @@ public final class Generator<T> {
|
||||
method.getName());
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import java.io.Closeable;
|
||||
@ -13,30 +12,38 @@ import java.nio.channels.Channel;
|
||||
/**
|
||||
* Wraps some closeable object such as a buffered writer, and the underlying stream.
|
||||
*
|
||||
* When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown this causes us to release the channel,
|
||||
* but not actually close it. This wrapper will attempt to close the wrapper (and so hopefully flush the channel), and then close the underlying channel.
|
||||
* When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown
|
||||
* this causes us to release the channel, but not actually close it. This wrapper will attempt to close the wrapper (and
|
||||
* so hopefully flush the channel), and then close the underlying channel.
|
||||
*
|
||||
* @param <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 Channel channel;
|
||||
|
||||
ChannelWrapper(T wrapper, Channel channel) {
|
||||
ChannelWrapper( T wrapper, Channel channel )
|
||||
{
|
||||
this.wrapper = wrapper;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
this.wrapper.close();
|
||||
} finally {
|
||||
this.channel.close();
|
||||
public void close() throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
wrapper.close();
|
||||
}
|
||||
finally
|
||||
{
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return this.wrapper;
|
||||
T get()
|
||||
{
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,48 +3,68 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.shared.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* An alternative closeable implementation that will free up resources in the filesystem.
|
||||
*
|
||||
* The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of (say, the Lua object referencing it has
|
||||
* gone), then the wrapped object will be closed by the filesystem.
|
||||
* The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of
|
||||
* (say, the Lua object referencing it has gone), then the wrapped object will be closed by the filesystem.
|
||||
*
|
||||
* Closing this will stop the filesystem tracking it, reducing the current descriptor count.
|
||||
*
|
||||
* In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks on the stream, it's not really possible as it'd require
|
||||
* numerous instances.
|
||||
* In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks
|
||||
* on the stream, it's not really possible as it'd require numerous instances.
|
||||
*
|
||||
* @param <T> The type of writer or channel to wrap.
|
||||
*/
|
||||
public class FileSystemWrapper<T extends Closeable> implements Closeable {
|
||||
final WeakReference<FileSystemWrapper<?>> self;
|
||||
public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable
|
||||
{
|
||||
private final FileSystem fileSystem;
|
||||
final MountWrapper mount;
|
||||
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.mount = mount;
|
||||
this.closeable = closeable;
|
||||
this.self = new WeakReference<>(this, queue);
|
||||
self = new WeakReference<>( this, queue );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.fileSystem.removeFile(this);
|
||||
this.closeable.close();
|
||||
public void close() throws IOException
|
||||
{
|
||||
isOpen = false;
|
||||
fileSystem.removeFile( this );
|
||||
closeable.close();
|
||||
}
|
||||
|
||||
void closeExternally()
|
||||
{
|
||||
isOpen = false;
|
||||
IoUtil.closeQuietly( closeable );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen()
|
||||
{
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public T get() {
|
||||
return this.closeable.get();
|
||||
public T get()
|
||||
{
|
||||
return closeable.get();
|
||||
}
|
||||
}
|
||||
|
@ -90,19 +90,30 @@ public final class ResourceMount implements IMount {
|
||||
|
||||
private void load() {
|
||||
boolean hasAny = false;
|
||||
FileEntry newRoot = new FileEntry(new Identifier(this.namespace, this.subPath));
|
||||
for (Identifier file : this.manager.findResources(this.subPath, s -> true)) {
|
||||
if (!file.getNamespace()
|
||||
.equals(this.namespace)) {
|
||||
continue;
|
||||
}
|
||||
String existingNamespace = null;
|
||||
|
||||
FileEntry newRoot = new FileEntry( new Identifier( namespace, subPath ) );
|
||||
for( Identifier file : manager.findResources( subPath, s -> true ) )
|
||||
{
|
||||
existingNamespace = file.getNamespace();
|
||||
|
||||
if( !file.getNamespace().equals( namespace ) ) continue;
|
||||
|
||||
String localPath = FileSystem.toLocal(file.getPath(), this.subPath);
|
||||
this.create(newRoot, localPath);
|
||||
hasAny = true;
|
||||
}
|
||||
|
||||
this.root = hasAny ? newRoot : null;
|
||||
root = hasAny ? newRoot : null;
|
||||
|
||||
if( !hasAny )
|
||||
{
|
||||
ComputerCraft.log.warn("Cannot find any files under /data/{}/{} for resource mount.", namespace, subPath);
|
||||
if( newRoot != null )
|
||||
{
|
||||
ComputerCraft.log.warn("There are files under /data/{}/{} though. Did you get the wrong namespace?", existingNamespace, subPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void create(FileEntry lastEntry, String path) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,168 +3,84 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.core.terminal;
|
||||
|
||||
public class TextBuffer {
|
||||
private final char[] m_text;
|
||||
public class TextBuffer
|
||||
{
|
||||
private final char[] text;
|
||||
|
||||
public TextBuffer(char c, int length) {
|
||||
this.m_text = new char[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
this.m_text[i] = c;
|
||||
}
|
||||
public TextBuffer( char c, int length )
|
||||
{
|
||||
text = new char[length];
|
||||
this.fill( c );
|
||||
}
|
||||
|
||||
public TextBuffer(String text) {
|
||||
this(text, 1);
|
||||
public TextBuffer( String text )
|
||||
{
|
||||
this.text = text.toCharArray();
|
||||
}
|
||||
|
||||
public TextBuffer(String text, int repetitions) {
|
||||
int textLength = text.length();
|
||||
this.m_text = new char[textLength * repetitions];
|
||||
for (int i = 0; i < repetitions; i++) {
|
||||
for (int j = 0; j < textLength; j++) {
|
||||
this.m_text[j + i * textLength] = text.charAt(j);
|
||||
}
|
||||
}
|
||||
public int length()
|
||||
{
|
||||
return text.length;
|
||||
}
|
||||
|
||||
public TextBuffer(TextBuffer text) {
|
||||
this(text, 1);
|
||||
public void write( String text )
|
||||
{
|
||||
write( text, 0 );
|
||||
}
|
||||
|
||||
public TextBuffer(TextBuffer text, int repetitions) {
|
||||
int textLength = text.length();
|
||||
this.m_text = new char[textLength * repetitions];
|
||||
for (int i = 0; i < repetitions; i++) {
|
||||
for (int j = 0; j < textLength; j++) {
|
||||
this.m_text[j + i * textLength] = text.charAt(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return this.m_text.length;
|
||||
}
|
||||
|
||||
public char charAt(int i) {
|
||||
return this.m_text[i];
|
||||
}
|
||||
|
||||
public String read() {
|
||||
return this.read(0, this.m_text.length);
|
||||
}
|
||||
|
||||
public String read(int start, int end) {
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, this.m_text.length);
|
||||
int textLength = Math.max(end - start, 0);
|
||||
return new String(this.m_text, start, textLength);
|
||||
}
|
||||
|
||||
public String read(int start) {
|
||||
return this.read(start, this.m_text.length);
|
||||
}
|
||||
|
||||
public void write(String text) {
|
||||
this.write(text, 0, text.length());
|
||||
}
|
||||
|
||||
public void write(String text, int start, int end) {
|
||||
public void write( String text, int start )
|
||||
{
|
||||
int pos = start;
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, pos + text.length());
|
||||
end = Math.min(end, this.m_text.length);
|
||||
for (int i = start; i < end; i++) {
|
||||
this.m_text[i] = text.charAt(i - pos);
|
||||
start = Math.max( start, 0 );
|
||||
int end = Math.min( start + text.length(), pos + text.length() );
|
||||
end = Math.min( end, this.text.length );
|
||||
for( int i = start; i < end; i++ )
|
||||
{
|
||||
this.text[i] = text.charAt( i - pos );
|
||||
}
|
||||
}
|
||||
|
||||
public void write(String text, int start) {
|
||||
this.write(text, start, start + text.length());
|
||||
}
|
||||
|
||||
public void write(TextBuffer text) {
|
||||
this.write(text, 0, text.length());
|
||||
}
|
||||
|
||||
public void write(TextBuffer text, int start, int end) {
|
||||
int pos = start;
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, pos + text.length());
|
||||
end = Math.min(end, this.m_text.length);
|
||||
for (int i = start; i < end; i++) {
|
||||
this.m_text[i] = text.charAt(i - pos);
|
||||
public void write( TextBuffer text )
|
||||
{
|
||||
int end = Math.min( text.length(), this.text.length );
|
||||
for( int i = 0; i < end; i++ )
|
||||
{
|
||||
this.text[i] = text.charAt( i );
|
||||
}
|
||||
}
|
||||
|
||||
public void write(TextBuffer text, int start) {
|
||||
this.write(text, start, start + text.length());
|
||||
public void fill( char c )
|
||||
{
|
||||
fill( c, 0, text.length );
|
||||
}
|
||||
|
||||
public void fill(char c) {
|
||||
this.fill(c, 0, this.m_text.length);
|
||||
}
|
||||
|
||||
public void fill(char c, int start, int end) {
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, this.m_text.length);
|
||||
for (int i = start; i < end; i++) {
|
||||
this.m_text[i] = c;
|
||||
public void fill( char c, int start, int end )
|
||||
{
|
||||
start = Math.max( start, 0 );
|
||||
end = Math.min( end, text.length );
|
||||
for( int i = start; i < end; i++ )
|
||||
{
|
||||
text[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
public void fill(char c, int start) {
|
||||
this.fill(c, start, this.m_text.length);
|
||||
public char charAt( int i )
|
||||
{
|
||||
return text[i];
|
||||
}
|
||||
|
||||
public void fill(String text) {
|
||||
this.fill(text, 0, this.m_text.length);
|
||||
}
|
||||
|
||||
public void fill(String text, int start, int end) {
|
||||
int pos = start;
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, this.m_text.length);
|
||||
|
||||
int textLength = text.length();
|
||||
for (int i = start; i < end; i++) {
|
||||
this.m_text[i] = text.charAt((i - pos) % textLength);
|
||||
public void setChar( int i, char c )
|
||||
{
|
||||
if( i >= 0 && i < text.length )
|
||||
{
|
||||
text[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
public void fill(String text, int start) {
|
||||
this.fill(text, start, this.m_text.length);
|
||||
public String toString()
|
||||
{
|
||||
return new String( text );
|
||||
}
|
||||
|
||||
public void fill(TextBuffer text) {
|
||||
this.fill(text, 0, this.m_text.length);
|
||||
}
|
||||
|
||||
public void fill(TextBuffer text, int start, int end) {
|
||||
int pos = start;
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, this.m_text.length);
|
||||
|
||||
int textLength = text.length();
|
||||
for (int i = start; i < end; i++) {
|
||||
this.m_text[i] = text.charAt((i - pos) % textLength);
|
||||
}
|
||||
}
|
||||
|
||||
public void fill(TextBuffer text, int start) {
|
||||
this.fill(text, start, this.m_text.length);
|
||||
}
|
||||
|
||||
public void setChar(int i, char c) {
|
||||
if (i >= 0 && i < this.m_text.length) {
|
||||
this.m_text[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new String(this.m_text);
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ public class MixinWorld {
|
||||
|
||||
@Inject (method = "setBlockEntity", at = @At ("HEAD"))
|
||||
public void setBlockEntity(BlockPos pos, @Nullable BlockEntity entity, CallbackInfo info) {
|
||||
if (!World.isHeightInvalid(pos) && entity != null && !entity.isRemoved() && this.iteratingTickingBlockEntities) {
|
||||
if (!World.isOutOfBuildLimitVertically(pos) && entity != null && !entity.isRemoved() && this.iteratingTickingBlockEntities) {
|
||||
setWorld(entity, this);
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ public final class BundledRedstone {
|
||||
}
|
||||
|
||||
public static int getDefaultOutput(@Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side) {
|
||||
return World.method_24794(pos) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput(world, pos, side) : -1;
|
||||
return World.isValid(pos) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput(world, pos, side) : -1;
|
||||
}
|
||||
|
||||
public static int getOutput(World world, BlockPos pos, Direction side) {
|
||||
@ -40,7 +40,7 @@ public final class BundledRedstone {
|
||||
}
|
||||
|
||||
private static int getUnmaskedOutput(World world, BlockPos pos, Direction side) {
|
||||
if (!World.method_24794(pos)) {
|
||||
if (!World.isValid(pos)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -35,13 +35,11 @@ public final class Peripherals {
|
||||
|
||||
@Nullable
|
||||
public static IPeripheral getPeripheral(World world, BlockPos pos, Direction side) {
|
||||
return World.method_24794(pos) && !world.isClient ? getPeripheralAt(world, pos, side) : null;
|
||||
return World.isValid(pos) && !world.isClient ? getPeripheralAt(world, pos, side) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static IPeripheral getPeripheralAt(World world, BlockPos pos, Direction side) {
|
||||
BlockEntity block = world.getBlockEntity(pos);
|
||||
|
||||
// Try the handlers in order:
|
||||
for (IPeripheralProvider peripheralProvider : providers) {
|
||||
try {
|
||||
|
@ -3,61 +3,59 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.shared;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.ModContainer;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.Util;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import dan200.computercraft.shared.util.InventoryUtil;
|
||||
|
||||
import net.minecraft.item.ItemStack;
|
||||
|
||||
public final class PocketUpgrades {
|
||||
public final class PocketUpgrades
|
||||
{
|
||||
private static final Map<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() {}
|
||||
|
||||
public static synchronized void register(@Nonnull IPocketUpgrade upgrade) {
|
||||
Objects.requireNonNull(upgrade, "upgrade cannot be null");
|
||||
public static synchronized void register( @Nonnull IPocketUpgrade upgrade )
|
||||
{
|
||||
Objects.requireNonNull( upgrade, "upgrade cannot be null" );
|
||||
|
||||
String id = upgrade.getUpgradeID()
|
||||
.toString();
|
||||
IPocketUpgrade existing = upgrades.get(id);
|
||||
if (existing != null) {
|
||||
throw new IllegalStateException("Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is " +
|
||||
"already registered by '" + existing.getUnlocalisedAdjective() + " pocket computer'");
|
||||
String id = upgrade.getUpgradeID().toString();
|
||||
IPocketUpgrade existing = upgrades.get( id );
|
||||
if( existing != null )
|
||||
{
|
||||
throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " pocket computer'. UpgradeID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " pocket computer'" );
|
||||
}
|
||||
|
||||
upgrades.put(id, upgrade);
|
||||
upgrades.put( id, upgrade );
|
||||
|
||||
// Infer the mod id by the identifier of the upgrade. This is not how the forge api works, so it may break peripheral mods using the api.
|
||||
// TODO: get the mod id of the mod that is currently being loaded.
|
||||
ModContainer mc = FabricLoader.getInstance().getModContainer(upgrade.getUpgradeID().getNamespace()).orElseGet(null);
|
||||
if( mc != null && mc.getMetadata().getId() != null ) upgradeOwners.put( upgrade, mc.getMetadata().getId() );
|
||||
}
|
||||
|
||||
public static IPocketUpgrade get(String id) {
|
||||
public static IPocketUpgrade get( String id )
|
||||
{
|
||||
// Fix a typo in the advanced modem upgrade's name. I'm sorry, I realise this is horrible.
|
||||
if (id.equals("computercraft:advanved_modem")) {
|
||||
id = "computercraft:advanced_modem";
|
||||
}
|
||||
if( id.equals( "computercraft:advanved_modem" ) ) id = "computercraft:advanced_modem";
|
||||
|
||||
return upgrades.get(id);
|
||||
return upgrades.get( id );
|
||||
}
|
||||
|
||||
public static IPocketUpgrade get(@Nonnull ItemStack stack) {
|
||||
if (stack.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
public static IPocketUpgrade get( @Nonnull ItemStack stack )
|
||||
{
|
||||
if( stack.isEmpty() ) return null;
|
||||
|
||||
for (IPocketUpgrade upgrade : upgrades.values()) {
|
||||
for( IPocketUpgrade upgrade : upgrades.values() )
|
||||
{
|
||||
ItemStack craftingStack = upgrade.getCraftingItem();
|
||||
if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && upgrade.isItemSuitable( stack ) )
|
||||
{
|
||||
@ -69,19 +67,22 @@ public final class PocketUpgrades {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getOwner(IPocketUpgrade upgrade) {
|
||||
return upgradeOwners.get(upgrade);
|
||||
public static String getOwner( IPocketUpgrade upgrade )
|
||||
{
|
||||
return upgradeOwners.get( upgrade );
|
||||
}
|
||||
|
||||
public static Iterable<IPocketUpgrade> getVanillaUpgrades() {
|
||||
public static Iterable<IPocketUpgrade> getVanillaUpgrades()
|
||||
{
|
||||
List<IPocketUpgrade> vanilla = new ArrayList<>();
|
||||
vanilla.add(ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal);
|
||||
vanilla.add(ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced);
|
||||
vanilla.add(ComputerCraftRegistry.PocketUpgrades.speaker);
|
||||
vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemNormal );
|
||||
vanilla.add( ComputerCraftRegistry.PocketUpgrades.wirelessModemAdvanced );
|
||||
vanilla.add( ComputerCraftRegistry.PocketUpgrades.speaker );
|
||||
return vanilla;
|
||||
}
|
||||
|
||||
public static Iterable<IPocketUpgrade> getUpgrades() {
|
||||
return Collections.unmodifiableCollection(upgrades.values());
|
||||
public static Iterable<IPocketUpgrade> getUpgrades()
|
||||
{
|
||||
return Collections.unmodifiableCollection( upgrades.values() );
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,15 @@
|
||||
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
|
||||
package dan200.computercraft.shared;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import net.minecraft.item.ItemStack;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
@ -13,86 +19,72 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
public final class TurtleUpgrades
|
||||
{
|
||||
private static class Wrapper
|
||||
{
|
||||
final ITurtleUpgrade upgrade;
|
||||
final String id;
|
||||
final String modId;
|
||||
boolean enabled;
|
||||
|
||||
import dan200.computercraft.ComputerCraft;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
Wrapper( ITurtleUpgrade upgrade )
|
||||
{
|
||||
this.upgrade = upgrade;
|
||||
this.id = upgrade.getUpgradeID()
|
||||
.toString();
|
||||
// TODO This should be the mod id of the mod the peripheral comes from
|
||||
this.modId = ComputerCraft.MOD_ID;
|
||||
this.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
import net.minecraft.item.ItemStack;
|
||||
private static ITurtleUpgrade[] vanilla;
|
||||
|
||||
public final class TurtleUpgrades {
|
||||
private static final Map<String, ITurtleUpgrade> upgrades = new HashMap<>();
|
||||
private static final IdentityHashMap<ITurtleUpgrade, Wrapper> wrappers = new IdentityHashMap<>();
|
||||
private static ITurtleUpgrade[] vanilla;
|
||||
private static boolean needsRebuild;
|
||||
|
||||
private TurtleUpgrades() {}
|
||||
|
||||
public static void register(@Nonnull ITurtleUpgrade upgrade) {
|
||||
Objects.requireNonNull(upgrade, "upgrade cannot be null");
|
||||
public static void register( @Nonnull ITurtleUpgrade upgrade )
|
||||
{
|
||||
Objects.requireNonNull( upgrade, "upgrade cannot be null" );
|
||||
rebuild();
|
||||
|
||||
Wrapper wrapper = new Wrapper(upgrade);
|
||||
Wrapper wrapper = new Wrapper( upgrade );
|
||||
String id = wrapper.id;
|
||||
ITurtleUpgrade existing = upgrades.get(id);
|
||||
if (existing != null) {
|
||||
throw new IllegalStateException("Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already " +
|
||||
"registered by '" + existing.getUnlocalisedAdjective() + " Turtle'");
|
||||
ITurtleUpgrade existing = upgrades.get( id );
|
||||
if( existing != null )
|
||||
{
|
||||
throw new IllegalStateException( "Error registering '" + upgrade.getUnlocalisedAdjective() + " Turtle'. Upgrade ID '" + id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" );
|
||||
}
|
||||
|
||||
upgrades.put(id, upgrade);
|
||||
wrappers.put(upgrade, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the cache of turtle upgrades. This is done before querying the cache or registering new upgrades.
|
||||
*/
|
||||
private static void rebuild() {
|
||||
if (!needsRebuild) {
|
||||
return;
|
||||
}
|
||||
|
||||
upgrades.clear();
|
||||
for (Wrapper wrapper : wrappers.values()) {
|
||||
if (!wrapper.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ITurtleUpgrade existing = upgrades.get(wrapper.id);
|
||||
if (existing != null) {
|
||||
ComputerCraft.log.error("Error registering '" + wrapper.upgrade.getUnlocalisedAdjective() + " Turtle'." + " Upgrade ID '" + wrapper.id +
|
||||
"' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'");
|
||||
continue;
|
||||
}
|
||||
|
||||
upgrades.put(wrapper.id, wrapper.upgrade);
|
||||
}
|
||||
|
||||
needsRebuild = false;
|
||||
upgrades.put( id, upgrade );
|
||||
wrappers.put( upgrade, wrapper );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ITurtleUpgrade get(String id) {
|
||||
public static ITurtleUpgrade get( String id )
|
||||
{
|
||||
rebuild();
|
||||
return upgrades.get(id);
|
||||
return upgrades.get( id );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getOwner(@Nonnull ITurtleUpgrade upgrade) {
|
||||
Wrapper wrapper = wrappers.get(upgrade);
|
||||
public static String getOwner( @Nonnull ITurtleUpgrade upgrade )
|
||||
{
|
||||
Wrapper wrapper = wrappers.get( upgrade );
|
||||
return wrapper != null ? wrapper.modId : null;
|
||||
}
|
||||
|
||||
public static ITurtleUpgrade get(@Nonnull ItemStack stack) {
|
||||
if (stack.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
public static ITurtleUpgrade get( @Nonnull ItemStack stack )
|
||||
{
|
||||
if( stack.isEmpty() ) return null;
|
||||
|
||||
for (Wrapper wrapper : wrappers.values()) {
|
||||
if (!wrapper.enabled) {
|
||||
continue;
|
||||
}
|
||||
for( Wrapper wrapper : wrappers.values() )
|
||||
{
|
||||
if( !wrapper.enabled ) continue;
|
||||
|
||||
ItemStack craftingStack = wrapper.upgrade.getCraftingItem();
|
||||
if( !craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade.isItemSuitable( stack ) )
|
||||
@ -104,8 +96,10 @@ public final class TurtleUpgrades {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Stream<ITurtleUpgrade> getVanillaUpgrades() {
|
||||
if (vanilla == null) {
|
||||
public static Stream<ITurtleUpgrade> getVanillaUpgrades()
|
||||
{
|
||||
if( vanilla == null )
|
||||
{
|
||||
vanilla = new ITurtleUpgrade[] {
|
||||
// ComputerCraft upgrades
|
||||
ComputerCraftRegistry.TurtleUpgrades.wirelessModemNormal,
|
||||
@ -119,64 +113,69 @@ public final class TurtleUpgrades {
|
||||
ComputerCraftRegistry.TurtleUpgrades.diamondShovel,
|
||||
ComputerCraftRegistry.TurtleUpgrades.diamondHoe,
|
||||
ComputerCraftRegistry.TurtleUpgrades.craftingTable,
|
||||
|
||||
ComputerCraftRegistry.TurtleUpgrades.netheritePickaxe,
|
||||
};
|
||||
}
|
||||
|
||||
return Arrays.stream(vanilla)
|
||||
.filter(x -> x != null && wrappers.get(x).enabled);
|
||||
return Arrays.stream( vanilla ).filter( x -> x != null && wrappers.get( x ).enabled );
|
||||
}
|
||||
|
||||
public static Stream<ITurtleUpgrade> getUpgrades() {
|
||||
return wrappers.values()
|
||||
.stream()
|
||||
.filter(x -> x.enabled)
|
||||
.map(x -> x.upgrade);
|
||||
public static Stream<ITurtleUpgrade> getUpgrades()
|
||||
{
|
||||
return wrappers.values().stream().filter( x -> x.enabled ).map( x -> x.upgrade );
|
||||
}
|
||||
|
||||
public static boolean suitableForFamily(ComputerFamily family, ITurtleUpgrade upgrade) {
|
||||
public static boolean suitableForFamily( ComputerFamily family, ITurtleUpgrade upgrade )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void enable(ITurtleUpgrade upgrade) {
|
||||
Wrapper wrapper = wrappers.get(upgrade);
|
||||
if (wrapper.enabled) {
|
||||
return;
|
||||
/**
|
||||
* Rebuild the cache of turtle upgrades. This is done before querying the cache or registering new upgrades.
|
||||
*/
|
||||
private static void rebuild()
|
||||
{
|
||||
if( !needsRebuild ) return;
|
||||
|
||||
upgrades.clear();
|
||||
for( Wrapper wrapper : wrappers.values() )
|
||||
{
|
||||
if( !wrapper.enabled ) continue;
|
||||
|
||||
ITurtleUpgrade existing = upgrades.get( wrapper.id );
|
||||
if( existing != null )
|
||||
{
|
||||
ComputerCraft.log.error( "Error registering '" + wrapper.upgrade.getUnlocalisedAdjective() + " Turtle'." +
|
||||
" Upgrade ID '" + wrapper.id + "' is already registered by '" + existing.getUnlocalisedAdjective() + " Turtle'" );
|
||||
continue;
|
||||
}
|
||||
|
||||
upgrades.put( wrapper.id, wrapper.upgrade );
|
||||
}
|
||||
|
||||
needsRebuild = false;
|
||||
}
|
||||
|
||||
public static void enable( ITurtleUpgrade upgrade )
|
||||
{
|
||||
Wrapper wrapper = wrappers.get( upgrade );
|
||||
if( wrapper.enabled ) return;
|
||||
|
||||
wrapper.enabled = true;
|
||||
needsRebuild = true;
|
||||
}
|
||||
|
||||
public static void disable(ITurtleUpgrade upgrade) {
|
||||
Wrapper wrapper = wrappers.get(upgrade);
|
||||
if (!wrapper.enabled) {
|
||||
return;
|
||||
}
|
||||
public static void disable( ITurtleUpgrade upgrade )
|
||||
{
|
||||
Wrapper wrapper = wrappers.get( upgrade );
|
||||
if( !wrapper.enabled ) return;
|
||||
|
||||
wrapper.enabled = false;
|
||||
upgrades.remove(wrapper.id);
|
||||
upgrades.remove( wrapper.id );
|
||||
}
|
||||
|
||||
public static void remove(ITurtleUpgrade upgrade) {
|
||||
wrappers.remove(upgrade);
|
||||
public static void remove( ITurtleUpgrade upgrade )
|
||||
{
|
||||
wrappers.remove( upgrade );
|
||||
needsRebuild = true;
|
||||
}
|
||||
|
||||
private static class Wrapper {
|
||||
final ITurtleUpgrade upgrade;
|
||||
final String id;
|
||||
final String modId;
|
||||
boolean enabled;
|
||||
|
||||
Wrapper(ITurtleUpgrade upgrade) {
|
||||
this.upgrade = upgrade;
|
||||
this.id = upgrade.getUpgradeID()
|
||||
.toString();
|
||||
// TODO This should be the mod id of the mod the peripheral comes from
|
||||
this.modId = ComputerCraft.MOD_ID;
|
||||
this.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.api.turtle.FakePlayer;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.command.CommandSource;
|
||||
import net.minecraft.command.CommandSource;
|
||||
import net.minecraft.server.command.ServerCommandSource;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
|
||||
|
@ -55,8 +55,9 @@ public final class RepeatArgumentType<T, U> implements ArgumentType<List<T>> {
|
||||
this.some = some;
|
||||
}
|
||||
|
||||
public static <T> RepeatArgumentType<T, T> some(ArgumentType<T> appender, SimpleCommandExceptionType missing) {
|
||||
return new RepeatArgumentType<>(appender, List::add, true, missing);
|
||||
public static <T> RepeatArgumentType<T, T> some( ArgumentType<T> appender, SimpleCommandExceptionType missing )
|
||||
{
|
||||
return new RepeatArgumentType<>( appender, List::add, false, missing );
|
||||
}
|
||||
|
||||
public static <T> RepeatArgumentType<T, List<T>> someFlat(ArgumentType<List<T>> appender, SimpleCommandExceptionType missing) {
|
||||
|
@ -8,7 +8,6 @@ package dan200.computercraft.shared.common;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import dan200.computercraft.shared.util.Colour;
|
||||
import dan200.computercraft.shared.util.ColourTracker;
|
||||
import dan200.computercraft.shared.util.ColourUtils;
|
||||
|
||||
@ -66,17 +65,10 @@ public final class ColourableRecipe extends SpecialCraftingRecipe {
|
||||
if (stack.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stack.getItem() instanceof IColouredItem) {
|
||||
colourable = stack;
|
||||
} else {
|
||||
DyeColor dye = ColourUtils.getStackColour(stack);
|
||||
if (dye == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Colour colour = Colour.fromInt(15 - dye.getId());
|
||||
tracker.addColour(colour.getR(), colour.getG(), colour.getB());
|
||||
else
|
||||
{
|
||||
DyeColor dye = ColourUtils.getStackColour( stack );
|
||||
if( dye != null ) tracker.addColour( dye );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,7 +207,7 @@ public class CommandAPI implements ILuaAPI {
|
||||
World world = this.computer.getWorld();
|
||||
BlockPos min = new BlockPos(Math.min(minX, maxX), Math.min(minY, maxY), Math.min(minZ, maxZ));
|
||||
BlockPos max = new BlockPos(Math.max(minX, maxX), Math.max(minY, maxY), Math.max(minZ, maxZ));
|
||||
if (!World.method_24794(min) || !World.method_24794(max)) {
|
||||
if (!World.isValid(min) || !World.isValid(max)) {
|
||||
throw new LuaException("Co-ordinates out of range");
|
||||
}
|
||||
|
||||
@ -284,7 +284,7 @@ public class CommandAPI implements ILuaAPI {
|
||||
// Get the details of the block
|
||||
World world = this.computer.getWorld();
|
||||
BlockPos position = new BlockPos(x, y, z);
|
||||
if (World.method_24794(position)) {
|
||||
if (World.isValid(position)) {
|
||||
return getBlockInfo(world, position);
|
||||
} else {
|
||||
throw new LuaException("Co-ordinates out of range");
|
||||
|
@ -316,7 +316,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getHostString() {
|
||||
return String.format("ComputerCraft %s (Minecraft %s)", ComputerCraftAPI.getInstalledVersion(), "1.16.2");
|
||||
return String.format("ComputerCraft %s (Minecraft %s)", ComputerCraftAPI.getInstalledVersion(), "1.16.4");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
@ -53,7 +53,9 @@ public class DiskRecipe extends SpecialCraftingRecipe {
|
||||
return false;
|
||||
}
|
||||
redstoneFound = true;
|
||||
} else if (ColourUtils.getStackColour(stack) != null) {
|
||||
}
|
||||
else if( ColourUtils.getStackColour( stack ) == null )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -74,14 +76,10 @@ public class DiskRecipe extends SpecialCraftingRecipe {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.paper.test(stack) && !this.redstone.test(stack)) {
|
||||
DyeColor dye = ColourUtils.getStackColour(stack);
|
||||
if (dye == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Colour colour = Colour.VALUES[dye.getId()];
|
||||
tracker.addColour(colour.getR(), colour.getG(), colour.getB());
|
||||
if( !paper.test( stack ) && !redstone.test( stack ) )
|
||||
{
|
||||
DyeColor dye = ColourUtils.getStackColour( stack );
|
||||
if( dye != null ) tracker.addColour( dye );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,8 +85,8 @@ public class InventoryMethods implements GenericSource
|
||||
* @link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail includes. More information can be fetched
|
||||
* with {@link #getItemDetail}.
|
||||
*
|
||||
* The table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs` rather than
|
||||
* `ipairs`.
|
||||
* The returned table is sparse, and so empty slots will be `nil` - it is recommended to loop over using `pairs`
|
||||
* rather than `ipairs`.
|
||||
*
|
||||
* @param inventory The current inventory.
|
||||
* @return All items in this inventory.
|
||||
|
@ -191,6 +191,10 @@ public class TurtleAPI implements ILuaAPI {
|
||||
/**
|
||||
* Place a block or item into the world in front of the turtle.
|
||||
*
|
||||
* "Placing" an item allows it to interact with blocks and entities in front of the turtle. For instance, buckets
|
||||
* can pick up and place down fluids, and wheat can be used to breed cows. However, you cannot use {@link #place} to
|
||||
* perform arbitrary block interactions, such as clicking buttons or flipping levers.
|
||||
*
|
||||
* @param args Arguments to place.
|
||||
* @return The turtle command result.
|
||||
* @cc.tparam [opt] string text When placing a sign, set its contents to this text.
|
||||
@ -210,6 +214,7 @@ public class TurtleAPI implements ILuaAPI {
|
||||
* @cc.tparam [opt] string text When placing a sign, set its contents to this text.
|
||||
* @cc.treturn boolean Whether the block could be placed.
|
||||
* @cc.treturn string|nil The reason the block was not placed.
|
||||
* @see #place For more information about placing items.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult placeUp(IArguments args) {
|
||||
@ -224,6 +229,7 @@ public class TurtleAPI implements ILuaAPI {
|
||||
* @cc.tparam [opt] string text When placing a sign, set its contents to this text.
|
||||
* @cc.treturn boolean Whether the block could be placed.
|
||||
* @cc.treturn string|nil The reason the block was not placed.
|
||||
* @see #place For more information about placing items.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult placeDown(IArguments args) {
|
||||
@ -380,16 +386,34 @@ public class TurtleAPI implements ILuaAPI {
|
||||
return this.trackCommand(new TurtleDetectCommand(InteractDirection.DOWN));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the block in front of the turtle is equal to the item in the currently selected slot.
|
||||
*
|
||||
* @return If the block and item are equal.
|
||||
* @cc.treturn boolean If the block and item are equal.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult compare() {
|
||||
return this.trackCommand(new TurtleCompareCommand(InteractDirection.FORWARD));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the block above the turtle is equal to the item in the currently selected slot.
|
||||
*
|
||||
* @return If the block and item are equal.
|
||||
* @cc.treturn boolean If the block and item are equal.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult compareUp() {
|
||||
return this.trackCommand(new TurtleCompareCommand(InteractDirection.UP));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the block below the turtle is equal to the item in the currently selected slot.
|
||||
*
|
||||
* @return If the block and item are equal.
|
||||
* @cc.treturn boolean If the block and item are equal.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult compareDown() {
|
||||
return this.trackCommand(new TurtleCompareCommand(InteractDirection.DOWN));
|
||||
@ -478,11 +502,56 @@ public class TurtleAPI implements ILuaAPI {
|
||||
return this.trackCommand(new TurtleSuckCommand(InteractDirection.DOWN, checkCount(count)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum amount of fuel this turtle currently holds.
|
||||
*
|
||||
* @return The fuel level, or "unlimited".
|
||||
* @cc.treturn[1] number The current amount of fuel a turtle this turtle has.
|
||||
* @cc.treturn[2] "unlimited" If turtles do not consume fuel when moving.
|
||||
* @see #getFuelLimit()
|
||||
* @see #refuel(Optional)
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object getFuelLevel() {
|
||||
return this.turtle.isFuelNeeded() ? this.turtle.getFuelLevel() : "unlimited";
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuel this turtle.
|
||||
*
|
||||
* While most actions a turtle can perform (such as digging or placing blocks), moving consumes fuel from the
|
||||
* turtle's internal buffer. If a turtle has no fuel, it will not move.
|
||||
*
|
||||
* {@link #refuel} refuels the turtle, consuming fuel items (such as coal or lava buckets) from the currently
|
||||
* selected slot and converting them into energy. This finishes once the turtle is fully refuelled or all items have
|
||||
* been consumed.
|
||||
*
|
||||
* @param countA The maximum number of items to consume. One can pass `0` to check if an item is combustable or not.
|
||||
* @return If this turtle could be refuelled.
|
||||
* @throws LuaException If the refuel count is out of range.
|
||||
* @cc.treturn[1] true If the turtle was refuelled.
|
||||
* @cc.treturn[2] false If the turtle was not refuelled.
|
||||
* @cc.treturn[2] string The reason the turtle was not refuelled (
|
||||
* @cc.usage Refuel a turtle from the currently selected slot.
|
||||
* <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
|
||||
public final MethodResult refuel(Optional<Integer> countA) throws LuaException {
|
||||
int count = countA.orElse(Integer.MAX_VALUE);
|
||||
@ -492,11 +561,29 @@ public class TurtleAPI implements ILuaAPI {
|
||||
return this.trackCommand(new TurtleRefuelCommand(count));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the item in the currently selected slot to the item in another slot.
|
||||
*
|
||||
* @param slot The slot to compare to.
|
||||
* @return If the items are the same.
|
||||
* @throws LuaException If the slot is out of range.
|
||||
* @cc.treturn boolean If the two items are equal.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult compareTo(int slot) throws LuaException {
|
||||
return this.trackCommand(new TurtleCompareToCommand(checkSlot(slot)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an item from the selected slot to another one.
|
||||
*
|
||||
* @param slotArg The slot to move this item to.
|
||||
* @param countArg The maximum number of items to move.
|
||||
* @return If the item was moved or not.
|
||||
* @throws LuaException If the slot is out of range.
|
||||
* @throws LuaException If the number of items is out of range.
|
||||
* @cc.treturn boolean If some items were successfully moved.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult transferTo(int slotArg, Optional<Integer> countArg) throws LuaException {
|
||||
int slot = checkSlot(slotArg);
|
||||
@ -515,16 +602,53 @@ public class TurtleAPI implements ILuaAPI {
|
||||
return this.turtle.getSelectedSlot() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum amount of fuel this turtle can hold.
|
||||
*
|
||||
* By default, normal turtles have a limit of 20,000 and advanced turtles of 100,000.
|
||||
*
|
||||
* @return The limit, or "unlimited".
|
||||
* @cc.treturn[1] number The maximum amount of fuel a turtle can hold.
|
||||
* @cc.treturn[2] "unlimited" If turtles do not consume fuel when moving.
|
||||
* @see #getFuelLevel()
|
||||
* @see #refuel(Optional)
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object getFuelLimit() {
|
||||
return this.turtle.isFuelNeeded() ? this.turtle.getFuelLimit() : "unlimited";
|
||||
}
|
||||
|
||||
/**
|
||||
* Equip (or unequip) an item on the left side of this turtle.
|
||||
*
|
||||
* This finds the item in the currently selected slot and attempts to equip it to the left side of the turtle. The
|
||||
* previous upgrade is removed and placed into the turtle's inventory. If there is no item in the slot, the previous
|
||||
* upgrade is removed, but no new one is equipped.
|
||||
*
|
||||
* @return Whether an item was equiped or not.
|
||||
* @cc.treturn[1] true If the item was equipped.
|
||||
* @cc.treturn[2] false If we could not equip the item.
|
||||
* @cc.treturn[2] string The reason equipping this item failed.
|
||||
* @see #equipRight()
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult equipLeft() {
|
||||
return this.trackCommand(new TurtleEquipCommand(TurtleSide.LEFT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Equip (or unequip) an item on the right side of this turtle.
|
||||
*
|
||||
* This finds the item in the currently selected slot and attempts to equip it to the right side of the turtle. The
|
||||
* previous upgrade is removed and placed into the turtle's inventory. If there is no item in the slot, the previous
|
||||
* upgrade is removed, but no new one is equipped.
|
||||
*
|
||||
* @return Whether an item was equiped or not.
|
||||
* @cc.treturn[1] true If the item was equipped.
|
||||
* @cc.treturn[2] false If we could not equip the item.
|
||||
* @cc.treturn[2] string The reason equipping this item failed.
|
||||
* @see #equipRight()
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult equipRight() {
|
||||
return this.trackCommand(new TurtleEquipCommand(TurtleSide.RIGHT));
|
||||
|
@ -119,10 +119,10 @@ public class TurtleMoveCommand implements ITurtleCommand {
|
||||
}
|
||||
|
||||
private static TurtleCommandResult canEnter(TurtlePlayer turtlePlayer, World world, BlockPos position) {
|
||||
if (World.isHeightInvalid(position)) {
|
||||
if (World.isOutOfBuildLimitVertically(position)) {
|
||||
return TurtleCommandResult.failure(position.getY() < 0 ? "Too low to move" : "Too high to move");
|
||||
}
|
||||
if (!World.method_24794(position)) {
|
||||
if (!World.isValid(position)) {
|
||||
return TurtleCommandResult.failure("Cannot leave the world");
|
||||
}
|
||||
|
||||
|
@ -364,7 +364,7 @@ public class TurtlePlaceCommand implements ITurtleCommand {
|
||||
private static boolean canDeployOnBlock(@Nonnull ItemPlacementContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position,
|
||||
Direction side, boolean allowReplaceable, String[] outErrorMessage) {
|
||||
World world = turtle.getWorld();
|
||||
if (!World.method_24794(position) || world.isAir(position) || (context.getStack()
|
||||
if (!World.isValid(position) || world.isAir(position) || (context.getStack()
|
||||
.getItem() instanceof BlockItem && WorldUtil.isLiquidBlock(world,
|
||||
position))) {
|
||||
return false;
|
||||
|
@ -77,7 +77,7 @@ public final class TurtlePlayer extends FakePlayer {
|
||||
private void setState(ITurtleAccess turtle) {
|
||||
if (this.currentScreenHandler != playerScreenHandler) {
|
||||
ComputerCraft.log.warn("Turtle has open container ({})", this.currentScreenHandler);
|
||||
closeCurrentScreen();
|
||||
closeHandledScreen();
|
||||
}
|
||||
|
||||
BlockPos position = turtle.getPosition();
|
||||
@ -91,13 +91,8 @@ public final class TurtlePlayer extends FakePlayer {
|
||||
}
|
||||
|
||||
public static TurtlePlayer get(ITurtleAccess access) {
|
||||
ServerWorld world = (ServerWorld) access.getWorld();
|
||||
if( !(access instanceof TurtleBrain) ) return create( access );
|
||||
|
||||
/*if (!(access instanceof TurtleBrain)) {
|
||||
return new TurtlePlayer(world, access.getOwningPlayer());
|
||||
}*/
|
||||
|
||||
TurtleBrain brain = (TurtleBrain) access;
|
||||
TurtlePlayer player = brain.m_cachedPlayer;
|
||||
if (player == null || player.getGameProfile() != getProfile(access.getOwningPlayer()) || player.getEntityWorld() != access.getWorld()) {
|
||||
|
@ -6,6 +6,8 @@
|
||||
|
||||
package dan200.computercraft.shared.util;
|
||||
|
||||
import net.minecraft.util.DyeColor;
|
||||
|
||||
/**
|
||||
* A reimplementation of the colour system in {@link ArmorDyeRecipe}, but bundled together as an object.
|
||||
*/
|
||||
@ -28,8 +30,15 @@ public class ColourTracker {
|
||||
this.count++;
|
||||
}
|
||||
|
||||
public boolean hasColour() {
|
||||
return this.count > 0;
|
||||
public void addColour( DyeColor dye )
|
||||
{
|
||||
Colour colour = Colour.VALUES[15 - dye.getId()];
|
||||
addColour( colour.getR(), colour.getG(), colour.getB() );
|
||||
}
|
||||
|
||||
public boolean hasColour()
|
||||
{
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public int getColour() {
|
||||
|
@ -14,7 +14,6 @@ import dan200.computercraft.api.turtle.FakePlayer;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.GenericFutureListener;
|
||||
|
||||
import net.minecraft.network.ClientConnection;
|
||||
import net.minecraft.network.NetworkSide;
|
||||
import net.minecraft.network.NetworkState;
|
||||
@ -25,15 +24,12 @@ import net.minecraft.network.packet.c2s.play.BoatPaddleStateC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.BookUpdateC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.ButtonClickC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.ChatMessageC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.ClickWindowC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.ClientStatusC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.ConfirmGuiActionC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.CraftRequestC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.GuiCloseC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.KeepAliveC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.PickFromInventoryC2SPacket;
|
||||
@ -48,7 +44,6 @@ import net.minecraft.network.packet.c2s.play.QueryEntityNbtC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.RenameItemC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.RequestCommandCompletionsC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.ResourcePackStatusC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.SelectVillagerTradeC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.SpectatorTeleportC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.TeleportConfirmC2SPacket;
|
||||
import net.minecraft.network.packet.c2s.play.UpdateBeaconC2SPacket;
|
||||
@ -127,10 +122,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler {
|
||||
public void onJigsawUpdate(@Nonnull UpdateJigsawC2SPacket packet) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVillagerTradeSelect(@Nonnull SelectVillagerTradeC2SPacket packet) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBookUpdate(@Nonnull BookUpdateC2SPacket packet) {
|
||||
}
|
||||
@ -207,14 +198,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler {
|
||||
public void onClientStatus(@Nonnull ClientStatusC2SPacket packet) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuiClose(@Nonnull GuiCloseC2SPacket packet) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClickWindow(@Nonnull ClickWindowC2SPacket packet) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCraftRequest(@Nonnull CraftRequestC2SPacket packet) {
|
||||
}
|
||||
@ -227,10 +210,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler {
|
||||
public void onCreativeInventoryAction(@Nonnull CreativeInventoryActionC2SPacket packet) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfirmTransaction(@Nonnull ConfirmGuiActionC2SPacket packet) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSignUpdate(@Nonnull UpdateSignC2SPacket packet) {
|
||||
}
|
||||
@ -309,10 +288,6 @@ public class FakeNetHandler extends ServerPlayNetworkHandler {
|
||||
this.closeReason = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupEncryption(@Nonnull SecretKey key) {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public PacketListener getPacketListener() {
|
||||
|
@ -40,7 +40,7 @@ public final class WorldUtil {
|
||||
.makeMap();
|
||||
|
||||
public static boolean isLiquidBlock(World world, BlockPos pos) {
|
||||
if (!World.method_24794(pos)) {
|
||||
if (!World.isValid(pos)) {
|
||||
return false;
|
||||
}
|
||||
return world.getBlockState(pos)
|
||||
|
@ -38,5 +38,21 @@
|
||||
"upgrade.computercraft.speaker.adjective": "(Alto-Falante)",
|
||||
"chat.computercraft.wired_modem.peripheral_connected": "Periférico \"%s\" conectado à rede",
|
||||
"chat.computercraft.wired_modem.peripheral_disconnected": "Periférico \"%s\" desconectado da rede",
|
||||
"gui.computercraft.tooltip.copy": "Copiar para a área de transferência"
|
||||
"gui.computercraft.tooltip.copy": "Copiar para a área de transferência",
|
||||
"commands.computercraft.tp.synopsis": "Teleprota para um computador específico.",
|
||||
"commands.computercraft.turn_on.done": "Ligou %s/%s computadores",
|
||||
"commands.computercraft.turn_on.desc": "Liga os computadores em escuta. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").",
|
||||
"commands.computercraft.turn_on.synopsis": "Liga computadores remotamente.",
|
||||
"commands.computercraft.shutdown.done": "Desliga %s/%s computadores",
|
||||
"commands.computercraft.shutdown.desc": "Desliga os computadores em escuta ou todos caso não tenha sido especificado. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").",
|
||||
"commands.computercraft.shutdown.synopsis": "Desliga computadores remotamente.",
|
||||
"commands.computercraft.dump.action": "Ver mais informação sobre este computador",
|
||||
"commands.computercraft.dump.desc": "Mostra o status de todos os computadores ou uma informação específica sobre um computador. Você pode especificar o id de instância do computador (ex.: 123), id do computador (ex.: #123) ou o rótulo (ex.: \"@MeuComputador\").",
|
||||
"commands.computercraft.dump.synopsis": "Mostra status de computadores.",
|
||||
"commands.computercraft.help.no_command": "Comando '%s' não existe",
|
||||
"commands.computercraft.help.no_children": "%s não tem sub-comandos",
|
||||
"commands.computercraft.help.desc": "Mostra essa mensagem de ajuda",
|
||||
"commands.computercraft.help.synopsis": "Providencia ajuda para um comando específico",
|
||||
"commands.computercraft.desc": "O comando /computercraft providencia várias ferramentas de depuração e administração para controle e interação com computadores.",
|
||||
"commands.computercraft.synopsis": "Vários comandos para controlar computadores."
|
||||
}
|
||||
|
@ -1,11 +1,34 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"name": "main",
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{ "type": "minecraft:dynamic", "name": "computercraft:computer" }
|
||||
]
|
||||
"type": "minecraft:dynamic",
|
||||
"name": "computercraft:computer"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "minecraft:alternative",
|
||||
"terms": [
|
||||
{
|
||||
"condition": "computercraft:block_named"
|
||||
},
|
||||
{
|
||||
"condition": "computercraft:has_id"
|
||||
},
|
||||
{
|
||||
"condition": "minecraft:inverted",
|
||||
"term": {
|
||||
"condition": "computercraft:player_creative"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -83,9 +83,9 @@ end
|
||||
|
||||
--- Tries to retrieve the computer or turtles own location.
|
||||
--
|
||||
-- @tparam[opt] number timeout The maximum time taken to establish our
|
||||
-- position. Defaults to 2 seconds if not specified.
|
||||
-- @tparam[opt] boolean debug Print debugging messages
|
||||
-- @tparam[opt=2] number timeout The maximum time in seconds taken to establish our
|
||||
-- position.
|
||||
-- @tparam[opt=false] boolean debug Print debugging messages
|
||||
-- @treturn[1] number This computer's `x` position.
|
||||
-- @treturn[1] number This computer's `y` position.
|
||||
-- @treturn[1] number This computer's `z` position.
|
||||
|
@ -137,6 +137,15 @@ handleMetatable = {
|
||||
return handle.seek(whence, offset)
|
||||
end,
|
||||
|
||||
--[[- Sets the buffering mode for an output file.
|
||||
|
||||
This has no effect under ComputerCraft, and exists with compatility
|
||||
with base Lua.
|
||||
@tparam string mode The buffering mode.
|
||||
@tparam[opt] number size The size of the buffer.
|
||||
@see file:setvbuf Lua's documentation for `setvbuf`.
|
||||
@deprecated This has no effect in CC.
|
||||
]]
|
||||
setvbuf = function(self, mode, size) end,
|
||||
|
||||
--- Write one or more values to the file
|
||||
|
@ -132,7 +132,17 @@ function drawLine(startX, startY, endX, endY, colour)
|
||||
return
|
||||
end
|
||||
|
||||
local minX, maxX, minY, maxY = sortCoords(startX, startY, endX, endY)
|
||||
local minX = math.min(startX, endX)
|
||||
local maxX, minY, maxY
|
||||
if minX == startX then
|
||||
minY = startY
|
||||
maxX = endX
|
||||
maxY = endY
|
||||
else
|
||||
minY = endY
|
||||
maxX = startX
|
||||
maxY = startY
|
||||
end
|
||||
|
||||
-- TODO: clip to screen rectangle?
|
||||
|
||||
|
@ -161,7 +161,9 @@ end
|
||||
-- @tparam string name The name of the peripheral to wrap.
|
||||
-- @treturn table|nil The table containing the peripheral's methods, or `nil` if
|
||||
-- there is no peripheral present with the given name.
|
||||
-- @usage peripheral.wrap("top").open(1)
|
||||
-- @usage Open the modem on the top of this computer.
|
||||
--
|
||||
-- peripheral.wrap("top").open(1)
|
||||
function wrap(name)
|
||||
expect(1, name, "string")
|
||||
|
||||
@ -183,16 +185,25 @@ function wrap(name)
|
||||
return result
|
||||
end
|
||||
|
||||
--- Find all peripherals of a specific type, and return the
|
||||
-- @{peripheral.wrap|wrapped} peripherals.
|
||||
--
|
||||
-- @tparam string ty The type of peripheral to look for.
|
||||
-- @tparam[opt] function(name:string, wrapped:table):boolean filter A
|
||||
-- filter function, which takes the peripheral's name and wrapped table
|
||||
-- and returns if it should be included in the result.
|
||||
-- @treturn table... 0 or more wrapped peripherals matching the given filters.
|
||||
-- @usage { peripheral.find("monitor") }
|
||||
-- @usage peripheral.find("modem", rednet.open)
|
||||
--[[- Find all peripherals of a specific type, and return the
|
||||
@{peripheral.wrap|wrapped} peripherals.
|
||||
|
||||
@tparam string ty The type of peripheral to look for.
|
||||
@tparam[opt] function(name:string, wrapped:table):boolean filter A
|
||||
filter function, which takes the peripheral's name and wrapped table
|
||||
and returns if it should be included in the result.
|
||||
@treturn table... 0 or more wrapped peripherals matching the given filters.
|
||||
@usage Find all monitors and store them in a table, writing "Hello" on each one.
|
||||
|
||||
local monitors = { peripheral.find("monitor") }
|
||||
for _, monitor in pairs(monitors) do
|
||||
monitor.write("Hello")
|
||||
end
|
||||
|
||||
@usage This abuses the `filter` argument to call @{rednet.open} on every modem.
|
||||
|
||||
peripheral.find("modem", rednet.open)
|
||||
]]
|
||||
function find(ty, filter)
|
||||
expect(1, ty, "string")
|
||||
expect(2, filter, "function", "nil")
|
||||
|
@ -381,6 +381,7 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle)
|
||||
local sArrayResult = "["
|
||||
local nObjectSize = 0
|
||||
local nArraySize = 0
|
||||
local largestArrayIndex = 0
|
||||
for k, v in pairs(t) do
|
||||
if type(k) == "string" then
|
||||
local sEntry
|
||||
@ -395,10 +396,17 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle)
|
||||
sObjectResult = sObjectResult .. "," .. sEntry
|
||||
end
|
||||
nObjectSize = nObjectSize + 1
|
||||
elseif type(k) == "number" and k > largestArrayIndex then --the largest index is kept to avoid losing half the array if there is any single nil in that array
|
||||
largestArrayIndex = k
|
||||
end
|
||||
end
|
||||
for _, v in ipairs(t) do
|
||||
local sEntry = serializeJSONImpl(v, tTracking, bNBTStyle)
|
||||
for k = 1, largestArrayIndex, 1 do --the array is read up to the very last valid array index, ipairs() would stop at the first nil value and we would lose any data after.
|
||||
local sEntry
|
||||
if t[k] == nil then --if the array is nil at index k the value is "null" as to keep the unused indexes in between used ones.
|
||||
sEntry = "null"
|
||||
else -- if the array index does not point to a nil we serialise it's content.
|
||||
sEntry = serializeJSONImpl(t[k], tTracking, bNBTStyle)
|
||||
end
|
||||
if nArraySize == 0 then
|
||||
sArrayResult = sArrayResult .. sEntry
|
||||
else
|
||||
|
@ -10,6 +10,7 @@ end
|
||||
--
|
||||
-- Generally you should not need to use this table - it only exists for
|
||||
-- backwards compatibility reasons.
|
||||
-- @deprecated
|
||||
native = turtle.native or turtle
|
||||
|
||||
local function addCraftMethod(object)
|
||||
|
@ -1,3 +1,28 @@
|
||||
New features in CC: Restitched 1.95.3
|
||||
|
||||
Several bug fixes:
|
||||
* Correctly serialise sparse arrays into JSON (livegamer999)
|
||||
* Fix rs.getBundledInput returning the output instead (SkyTheCodeMaster)
|
||||
* Programs run via edit are now a little better behaved (Wojbie)
|
||||
* Add User-Agent to a websocket's headers.
|
||||
|
||||
# New features in CC: Restitched 1.95.2
|
||||
|
||||
* Add `isReadOnly` to `fs.attributes` (Lupus590)
|
||||
* Many more programs now support numpad enter (Wojbie)
|
||||
|
||||
Several bug fixes:
|
||||
* Fix some commands failing to parse on dedicated servers.
|
||||
* Hopefully improve edit's behaviour with AltGr on some European keyboards.
|
||||
* Prevent files being usable after their mount was removed.
|
||||
* Fix the `id` program crashing on non-disk items (Wojbie).
|
||||
|
||||
# New features in CC: Restitched 1.95.1
|
||||
|
||||
Several bug fixes:
|
||||
* Command computers now drop items again.
|
||||
* Restore crafting of disks with dyes.
|
||||
|
||||
# New features in CC: Restitched 1.95.0
|
||||
|
||||
* Optimise the paint program's initial render.
|
||||
|
@ -1,23 +1,9 @@
|
||||
New features in CC: Restitched 1.95.0
|
||||
New features in CC: Restitched 1.95.3
|
||||
|
||||
* Optimise the paint program's initial render.
|
||||
* Several documentation improvments (Gibbo3771, MCJack123).
|
||||
* `fs.combine` now accepts multiple arguments.
|
||||
* Add a setting (`bios.strict_globals`) to error when accidentally declaring a global. (Lupus590).
|
||||
* Add an improved help viewer which allows scrolling up and down (MCJack123).
|
||||
* Add `cc.strings` module, with utilities for wrapping text (Lupus590).
|
||||
* The `clear` program now allows resetting the palette too (Luca0208).
|
||||
|
||||
And several bug fixes:
|
||||
* Fix memory leak in generic peripherals.
|
||||
* Fix crash when a turtle is broken while being ticked.
|
||||
* `textutils.*tabulate` now accepts strings _or_ numbers.
|
||||
* We now deny _all_ local IPs, using the magic `$private` host. Previously the IPv6 loopback interface was not blocked.
|
||||
* Fix crash when rendering monitors if the block has not yet been synced. You will need to regenerate the config file to apply this change.
|
||||
* `read` now supports numpad enter (TheWireLord)
|
||||
* Correctly handle HTTP redirects to URLs containing escape characters.
|
||||
* Fix integer overflow in `os.epoch`.
|
||||
* Allow using pickaxes (and other items) for turtle upgrades which have mod-specific NBT.
|
||||
* Fix duplicate turtle/pocket upgrade recipes appearing in JEI.
|
||||
Several bug fixes:
|
||||
* Correctly serialise sparse arrays into JSON (livegamer999)
|
||||
* Fix rs.getBundledInput returning the output instead (SkyTheCodeMaster)
|
||||
* Programs run via edit are now a little better behaved (Wojbie)
|
||||
* Add User-Agent to a websocket's headers.
|
||||
|
||||
Type "help changelog" to see the full version history.
|
||||
|
@ -1,23 +1,28 @@
|
||||
--- Provides a "pretty printer", for rendering data structures in an
|
||||
-- aesthetically pleasing manner.
|
||||
--
|
||||
-- In order to display something using @{cc.pretty}, you build up a series of
|
||||
-- @{Doc|documents}. These behave a little bit like strings; you can concatenate
|
||||
-- them together and then print them to the screen.
|
||||
--
|
||||
-- However, documents also allow you to control how they should be printed. There
|
||||
-- are several functions (such as @{nest} and @{group}) which allow you to control
|
||||
-- the "layout" of the document. When you come to display the document, the 'best'
|
||||
-- (most compact) layout is used.
|
||||
--
|
||||
-- @module cc.pretty
|
||||
-- @usage Print a table to the terminal
|
||||
-- local pretty = require "cc.pretty"
|
||||
-- pretty.print(pretty.pretty({ 1, 2, 3 }))
|
||||
--
|
||||
-- @usage Build a custom document and display it
|
||||
-- local pretty = require "cc.pretty"
|
||||
-- pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world")))
|
||||
--[[- Provides a "pretty printer", for rendering data structures in an
|
||||
aesthetically pleasing manner.
|
||||
|
||||
In order to display something using @{cc.pretty}, you build up a series of
|
||||
@{Doc|documents}. These behave a little bit like strings; you can concatenate
|
||||
them together and then print them to the screen.
|
||||
|
||||
However, documents also allow you to control how they should be printed. There
|
||||
are several functions (such as @{nest} and @{group}) which allow you to control
|
||||
the "layout" of the document. When you come to display the document, the 'best'
|
||||
(most compact) layout is used.
|
||||
|
||||
The structure of this module is based on [A Prettier Printer][prettier].
|
||||
|
||||
[prettier]: https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf "A Prettier Printer"
|
||||
|
||||
@module cc.pretty
|
||||
@usage Print a table to the terminal
|
||||
local pretty = require "cc.pretty"
|
||||
pretty.print(pretty.pretty({ 1, 2, 3 }))
|
||||
|
||||
@usage Build a custom document and display it
|
||||
local pretty = require "cc.pretty"
|
||||
pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world")))
|
||||
]]
|
||||
|
||||
local expect = require "cc.expect"
|
||||
local expect, field = expect.expect, expect.field
|
||||
|
@ -5,17 +5,24 @@
|
||||
|
||||
local expect = require "cc.expect".expect
|
||||
|
||||
--- Wraps a block of text, so that each line fits within the given width.
|
||||
--
|
||||
-- This may be useful if you want to wrap text before displaying it to a
|
||||
-- @{monitor} or @{printer} without using @{_G.print|print}.
|
||||
--
|
||||
-- @tparam string text The string to wrap.
|
||||
-- @tparam[opt] number width The width to constrain to, defaults to the width of
|
||||
-- the terminal.
|
||||
--
|
||||
-- @treturn { string... } The wrapped input string.
|
||||
-- @usage require "cc.strings".wrap("This is a long piece of text", 10)
|
||||
--[[- Wraps a block of text, so that each line fits within the given width.
|
||||
|
||||
This may be useful if you want to wrap text before displaying it to a
|
||||
@{monitor} or @{printer} without using @{_G.print|print}.
|
||||
|
||||
@tparam string text The string to wrap.
|
||||
@tparam[opt] number width The width to constrain to, defaults to the width of
|
||||
the terminal.
|
||||
@treturn { string... } The wrapped input string as a list of lines.
|
||||
@usage Wrap a string and write it to the terminal.
|
||||
|
||||
term.clear()
|
||||
local lines = require "cc.strings".wrap("This is a long piece of text", 10)
|
||||
for i = 1, #lines do
|
||||
term.setCursorPos(1, i)
|
||||
term.write(lines[i])
|
||||
end
|
||||
]]
|
||||
local function wrap(text, width)
|
||||
expect(1, text, "string")
|
||||
expect(2, width, "number", "nil")
|
||||
|
@ -1,4 +1,4 @@
|
||||
Please report bugs at https://github.com/Merith-TK/cc-restiched. Thanks!
|
||||
Please report bugs at https://github.com/Merith-TK/cc-restitched/issues. Thanks!
|
||||
View the documentation at https://tweaked.cc
|
||||
Show off your programs or ask for help at our forum: https://forums.computercraft.cc
|
||||
You can disable these messages by running "set motd.enable false".
|
||||
|
@ -47,6 +47,30 @@ else
|
||||
stringColour = colours.white
|
||||
end
|
||||
|
||||
local runHandler = [[multishell.setTitle(multishell.getCurrent(), %q)
|
||||
local current = term.current()
|
||||
local ok, err = load(%q, %q, nil, _ENV)
|
||||
if ok then ok, err = pcall(ok, ...) end
|
||||
term.redirect(current)
|
||||
term.setTextColor(term.isColour() and colours.yellow or colours.white)
|
||||
term.setBackgroundColor(colours.black)
|
||||
term.setCursorBlink(false)
|
||||
local _, y = term.getCursorPos()
|
||||
local _, h = term.getSize()
|
||||
if not ok then
|
||||
printError(err)
|
||||
end
|
||||
if ok and y >= h then
|
||||
term.scroll(1)
|
||||
end
|
||||
term.setCursorPos(1, h)
|
||||
if ok then
|
||||
write("Program finished. ")
|
||||
end
|
||||
write("Press any key to continue")
|
||||
os.pullEvent('key')
|
||||
]]
|
||||
|
||||
-- Menus
|
||||
local bMenu = false
|
||||
local nMenuItem = 1
|
||||
@ -89,7 +113,7 @@ local function load(_sPath)
|
||||
end
|
||||
end
|
||||
|
||||
local function save(_sPath)
|
||||
local function save(_sPath, fWrite)
|
||||
-- Create intervening folder
|
||||
local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len())
|
||||
if not fs.exists(sDir) then
|
||||
@ -101,8 +125,8 @@ local function save(_sPath)
|
||||
local function innerSave()
|
||||
file, fileerr = fs.open(_sPath, "w")
|
||||
if file then
|
||||
for _, sLine in ipairs(tLines) do
|
||||
file.write(sLine .. "\n")
|
||||
if file then
|
||||
fWrite(file)
|
||||
end
|
||||
else
|
||||
error("Failed to open " .. _sPath)
|
||||
@ -293,7 +317,11 @@ local tMenuFuncs = {
|
||||
if bReadOnly then
|
||||
sStatus = "Access denied"
|
||||
else
|
||||
local ok, _, fileerr = save(sPath)
|
||||
local ok, _, fileerr = save(sPath, function(file)
|
||||
for _, sLine in ipairs(tLines) do
|
||||
file.write(sLine .. "\n")
|
||||
end
|
||||
end)
|
||||
if ok then
|
||||
sStatus = "Saved to " .. sPath
|
||||
else
|
||||
@ -390,8 +418,18 @@ local tMenuFuncs = {
|
||||
bRunning = false
|
||||
end,
|
||||
Run = function()
|
||||
local sTempPath = "/.temp"
|
||||
local ok = save(sTempPath)
|
||||
local sTitle = fs.getName(sPath)
|
||||
if sTitle:sub(-4) == ".lua" then
|
||||
sTitle = sTitle:sub(1, -5)
|
||||
end
|
||||
local sTempPath = bReadOnly and ".temp." .. sTitle or fs.combine(fs.getDir(sPath), ".temp." .. sTitle)
|
||||
if fs.exists(sTempPath) then
|
||||
sStatus = "Error saving to " .. sTempPath
|
||||
return
|
||||
end
|
||||
local ok = save(sTempPath, function(file)
|
||||
file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@" .. fs.getName(sPath)))
|
||||
end)
|
||||
if ok then
|
||||
local nTask = shell.openTab(sTempPath)
|
||||
if nTask then
|
||||
@ -667,8 +705,8 @@ while bRunning do
|
||||
end
|
||||
end
|
||||
|
||||
elseif param == keys.enter then
|
||||
-- Enter
|
||||
elseif param == keys.enter or param == keys.numPadEnter then
|
||||
-- Enter/Numpad Enter
|
||||
if not bMenu and not bReadOnly then
|
||||
-- Newline
|
||||
local sLine = tLines[y]
|
||||
@ -687,7 +725,7 @@ while bRunning do
|
||||
|
||||
end
|
||||
|
||||
elseif param == keys.leftCtrl or param == keys.rightCtrl or param == keys.rightAlt then
|
||||
elseif param == keys.leftCtrl or param == keys.rightCtrl then
|
||||
-- Menu toggle
|
||||
bMenu = not bMenu
|
||||
if bMenu then
|
||||
@ -696,7 +734,12 @@ while bRunning do
|
||||
term.setCursorBlink(true)
|
||||
end
|
||||
redrawMenu()
|
||||
|
||||
elseif param == keys.rightAlt then
|
||||
if bMenu then
|
||||
bMenu = false
|
||||
term.setCursorBlink(true)
|
||||
redrawMenu()
|
||||
end
|
||||
end
|
||||
|
||||
elseif sEvent == "char" then
|
||||
@ -758,6 +801,7 @@ while bRunning do
|
||||
end
|
||||
else
|
||||
bMenu = false
|
||||
term.setCursorBlink(true)
|
||||
redrawMenu()
|
||||
end
|
||||
end
|
||||
|
@ -349,7 +349,7 @@ local function accessMenu()
|
||||
selection = #mChoices
|
||||
end
|
||||
|
||||
elseif key == keys.enter then
|
||||
elseif key == keys.enter or key == keys.numPadEnter then
|
||||
-- Select an option
|
||||
return menu_choices[mChoices[selection]]()
|
||||
elseif key == keys.leftCtrl or keys == keys.rightCtrl then
|
||||
|
@ -199,8 +199,8 @@ while true do
|
||||
drawMenu()
|
||||
drawFrontend()
|
||||
end
|
||||
elseif key == keys.enter then
|
||||
-- Enter
|
||||
elseif key == keys.enter or key == keys.numPadEnter then
|
||||
-- Enter/Numpad Enter
|
||||
break
|
||||
end
|
||||
end
|
||||
|
@ -13,16 +13,30 @@ if sDrive == nil then
|
||||
end
|
||||
|
||||
else
|
||||
local bData = disk.hasData(sDrive)
|
||||
if not bData then
|
||||
if disk.hasAudio(sDrive) then
|
||||
local title = disk.getAudioTitle(sDrive)
|
||||
if title then
|
||||
print("Has audio track \"" .. title .. "\"")
|
||||
else
|
||||
print("Has untitled audio")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if not disk.hasData(sDrive) then
|
||||
print("No disk in drive " .. sDrive)
|
||||
return
|
||||
end
|
||||
|
||||
print("The disk is #" .. disk.getID(sDrive))
|
||||
local id = disk.getID(sDrive)
|
||||
if id then
|
||||
print("The disk is #" .. id)
|
||||
else
|
||||
print("Non-disk data source")
|
||||
end
|
||||
|
||||
local label = disk.getLabel(sDrive)
|
||||
if label then
|
||||
print("The disk is labelled \"" .. label .. "\"")
|
||||
print("Labelled \"" .. label .. "\"")
|
||||
end
|
||||
end
|
||||
|
@ -546,7 +546,7 @@ local function playGame()
|
||||
msgBox("Game Over!")
|
||||
while true do
|
||||
local _, k = os.pullEvent("key")
|
||||
if k == keys.space or k == keys.enter then
|
||||
if k == keys.space or k == keys.enter or k == keys.numPadEnter then
|
||||
break
|
||||
end
|
||||
end
|
||||
@ -627,7 +627,7 @@ local function runMenu()
|
||||
elseif key == keys.down or key == keys.s then
|
||||
selected = selected % 2 + 1
|
||||
drawMenu()
|
||||
elseif key == keys.enter or key == keys.space then
|
||||
elseif key == keys.enter or key == keys.numPadEnter or key == keys.space then
|
||||
break --begin play!
|
||||
end
|
||||
end
|
||||
|
@ -14,6 +14,6 @@
|
||||
],
|
||||
"result": {
|
||||
"item": "computercraft:disk",
|
||||
"nbt": "{color:1118481}"
|
||||
"nbt": "{Color:1118481}"
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@
|
||||
],
|
||||
"result": {
|
||||
"item": "computercraft:disk",
|
||||
"nbt": "{color:15905484}"
|
||||
"nbt": "{Color:15905484}"
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@
|
||||
],
|
||||
"result": {
|
||||
"item": "computercraft:disk",
|
||||
"nbt": "{color:8375321}"
|
||||
"nbt": "{Color:8375321}"
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@
|
||||
],
|
||||
"result": {
|
||||
"item": "computercraft:disk",
|
||||
"nbt": "{color:14605932}"
|
||||
"nbt": "{Color:14605932}"
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@
|
||||
],
|
||||
"result": {
|
||||
"item": "computercraft:disk",
|
||||
"nbt": "{color:10072818}"
|
||||
"nbt": "{Color:10072818}"
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@
|
||||
],
|
||||
"result": {
|
||||
"item": "computercraft:disk",
|
||||
"nbt": "{color:15040472}"
|
||||
"nbt": "{Color:15040472}"
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@
|
||||
],
|
||||
"result": {
|
||||
"item": "computercraft:disk",
|
||||
"nbt": "{color:15905331}"
|
||||
"nbt": "{Color:15905331}"
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@
|
||||
],
|
||||
"result": {
|
||||
"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
Loading…
x
Reference in New Issue
Block a user