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