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