mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-24 18:37:38 +00:00 
			
		
		
		
	Merge branch 'master' into mc-1.14.x
This also deletes display list support - MC 1.14 now requires VBOs to be supported in some capacity.
This commit is contained in:
		| @@ -14,5 +14,9 @@ trim_trailing_whitespace = false | ||||
| [*.sexp] | ||||
| indent_size = 2 | ||||
|  | ||||
| [*.yml] | ||||
| indent_size = 2 | ||||
|  | ||||
|  | ||||
| [*.properties] | ||||
| insert_final_newline = false | ||||
|   | ||||
							
								
								
									
										13
									
								
								.github/workflows/main-ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/workflows/main-ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -15,6 +15,14 @@ jobs: | ||||
|       with: | ||||
|         java-version: 1.8 | ||||
|  | ||||
|     - name: Cache gradle dependencies | ||||
|       uses: actions/cache@v1 | ||||
|       with: | ||||
|         path: ~/.gradle/caches | ||||
|         key: ${{ runner.os }}-gradle-${{ hashFiles('gradle.properties') }} | ||||
|         restore-keys: | | ||||
|           ${{ runner.os }}-gradle- | ||||
|  | ||||
|     - name: Build with Gradle | ||||
|       run: ./gradlew build --no-daemon || ./gradlew build --no-daemon | ||||
|  | ||||
| @@ -34,6 +42,9 @@ jobs: | ||||
|     - name: Lint Lua code | ||||
|       run: | | ||||
|         test -d bin || mkdir bin | ||||
|         test -f bin/illuaminate || wget -q -Obin/illuaminate https://squiddev.cc/illuaminate/bin/illuaminate | ||||
|         test -f bin/illuaminate || wget -q -Obin/illuaminate https://squiddev.cc/illuaminate/linux-x86-64/illuaminate | ||||
|         chmod +x bin/illuaminate | ||||
|         bin/illuaminate lint | ||||
|  | ||||
|     - name: Check whitespace | ||||
|       run: python3 tools/check-lines.py | ||||
|   | ||||
							
								
								
									
										16
									
								
								.github/workflows/make-doc.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										16
									
								
								.github/workflows/make-doc.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| set -eu | ||||
|  | ||||
| DEST="${GITHUB_REF#refs/*/}" | ||||
| echo "Uploading docs to https://tweaked.cc/$DEST" | ||||
|  | ||||
| # Setup ssh key | ||||
| mkdir -p "$HOME/.ssh/" | ||||
| echo "$SSH_KEY" > "$HOME/.ssh/key" | ||||
| chmod 600 "$HOME/.ssh/key" | ||||
|  | ||||
| # And upload | ||||
| rsync -avc -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no -p $SSH_PORT" \ | ||||
|       "$GITHUB_WORKSPACE/doc/" \ | ||||
|       "$SSH_USER@$SSH_HOST:/var/www/tweaked.cc/$DEST" | ||||
							
								
								
									
										29
									
								
								.github/workflows/make-doc.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.github/workflows/make-doc.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| name: Build documentation | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master ] | ||||
|     tags: | ||||
|  | ||||
| jobs: | ||||
|   make_doc: | ||||
|     name: Build | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|     - uses: actions/checkout@v1 | ||||
|  | ||||
|     - name: Build documentation | ||||
|       run: | | ||||
|         test -d bin || mkdir bin | ||||
|         test -f bin/illuaminate || wget -q -Obin/illuaminate https://squiddev.cc/illuaminate/linux-x86-64/illuaminate | ||||
|         chmod +x bin/illuaminate | ||||
|         bin/illuaminate doc-gen | ||||
|  | ||||
|     - name: Upload documentation | ||||
|       run: .github/workflows/make-doc.sh 2> /dev/null | ||||
|       env: | ||||
|         SSH_KEY:  ${{ secrets.SSH_KEY  }} | ||||
|         SSH_USER: ${{ secrets.SSH_USER }} | ||||
|         SSH_HOST: ${{ secrets.SSH_HOST }} | ||||
|         SSH_PORT: ${{ secrets.SSH_PORT }} | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,8 @@ | ||||
| /logs | ||||
| /build | ||||
| /out | ||||
| /doc/**/*.html | ||||
| /doc/index.json | ||||
|  | ||||
| # Runtime directories | ||||
| /run | ||||
|   | ||||
							
								
								
									
										36
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| # Contributing to CC: Tweaked | ||||
| As with many open source projects, CC: Tweaked thrives on contributions from other people! This document (hopefully) | ||||
| provides an introduction as to how to get started in helping out. | ||||
|  | ||||
| If you've any other questions, [just ask the community][community] or [open an issue][new-issue]. | ||||
|  | ||||
| ## Reporting issues | ||||
| If you have a bug, suggestion, or other feedback, the best thing to do is [file an issue][new-issue]. When doing so, | ||||
| do use the issue templates - they provide a useful hint on what information to provide. | ||||
|  | ||||
| ## Developing | ||||
| In order to develop CC: Tweaked, you'll need to download the source code and then run it. This is a pretty simple | ||||
| process. | ||||
|  | ||||
|  - **Clone the repository:** `git clone https://github.com/SquidDev-CC/CC-Tweaked.git && cd CC-Tweaked` | ||||
|  - **Setup Forge:** `./gradlew build` | ||||
|  - **Run Minecraft:** `./gradlew runClient` (or run the `GradleStart` class from your IDE). | ||||
|  | ||||
| If you want to run CC:T in a normal Minecraft instance, run `./gradlew build` and copy the `.jar` from `build/libs`. | ||||
| These commands may take a few minutes to run the first time, as the environment is set up, but should be much faster | ||||
| afterwards. | ||||
|  | ||||
| ### Code linters | ||||
| CC: Tweaked uses a couple of "linters" on its source code, to enforce a consistent style across the project. While these | ||||
| are run whenever you submit a PR, it's often useful to run this before committing. | ||||
|  | ||||
|  - **[Checkstyle]:** Checks Java code to ensure it is consistently formatted. This can be run with `./gradlew build` or | ||||
|    `./gradle check`. | ||||
|  - **[illuaminate]:** Checks Lua code for semantic and styleistic issues. See [the usage section][illuaminate-usage] for | ||||
|    how to download and run it. | ||||
|  | ||||
| [new-issue]: https://github.com/SquidDev-CC/CC-Tweaked/issues/new/choose "Create a new issue" | ||||
| [community]: README.md#Community "Get in touch with the community." | ||||
| [checkstyle]: https://checkstyle.org/ | ||||
| [illuaminate]: https://github.com/SquidDev/illuaminate/ | ||||
| [illuaminate-usage]: https://github.com/SquidDev/illuaminate/blob/master/README.md#usage | ||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							| @@ -37,20 +37,14 @@ several features have been included, such as full block modems, the Cobalt runti | ||||
| computers. | ||||
|  | ||||
| ## Contributing | ||||
| Any contribution is welcome, be that using the mod, reporting bugs or contributing code. In order to start helping | ||||
| develop CC:T, you'll need to follow these steps: | ||||
|  | ||||
|  - **Clone the repository:** `git clone https://github.com/SquidDev-CC/CC-Tweaked.git && cd CC-Tweaked` | ||||
|  - **Setup Forge:** `./gradlew build` | ||||
|  - **Test your changes:** `./gradlew runClient` (or run the `GradleStart` class from your IDE). | ||||
|  | ||||
| If you want to run CC:T in a normal Minecraft instance, run `./gradlew build` and copy the `.jar` from `build/libs`. | ||||
| Any contribution is welcome, be that using the mod, reporting bugs or contributing code. If you want to get started | ||||
| developing the mod, [check out the instructions here](CONTRIBUTING.md#developing). | ||||
|  | ||||
| ## Community | ||||
| If you need help getting started with CC: Tweaked, want to show off your latest project, or just want to chat about | ||||
| ComputerCraft we have a [forum](https://forums.computercraft.cc/) and [Discord guild](https://discord.gg/H2UyJXe)! | ||||
| There's also a fairly populated, albeit quiet [IRC channel](http://webchat.esper.net/?channels=#computercraft), if | ||||
| that's more your cup of tea. | ||||
| ComputerCraft we have a [forum](https://forums.computercraft.cc/) and [Discord guild](https://discord.computercraft.cc)! | ||||
| There's also a fairly populated, albeit quiet [IRC channel](http://webchat.esper.net/?channels=computercraft), if that's | ||||
| more your cup of tea. | ||||
|  | ||||
| I'd generally recommend you don't contact me directly (email, DM, etc...) unless absolutely necessary (i.e. in order to | ||||
| report exploits). You'll get a far quicker response if you ask the whole community! | ||||
|   | ||||
| @@ -104,7 +104,7 @@ dependencies { | ||||
|  | ||||
|     runtimeOnly fg.deobf("mezz.jei:jei-1.14.4:6.0.0.27") | ||||
|  | ||||
|     shade 'org.squiddev:Cobalt:0.5.0-SNAPSHOT' | ||||
|     shade 'org.squiddev:Cobalt:0.5.1-SNAPSHOT' | ||||
|  | ||||
|     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' | ||||
|     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' | ||||
|   | ||||
							
								
								
									
										11
									
								
								doc/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								doc/index.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| #  [](https://minecraft.curseforge.com/projects/cc-tweaked "Download CC: Tweaked on CurseForge") | ||||
|  | ||||
| CC: Tweaked is a fork of [ComputerCraft], adding programmable computers, turtles and more to Minecraft. | ||||
|  | ||||
| This website contains documentation for all Lua libraries and APIs from the latest version of CC: Tweaked. This | ||||
| documentation is still in development, so will most likely be incomplete. If you've found something you think is wrong, | ||||
| or would like to help out [please get in touch on GitHub][gh]. | ||||
|  | ||||
| [bug]: https://github.com/SquidDev-CC/CC-Tweaked/issues/new/choose | ||||
| [computercraft]: https://github.com/dan200/ComputerCraft "ComputerCraft on GitHub" | ||||
| [gh]: https://github.com/SquidDev-CC/CC-Tweaked "CC:Tweaked on GitHub" | ||||
| Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										6
									
								
								doc/stub/commands.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								doc/stub/commands.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| function exec(command) end | ||||
| function execAsync(commad) end | ||||
| function list() end | ||||
| function getBlockPosition() end | ||||
| function getBlockInfos(min_x, min_y, min_z, max_x, max_y, max_z) end | ||||
| function getBlockInfo(x, y, z) end | ||||
							
								
								
									
										42
									
								
								doc/stub/fs.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								doc/stub/fs.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| ---  The FS API allows you to manipulate files and the filesystem. | ||||
| -- | ||||
| -- @module fs | ||||
|  | ||||
| function list(path) end | ||||
| function combine(base, child) end | ||||
| function getName(path) end | ||||
| function getSize(path) end | ||||
| function exists(path) end | ||||
| function isDir(path) end | ||||
| function isReadOnly(path) end | ||||
| function makeDir(path) end | ||||
| function move(from, to) end | ||||
| function copy(from, to) end | ||||
| function delete(path) end | ||||
| function open(path, mode) end | ||||
| function getDrive(path) end | ||||
| function getFreeSpace(path) end | ||||
| function find(pattern) end | ||||
| function getDir(path) end | ||||
|  | ||||
| --- A file handle which can be read from. | ||||
| -- | ||||
| -- @type ReadHandle | ||||
| -- @see fs.open | ||||
| local ReadHandle = {} | ||||
| function ReadHandle.read(count) end | ||||
| function ReadHandle.readAll() end | ||||
| function ReadHandle.readLine(with_trailing) end | ||||
| function ReadHandle.seek(whence, offset) end | ||||
| function ReadHandle.close() end | ||||
|  | ||||
| --- A file handle which can be written to. | ||||
| -- | ||||
| -- @type WriteHandle | ||||
| -- @see fs.open | ||||
| local WriteHandle = {} | ||||
| function WriteHandle.write(text) end | ||||
| function WriteHandle.writeLine(text) end | ||||
| function WriteHandle.flush(text) end | ||||
| function WriteHandle.seek(whence, offset) end | ||||
| function WriteHandle.close() end | ||||
							
								
								
									
										229
									
								
								doc/stub/http.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								doc/stub/http.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,229 @@ | ||||
| --- The http library allows communicating with web servers, sending and | ||||
| -- receiving data from them. | ||||
| -- | ||||
| -- #### `http_check` event | ||||
| -- | ||||
| -- @module http | ||||
|  | ||||
| --- Asynchronously make a HTTP request to the given url. | ||||
| -- | ||||
| -- This returns immediately, a [`http_success`](#http-success-event) or | ||||
| -- [`http_failure`](#http-failure-event) will be queued once the request has | ||||
| -- completed. | ||||
| -- | ||||
| -- @tparam      string url   The url to request | ||||
| -- @tparam[opt] string body  An optional string containing the body of the | ||||
| -- request. If specified, a `POST` request will be made instead. | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of this request. | ||||
| -- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| -- the body will not be UTF-8 encoded, and the received response will not be | ||||
| -- decoded. | ||||
| -- | ||||
| -- @tparam[2] { | ||||
| --   url = string, body? = string, headers? = { [string] = string }, | ||||
| --   binary? = boolean, method? = string, redirect? = boolean, | ||||
| -- } request Options for the request. | ||||
| -- | ||||
| -- This table form is an expanded version of the previous syntax. All arguments | ||||
| -- from above are passed in as fields instead (for instance, | ||||
| -- `http.request("https://example.com")` becomes `http.request { url = | ||||
| -- "https://example.com" }`). | ||||
| -- | ||||
| -- This table also accepts several additional options: | ||||
| -- | ||||
| --  - `method`: Which HTTP method to use, for instance `"PATCH"` or `"DELETE"`. | ||||
| --  - `redirect`: Whether to follow HTTP redirects. Defaults to true. | ||||
| -- | ||||
| -- @see http.get  For a synchronous way to make GET requests. | ||||
| -- @see http.post For a synchronous way to make POST requests. | ||||
| function request(...) end | ||||
|  | ||||
| --- Make a HTTP GET request to the given url. | ||||
| -- | ||||
| -- @tparam string url   The url to request | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of this request. | ||||
| -- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| -- the body will not be UTF-8 encoded, and the received response will not be | ||||
| -- decoded. | ||||
| -- | ||||
| -- @tparam[2] { | ||||
| --   url = string, headers? = { [string] = string }, | ||||
| --   binary? = boolean, method? = string, redirect? = boolean, | ||||
| -- } request Options for the request. See @{http.request} for details on how | ||||
| -- these options behave. | ||||
| -- | ||||
| -- @treturn Response The resulting http response, which can be read from. | ||||
| -- @treturn[2] nil When the http request failed, such as in the event of a 404 | ||||
| -- error or connection timeout. | ||||
| -- @treturn string A message detailing why the request failed. | ||||
| -- @treturn Response|nil The failing http response, if available. | ||||
| -- | ||||
| -- @usage Make a request to [example.computercraft.cc](https://example.computercraft.cc), | ||||
| -- and print the returned page. | ||||
| -- ```lua | ||||
| -- local request = http.get("https://example.computercraft.cc") | ||||
| -- print(request.readAll()) | ||||
| -- -- => HTTP is working! | ||||
| -- request.close() | ||||
| -- ``` | ||||
| function get(...) end | ||||
|  | ||||
| --- Make a HTTP POST request to the given url. | ||||
| -- | ||||
| -- @tparam string url   The url to request | ||||
| -- @tparam string body  The body of the POST request. | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of this request. | ||||
| -- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| -- the body will not be UTF-8 encoded, and the received response will not be | ||||
| -- decoded. | ||||
| -- | ||||
| -- @tparam[2] { | ||||
| --   url = string, body? = string, headers? = { [string] = string }, | ||||
| --   binary? = boolean, method? = string, redirect? = boolean, | ||||
| -- } request Options for the request. See @{http.request} for details on how | ||||
| -- these options behave. | ||||
| -- | ||||
| -- @treturn Response The resulting http response, which can be read from. | ||||
| -- @treturn[2] nil When the http request failed, such as in the event of a 404 | ||||
| -- error or connection timeout. | ||||
| -- @treturn string A message detailing why the request failed. | ||||
| -- @treturn Response|nil The failing http response, if available. | ||||
| function post(...) end | ||||
|  | ||||
| --- A http response. This acts very much like a @{fs.ReadHandle|file}, though | ||||
| -- provides some http specific methods. | ||||
| -- | ||||
| -- #### `http_success` event | ||||
| -- #### `http_failure` event | ||||
| -- | ||||
| -- @type Response | ||||
| -- @see http.request On how to make a http request. | ||||
| local Response = {} | ||||
|  | ||||
| --- Returns the response code and response message returned by the server | ||||
| -- | ||||
| -- @treturn number The response code (i.e. 200) | ||||
| -- @treturn string The response message (i.e. "OK") | ||||
| function Response.getResponseCode() end | ||||
|  | ||||
| --- Get a table containing the response's headers, in a format similar to that | ||||
| -- required by @{http.request}. If multiple headers are sent with the same | ||||
| -- name, they will be combined with a comma. | ||||
| -- | ||||
| -- @treturn { [string]=string } The response's headers. | ||||
| -- Make a request to [example.computercraft.cc](https://example.computercraft.cc), | ||||
| -- and print the returned headers. | ||||
| -- ```lua | ||||
| -- local request = http.get("https://example.computercraft.cc") | ||||
| -- print(textutils.serialize(request.getResponseHeaders())) | ||||
| -- -- => { | ||||
| -- --   [ "Content-Type" ] = "text/plain; charset=utf8", | ||||
| -- --   [ "content-length" ] = 17, | ||||
| -- --   ... | ||||
| -- -- } | ||||
| -- request.close() | ||||
| -- ``` | ||||
| function Response.getResponseHeaders() end | ||||
|  | ||||
| function Response.read(count) end | ||||
| function Response.readAll() end | ||||
| function Response.readLine(with_trailing) end | ||||
| function Response.seek(whence, offset) end | ||||
| function Response.close() end | ||||
|  | ||||
| --- Asynchronously determine whether a URL can be requested. | ||||
| -- | ||||
| -- If this returns `true`, one should also listen for [`http_check` | ||||
| -- events](#http-check-event) which will container further information about | ||||
| -- whether the URL is allowed or not. | ||||
| -- | ||||
| -- @tparam string url The URL to check. | ||||
| -- @treturn true When this url is not invalid. This does not imply that it is | ||||
| -- allowed - see the comment above. | ||||
| -- @treturn[2] false When this url is invalid. | ||||
| -- @treturn string A reason why this URL is not valid (for instance, if it is | ||||
| -- malformed, or blocked). | ||||
| -- | ||||
| -- @see http.checkURL For a synchronous version. | ||||
| function checkURLAsync(url) end | ||||
|  | ||||
| --- Determine whether a URL can be requested. | ||||
| -- | ||||
| -- If this returns `true`, one should also listen for [`http_check` | ||||
| -- events](#http-check-event) which will container further information about | ||||
| -- whether the URL is allowed or not. | ||||
| -- | ||||
| -- @tparam string url The URL to check. | ||||
| -- @treturn true When this url is valid and can be requested via @{http.request}. | ||||
| -- @treturn[2] false When this url is invalid. | ||||
| -- @treturn string A reason why this URL is not valid (for instance, if it is | ||||
| -- malformed, or blocked). | ||||
| -- | ||||
| -- @see http.checkURLAsync For an asynchronous version. | ||||
| -- | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- print(http.checkURL("https://example.computercraft.cc/")) | ||||
| -- -- => true | ||||
| -- print(http.checkURL("http://localhost/")) | ||||
| -- -- => false Domain not permitted | ||||
| -- print(http.checkURL("not a url")) | ||||
| -- -- => false URL malformed | ||||
| -- ``` | ||||
| function checkURL(url) end | ||||
|  | ||||
| --- Open a websocket. | ||||
| -- | ||||
| -- @tparam string url The websocket url to connect to. This should have the | ||||
| -- `ws://` or `wss://` protocol. | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of the initial websocket connection. | ||||
| -- | ||||
| -- @treturn Websocket The websocket connection. | ||||
| -- @treturn[2] false If the websocket connection failed. | ||||
| -- @treturn string An error message describing why the connection failed. | ||||
| function websocket(url, headers) end | ||||
|  | ||||
| --- Asynchronously open a websocket. | ||||
| -- | ||||
| -- This returns immediately, a [`websocket_success`](#websocket-success-event) | ||||
| -- or [`websocket_failure`](#websocket-failure-event) will be queued once the | ||||
| -- request has completed. | ||||
| -- | ||||
| -- @tparam string url The websocket url to connect to. This should have the | ||||
| -- `ws://` or `wss://` protocol. | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of the initial websocket connection. | ||||
| function websocketAsync(url, headers) end | ||||
|  | ||||
| --- A websocket, which can be used to send an receive messages with a web | ||||
| -- server. | ||||
| -- | ||||
| -- @type Websocket | ||||
| -- @see http.websocket On how to open a websocket. | ||||
| local Websocket = {} | ||||
|  | ||||
| --- Send a websocket message to the connected server. | ||||
| -- | ||||
| -- @tparam string message The message to send. | ||||
| -- @tparam[opt] boolean binary Whether this message should be treated as a | ||||
| -- binary string, rather than encoded text. | ||||
| -- @throws If the websocket has been closed. | ||||
| function Websocket.send(message, binary) end | ||||
|  | ||||
| --- Wait for a message from the server. | ||||
| -- | ||||
| -- @tparam[opt] number timeout The number of seconds to wait if no message is | ||||
| -- received. | ||||
| -- @treturn[1] string The received message. | ||||
| -- @treturn boolean If this was a binary message. | ||||
| -- @treturn[2] nil If the websocket was closed while waiting, or if we timed out. | ||||
| -- @throws If the websocket has been closed. | ||||
| function Websocket.receive(timeout) end | ||||
|  | ||||
| --- Close this websocket. This will terminate the connection, meaning messages | ||||
| -- can no longer be sent or received along it. | ||||
| function Websocket.close() end | ||||
							
								
								
									
										17
									
								
								doc/stub/os.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								doc/stub/os.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| function queueEvent(event, ...) end | ||||
| function startTimer(delay) end | ||||
| function setAlarm(time) end | ||||
| function shutdown() end | ||||
| function reboot() end | ||||
| function getComputerID() end | ||||
| computerID = getComputerID | ||||
| function setComputerLabel(label) end | ||||
| function getComputerLabel() end | ||||
| computerLabel = getComputerLabel | ||||
| function clock() end | ||||
| function time(timezone) end | ||||
| function day(timezone) end | ||||
| function cancelTimer(id) end | ||||
| function cancelAlarm(id) end | ||||
| function epoch(timezone) end | ||||
| function date(format, time) end | ||||
							
								
								
									
										14
									
								
								doc/stub/redstone.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								doc/stub/redstone.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| function getSides() end | ||||
| function setOutput(side, on) end | ||||
| function getOutput(side) end | ||||
| function getInput(side) end | ||||
| function setBundledOutput(side, output) end | ||||
| function getBundledOutput(side) end | ||||
| function getBundledInput(side) end | ||||
| function testBundledInput(side, mask) end | ||||
| function setAnalogOutput(side, value) end | ||||
| setAnalogueOutput = setAnalogOutput | ||||
| function getAnalogOutput(sid) end | ||||
| getAnalogueOutput = getAnalogOutput | ||||
| function getAnalogInput(side) end | ||||
| getAnalogueInput = getAnaloguInput | ||||
							
								
								
									
										52
									
								
								doc/stub/term.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								doc/stub/term.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| function write(text) end | ||||
| function scroll(lines) end | ||||
| function setCursorPos(x, y) end | ||||
| function setCursorBlink(blink) end | ||||
| function getCursorPos() end | ||||
| function getSize() end | ||||
| function clear() end | ||||
| function clearLine() end | ||||
| function setTextColour(colour) end | ||||
| setTextColor = setTextColour | ||||
| function setBackgroundColour(colour) end | ||||
| setBackgroundColor = setBackgroundColour | ||||
| function isColour() end | ||||
| isColor = isColour | ||||
| function getTextColour() end | ||||
| getTextColor = getTextColor | ||||
| function getBackgroundColour() end | ||||
| getBackgroundColour = getBackgroundColour | ||||
| function blit(text, text_colours, background_colours) end | ||||
| function setPaletteColour(colour, ...) end | ||||
| setPaletteColour = setPaletteColour | ||||
| function getPaletteColour(colour, ...) end | ||||
| getPaletteColour = getPaletteColour | ||||
| function nativePaletteColour(colour) end | ||||
| nativePaletteColour = nativePaletteColour | ||||
|  | ||||
| --- @type Redirect | ||||
| local Redirect = {} | ||||
|  | ||||
| Redirect.write = write | ||||
| Redirect.scroll = scroll | ||||
| Redirect.setCursorPos = setCursorPos | ||||
| Redirect.setCursorBlink = setCursorBlink | ||||
| Redirect.getCursorPos = getCursorPos | ||||
| Redirect.getSize = getSize | ||||
| Redirect.clear = clear | ||||
| Redirect.clearLine = clearLine | ||||
| Redirect.setTextColour = setTextColour | ||||
| Redirect.setTextColor = setTextColor | ||||
| Redirect.setBackgroundColour = setBackgroundColour | ||||
| Redirect.setBackgroundColor = setBackgroundColor | ||||
| Redirect.isColour = isColour | ||||
| Redirect.isColor = isColor | ||||
| Redirect.getTextColour = getTextColour | ||||
| Redirect.getTextColor = getTextColor | ||||
| Redirect.getBackgroundColour = getBackgroundColour | ||||
| Redirect.getBackgroundColor = getBackgroundColor | ||||
| Redirect.blit = blit | ||||
| Redirect.setPaletteColour = setPaletteColour | ||||
| Redirect.setPaletteColor = setPaletteColor | ||||
| Redirect.getPaletteColour = getPaletteColour | ||||
| Redirect.getPaletteColor = getPaletteColor | ||||
							
								
								
									
										230
									
								
								doc/stub/turtle.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								doc/stub/turtle.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,230 @@ | ||||
| --- Move the turtle forward one block. | ||||
| -- @treturn boolean Whether the turtle could successfully move. | ||||
| -- @treturn string|nil The reason the turtle could not move. | ||||
| function forward() end | ||||
|  | ||||
| --- Move the turtle backwards one block. | ||||
| -- @treturn boolean Whether the turtle could successfully move. | ||||
| -- @treturn string|nil The reason the turtle could not move. | ||||
| function back() end | ||||
|  | ||||
| --- Move the turtle up one block. | ||||
| -- @treturn boolean Whether the turtle could successfully move. | ||||
| -- @treturn string|nil The reason the turtle could not move. | ||||
| function up() end | ||||
|  | ||||
| --- Move the turtle down one block. | ||||
| -- @treturn boolean Whether the turtle could successfully move. | ||||
| -- @treturn string|nil The reason the turtle could not move. | ||||
| function down() end | ||||
|  | ||||
| --- Rotate the turtle 90 degress to the left. | ||||
| function turnLeft() end | ||||
|  | ||||
| --- Rotate the turtle 90 degress to the right. | ||||
| function turnRight() end | ||||
|  | ||||
| --- Attempt to break the block in front of the turtle. | ||||
| -- | ||||
| -- This requires a turtle tool capable of breaking the block. Diamond pickaxes | ||||
| -- (mining turtles) can break any vanilla block, but other tools (such as axes) | ||||
| -- are more limited. | ||||
| -- | ||||
| -- @tparam[opt] "left"|"right" side The specific tool to use. | ||||
| -- @treturn boolean Whether a block was broken. | ||||
| -- @treturn string|nil The reason no block was broken. | ||||
| function dig(side) end | ||||
|  | ||||
| --- Attempt to break the block above the turtle. See @{dig} for full details. | ||||
| -- | ||||
| -- @tparam[opt] "left"|"right" side The specific tool to use. | ||||
| -- @treturn boolean Whether a block was broken. | ||||
| -- @treturn string|nil The reason no block was broken. | ||||
| function digUp(side) end | ||||
|  | ||||
| --- Attempt to break the block below the turtle. See @{dig} for full details. | ||||
| -- | ||||
| -- @tparam[opt] "left"|"right" side The specific tool to use. | ||||
| -- @treturn boolean Whether a block was broken. | ||||
| -- @treturn string|nil The reason no block was broken. | ||||
| function digDown(side) end | ||||
|  | ||||
| --- Attack the entity in front of the turtle. | ||||
| -- | ||||
| -- @tparam[opt] "left"|"right" side The specific tool to use. | ||||
| -- @treturn boolean Whether an entity was attacked. | ||||
| -- @treturn string|nil The reason nothing was attacked. | ||||
| function attack(side) end | ||||
|  | ||||
| --- Attack the entity above the turtle. | ||||
| -- | ||||
| -- @tparam[opt] "left"|"right" side The specific tool to use. | ||||
| -- @treturn boolean Whether an entity was attacked. | ||||
| -- @treturn string|nil The reason nothing was attacked. | ||||
| function attackUp(side) end | ||||
|  | ||||
| --- Attack the entity below the turtle. | ||||
| -- | ||||
| -- @tparam[opt] "left"|"right" side The specific tool to use. | ||||
| -- @treturn boolean Whether an entity was attacked. | ||||
| -- @treturn string|nil The reason nothing was attacked. | ||||
| function attackDown(side) end | ||||
|  | ||||
| --- Place a block or item into the world in front of the turtle. | ||||
| -- | ||||
| -- @treturn boolean Whether the block could be placed. | ||||
| -- @treturn string|nil The reason the block was not placed. | ||||
| function place() end | ||||
|  | ||||
| --- Place a block or item into the world above the turtle. | ||||
| -- | ||||
| -- @treturn boolean Whether the block could be placed. | ||||
| -- @treturn string|nil The reason the block was not placed. | ||||
| function placeUp() end | ||||
|  | ||||
| --- Place a block or item into the world below the turtle. | ||||
| -- | ||||
| -- @treturn boolean Whether the block could be placed. | ||||
| -- @treturn string|nil The reason the block was not placed. | ||||
| function placeDown() end | ||||
|  | ||||
| --- Drop the currently selected stack into the inventory in front of the turtle, | ||||
| -- or as an item into the world if there is no inventory. | ||||
| -- | ||||
| -- @tparam[opt] number count The number of items to drop. If not given, the | ||||
| -- entire stack will be dropped. | ||||
| -- @treturn boolean Whether items were dropped. | ||||
| -- @treturn string|nil The reason the no items were dropped. | ||||
| -- @see select | ||||
| function drop(count) end | ||||
|  | ||||
| --- Drop the currently selected stack into the inventory above the turtle, or as | ||||
| -- an item into the world if there is no inventory. | ||||
| -- | ||||
| -- @tparam[opt] number count The number of items to drop. If not given, the | ||||
| -- entire stack will be dropped. | ||||
| -- @treturn boolean Whether items were dropped. | ||||
| -- @treturn string|nil The reason the no items were dropped. | ||||
| -- @see select | ||||
| function dropUp(count) end | ||||
|  | ||||
| --- Drop the currently selected stack into the inventory below the turtle, or as | ||||
| -- an item into the world if there is no inventory. | ||||
| -- | ||||
| -- @tparam[opt] number count The number of items to drop. If not given, the | ||||
| -- entire stack will be dropped. | ||||
| -- @treturn boolean Whether items were dropped. | ||||
| -- @treturn string|nil The reason the no items were dropped. | ||||
| -- @see select | ||||
| function dropDown(count) end | ||||
|  | ||||
| --- Suck an item from the inventory in front of the turtle, or from an item | ||||
| -- floating in the world. | ||||
| -- | ||||
| -- This will pull items into the first acceptable slot, starting at the | ||||
| -- @{select|currently selected} one. | ||||
| -- | ||||
| -- @tparam[opt] number count The number of items to suck. If not given, up to a | ||||
| -- stack of items will be picked up. | ||||
| -- @treturn boolean Whether items were picked up. | ||||
| -- @treturn string|nil The reason the no items were picked up. | ||||
| function suck(count) end | ||||
|  | ||||
| --- Suck an item from the inventory above the turtle, or from an item floating | ||||
| -- in the world. | ||||
| -- | ||||
| -- @tparam[opt] number count The number of items to suck. If not given, up to a | ||||
| -- stack of items will be picked up. | ||||
| -- @treturn boolean Whether items were picked up. | ||||
| -- @treturn string|nil The reason the no items were picked up. | ||||
| function suckUp(count) end | ||||
|  | ||||
| --- Suck an item from the inventory below the turtle, or from an item floating | ||||
| -- in the world. | ||||
| -- | ||||
| -- @tparam[opt] number count The number of items to suck. If not given, up to a | ||||
| -- stack of items will be picked up. | ||||
| -- @treturn boolean Whether items were picked up. | ||||
| -- @treturn string|nil The reason the no items were picked up. | ||||
| function suckDown(count) end | ||||
|  | ||||
| --- Check if there is a solid block in front of the turtle. In this case, solid | ||||
| -- refers to any non-air or liquid block. | ||||
| -- | ||||
| -- @treturn boolean If there is a solid block in front. | ||||
| function detect() end | ||||
|  | ||||
| --- Check if there is a solid block above the turtle. | ||||
| -- | ||||
| -- @treturn boolean If there is a solid block above. | ||||
| function detectUp() end | ||||
|  | ||||
| --- Check if there is a solid block below the turtle. | ||||
| -- | ||||
| -- @treturn boolean If there is a solid block below. | ||||
| function detectDown() end | ||||
|  | ||||
| function compare() end | ||||
| function compareUp() end | ||||
| function compareDown() end | ||||
|  | ||||
| function inspect() end | ||||
| function inspectUp() end | ||||
| function inspectDown() end | ||||
|  | ||||
|  | ||||
| --- Change the currently selected slot. | ||||
| -- | ||||
| -- The selected slot is determines what slot actions like @{drop} or | ||||
| -- @{getItemCount} act on. | ||||
| -- | ||||
| -- @tparam number slot The slot to select. | ||||
| -- @see getSelectedSlot | ||||
| function select(slot) end | ||||
|  | ||||
| --- Get the currently selected slot. | ||||
| -- | ||||
| -- @treturn number The current slot. | ||||
| -- @see select | ||||
| function getSelectedSlot() end | ||||
|  | ||||
| --- Get the number of items in the given slot. | ||||
| -- | ||||
| -- @tparam[opt] number slot The slot we wish to check. Defaults to the @{turtle.select|selected slot}. | ||||
| -- @treturn number The number of items in this slot. | ||||
| function getItemCount(slot) end | ||||
|  | ||||
| --- Get the remaining number of items which may be stored in this stack. | ||||
| -- | ||||
| -- For instance, if a slot contains 13 blocks of dirt, it has room for another 51. | ||||
| -- | ||||
| -- @tparam[opt] number slot The slot we wish to check. Defaults to the @{turtle.select|selected slot}. | ||||
| -- @treturn number The space left in this slot. | ||||
| function getItemSpace(slot) end | ||||
|  | ||||
|  | ||||
| --- Get detailed information about the items in the given slot. | ||||
| -- | ||||
| -- @tparam[opt] number slot The slot to get information about. Defaults to the @{turtle.select|selected slot}. | ||||
| -- @treturn nil|table Information about the given slot, or @{nil} if it is empty. | ||||
| -- @usage Print the current slot, assuming it contains 13 dirt. | ||||
| -- | ||||
| --     print(textutils.serialize(turtle.getItemDetail())) | ||||
| --     -- => { | ||||
| --     --    name = "minecraft:dirt", | ||||
| --     --    damage = 0, | ||||
| --     --    count = 13, | ||||
| --     -- } | ||||
| function getItemDetail(slot) end | ||||
|  | ||||
| function getFuelLevel() end | ||||
|  | ||||
| function refuel(count) end | ||||
| function compareTo(slot) end | ||||
| function transferTo(slot, count) end | ||||
|  | ||||
| function getFuelLimit() end | ||||
| function equipLeft() end | ||||
| function equipRight() end | ||||
|  | ||||
| function craft(limit) end | ||||
							
								
								
									
										186
									
								
								doc/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								doc/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| /* Basic reset on elements */ | ||||
| h1, h2, h3, h4, p, table, div, body { | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|     border: 0; | ||||
|     font-size: 100%; | ||||
|     font: inherit; | ||||
|     vertical-align: baseline; | ||||
| } | ||||
| /* Make the page a little more airy */ | ||||
| body { | ||||
|     margin: 20px auto; | ||||
|     max-width: 1200px; | ||||
|     padding: 0 10px; | ||||
|     line-height: 1.6; | ||||
|     color: #222; | ||||
|     background: #fff; | ||||
| } | ||||
|  | ||||
| /* Try to use system default fonts. */ | ||||
| body { | ||||
|     font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", | ||||
|                  "Droid Sans", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; | ||||
| } | ||||
|  | ||||
| code, pre, .parameter, .type, .definition-name, .reference { | ||||
|     font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; | ||||
| } | ||||
|  | ||||
| /* Some definitions of basic tags */ | ||||
| code { | ||||
|     color: #c7254e; | ||||
|     background-color: #f9f2f4; | ||||
| } | ||||
|  | ||||
| p { | ||||
|     margin: 0.9em 0; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|     font-size: 1.5em; | ||||
|     font-weight: lighter; | ||||
|     border-bottom: solid 1px #aaa; | ||||
| } | ||||
|  | ||||
| h2, h3, h4 { margin: 1.4em 0 0.3em;} | ||||
| h2 { font-size: 1.25em; } | ||||
| h3 { font-size: 1.15em; font-weight: bold; } | ||||
| h4 { font-size: 1.06em; } | ||||
|  | ||||
| a, a:visited, a:active { font-weight: bold; color: #004080; text-decoration: none; } | ||||
| a:hover { text-decoration: underline; } | ||||
|  | ||||
| blockquote { margin-left: 3em; } | ||||
|  | ||||
| /* Stop sublists from having initial vertical space */ | ||||
| ul ul { margin-top: 0px; } | ||||
| ol ul { margin-top: 0px; } | ||||
| ol ol { margin-top: 0px; } | ||||
| ul ol { margin-top: 0px; } | ||||
|  | ||||
| /* Make the target distinct; helps when we're navigating to a function */ | ||||
| a:target + * { background-color: #FFFF99; } | ||||
|  | ||||
| /* Allow linking to any subsection */ | ||||
| a[name]::before { content: "#"; } | ||||
|  | ||||
| /* Layout */ | ||||
| #main { | ||||
|     display: flex; | ||||
|     flex-wrap: nowrap; | ||||
|     justify-content: space-between; | ||||
|     min-height: calc(100vh - 100px); | ||||
| } | ||||
|  | ||||
| #main > nav { | ||||
|     flex-basis: 30%; | ||||
|     min-width: 150px; | ||||
|     max-width: 250px; | ||||
|     background-color: #f0f0f0; | ||||
| } | ||||
|  | ||||
| nav h1, nav ul { padding: 0em 10px; } | ||||
|  | ||||
| nav h2 { | ||||
|     background-color:#e7e7e7; | ||||
|     font-size: 1.1em; | ||||
|     color:#000000; | ||||
|     padding: 5px 10px; | ||||
| } | ||||
|  | ||||
| nav ul { | ||||
|     list-style-type: none; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| #content { | ||||
|     flex-shrink: 1; | ||||
|     flex-basis: 80%; | ||||
|     padding: 0px 10px; | ||||
| } | ||||
|  | ||||
| footer { | ||||
|     text-align: right; | ||||
|     font-size: 0.8em; | ||||
| } | ||||
|  | ||||
| /* The definition lists at the top of each page */ | ||||
| table.definition-list { | ||||
|     border-collapse: collapse; | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| table.definition-list td, table.definition-list th { | ||||
|     border: 1px solid #cccccc; | ||||
|     padding: 5px; | ||||
| } | ||||
|  | ||||
| table.definition-list th { | ||||
|     background-color: #f0f0f0; | ||||
|     min-width: 200px; | ||||
|     white-space: nowrap; | ||||
|     text-align: right; | ||||
| } | ||||
|  | ||||
| table.definition-list td { width: 100%; } | ||||
|  | ||||
| dl.definition dt { | ||||
|     border-top: 1px solid #ccc; | ||||
|     padding-top: 1em; | ||||
|     display: flex; | ||||
| } | ||||
|  | ||||
| dl.definition dt .definition-name { | ||||
|     padding: 0 0.1em; | ||||
|     margin: 0 0.1em; | ||||
|     flex-grow: 1; | ||||
| } | ||||
|  | ||||
|  | ||||
| dl.definition dd { | ||||
|     padding-bottom: 1em; | ||||
|     margin: 10px 0 0 20px; | ||||
| } | ||||
|  | ||||
| dl.definition h3 { | ||||
|     font-size: .95em; | ||||
| } | ||||
|  | ||||
| /* Links to source-code */ | ||||
| .source-link { font-size: 0.8em; } | ||||
| .source-link::before { content: '[' } | ||||
| .source-link::after  { content: ']' } | ||||
| a.source-link, a.source-link:visited, a.source-link:active { color: #505050; } | ||||
|  | ||||
| /* Method definitions */ | ||||
| span.parameter:after { content:":"; padding-left: 0.3em; } | ||||
| .optional { text-decoration: underline dotted; } | ||||
|  | ||||
| /** Fancy colour display. */ | ||||
| .colour-ref { | ||||
|     display: inline-block; | ||||
|     width: 0.8em; | ||||
|     height: 0.8em; | ||||
|     margin: 0.1em 0.1em 0.3em 0.1em; /* Terrrible hack to force vertical alignment. */ | ||||
|     border: solid 1px black; | ||||
|     box-sizing: border-box; | ||||
|     vertical-align: middle; | ||||
| } | ||||
|  | ||||
| /* styles for prettification of source */ | ||||
| .highlight .comment { color: #558817; } | ||||
| .highlight .constant { color: #a8660d; } | ||||
| .highlight .escape { color: #844631; } | ||||
| .highlight .keyword { color: #aa5050; font-weight: bold; } | ||||
| .highlight .library { color: #0e7c6b; } | ||||
| .highlight .marker { color: #512b1e; background: #fedc56; font-weight: bold; } | ||||
| .highlight .string { color: #8080ff; } | ||||
| .highlight .literal-kw { color: #8080ff; } | ||||
| .highlight .number { color: #f8660d; } | ||||
| .highlight .operator { color: #2239a8; font-weight: bold; } | ||||
| .highlight .preprocessor, pre .prepro { color: #a33243; } | ||||
| .highlight .global { color: #800080; } | ||||
| .highlight .user-keyword { color: #800080; } | ||||
| .highlight .prompt { color: #558817; } | ||||
| .highlight .url { color: #272fc2; text-decoration: underline; } | ||||
| @@ -1,19 +1,51 @@ | ||||
| ; -*- mode: Lisp;-*- | ||||
|  | ||||
| (sources | ||||
|   /doc/stub/ | ||||
|   /src/main/resources/assets/computercraft/lua/bios.lua | ||||
|   /src/main/resources/assets/computercraft/lua/rom/ | ||||
|   /src/test/resources/test-rom) | ||||
|  | ||||
|  | ||||
| (doc | ||||
|   (title "CC: Tweaked") | ||||
|   (index doc/index.md) | ||||
|   (source-link https://github.com/SquidDev-CC/CC-Tweaked/blob/${commit}/${path}#L${line}) | ||||
|  | ||||
|   (library-path | ||||
|     /doc/stub/ | ||||
|  | ||||
|     /src/main/resources/assets/computercraft/lua/rom/apis | ||||
|     /src/main/resources/assets/computercraft/lua/rom/apis/command | ||||
|     /src/main/resources/assets/computercraft/lua/rom/apis/turtle | ||||
|  | ||||
|     /src/main/resources/assets/computercraft/lua/rom/modules/main | ||||
|     /src/main/resources/assets/computercraft/lua/rom/modules/command | ||||
|     /src/main/resources/assets/computercraft/lua/rom/modules/turtle)) | ||||
|  | ||||
| (at / | ||||
|   (linters | ||||
|     syntax:string-index | ||||
|  | ||||
|     ;; It'd be nice to avoid this, but right now there's a lot of instances of | ||||
|     ;; it. | ||||
|     -var:set-loop | ||||
|  | ||||
|     ;; It's useful to name arguments for documentation, so we allow this. It'd | ||||
|     ;; be good to find a compromise in the future, but this works for now. | ||||
|     -var:unused-arg)) | ||||
|     -var:unused-arg | ||||
|  | ||||
|     ;; Suppress a couple of documentation comments warnings for now. We'll | ||||
|     ;; hopefully be able to remove them in the future. | ||||
|     -doc:undocumented -doc:undocumented-arg -doc:unresolved-reference | ||||
|     -var:unresolved-member) | ||||
|   (lint | ||||
|     (bracket-spaces | ||||
|       (call no-space) | ||||
|       (function-args no-space) | ||||
|       (parens no-space) | ||||
|       (table space) | ||||
|       (index no-space)))) | ||||
|  | ||||
| ;; We disable the unused global linter in bios.lua and the APIs. In the future | ||||
| ;; hopefully we'll get illuaminate to handle this. | ||||
| @@ -21,8 +53,26 @@ | ||||
|   (/src/main/resources/assets/computercraft/lua/bios.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/) | ||||
|   (linters -var:unused-global) | ||||
|   (lint | ||||
|     (allow-toplevel-global true))) | ||||
|   (lint (allow-toplevel-global true))) | ||||
|  | ||||
| ;; These warnings are broken right now | ||||
| (at (bios.lua worm.lua) (linters -control:unreachable)) | ||||
| ;; Silence some variable warnings in documentation stubs. | ||||
| (at /doc/stub | ||||
|   (linters -var:unused-global) | ||||
|   (lint (allow-toplevel-global true))) | ||||
|  | ||||
| ;; Ensure any fully documented modules stay fully documented. | ||||
| (at | ||||
|   (/src/main/resources/assets/computercraft/lua/rom/apis/colors.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/colours.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/disk.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/gps.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/help.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/keys.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/paintutils.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/parallel.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/peripheral.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/rednet.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/settings.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/texutils.lua | ||||
|    /src/main/resources/assets/computercraft/lua/rom/apis/vector.lua) | ||||
|   (linters doc:undocumented doc:undocumented-arg)) | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import dan200.computercraft.shared.peripheral.modem.wireless.BlockWirelessModem; | ||||
| import dan200.computercraft.shared.peripheral.monitor.BlockMonitor; | ||||
| import dan200.computercraft.shared.peripheral.printer.BlockPrinter; | ||||
| import dan200.computercraft.shared.peripheral.speaker.BlockSpeaker; | ||||
| import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; | ||||
| import dan200.computercraft.shared.pocket.items.ItemPocketComputer; | ||||
| import dan200.computercraft.shared.pocket.peripherals.PocketModem; | ||||
| import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker; | ||||
| @@ -81,7 +82,7 @@ public final class ComputerCraft | ||||
|     public static long httpMaxDownload = 16 * 1024 * 1024; | ||||
|     public static long httpMaxUpload = 4 * 1024 * 1024; | ||||
|     public static int httpMaxWebsockets = 4; | ||||
|     public static int httpMaxWebsocketMessage = Websocket.MAX_MESSAGE_SIZE; | ||||
|     public static int httpMaxWebsocketMessage = 128 * 1024; | ||||
|  | ||||
|     public static boolean enableCommandBlock = false; | ||||
|     public static int modem_range = 64; | ||||
| @@ -89,6 +90,7 @@ public final class ComputerCraft | ||||
|     public static int modem_rangeDuringStorm = 64; | ||||
|     public static int modem_highAltitudeRangeDuringStorm = 384; | ||||
|     public static int maxNotesPerTick = 8; | ||||
|     public static MonitorRenderer monitorRenderer = MonitorRenderer.BEST; | ||||
|  | ||||
|     public static boolean turtlesNeedFuel = true; | ||||
|     public static int turtleFuelLimit = 20000; | ||||
|   | ||||
| @@ -0,0 +1,81 @@ | ||||
| /* | ||||
|  * This file is part of the public ComputerCraft API - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2020. 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.filesystem; | ||||
|  | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.nio.file.attribute.FileTime; | ||||
| import java.time.Instant; | ||||
|  | ||||
| /** | ||||
|  * A simple version of {@link BasicFileAttributes}, which provides what information a {@link IMount} already exposes. | ||||
|  */ | ||||
| final class FileAttributes implements BasicFileAttributes | ||||
| { | ||||
|     private static final FileTime EPOCH = FileTime.from( Instant.EPOCH ); | ||||
|  | ||||
|     private final boolean isDirectory; | ||||
|     private final long size; | ||||
|  | ||||
|     FileAttributes( boolean isDirectory, long size ) | ||||
|     { | ||||
|         this.isDirectory = isDirectory; | ||||
|         this.size = size; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public FileTime lastModifiedTime() | ||||
|     { | ||||
|         return EPOCH; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public FileTime lastAccessTime() | ||||
|     { | ||||
|         return EPOCH; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public FileTime creationTime() | ||||
|     { | ||||
|         return EPOCH; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isRegularFile() | ||||
|     { | ||||
|         return !isDirectory; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isDirectory() | ||||
|     { | ||||
|         return isDirectory; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isSymbolicLink() | ||||
|     { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isOther() | ||||
|     { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long size() | ||||
|     { | ||||
|         return size; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Object fileKey() | ||||
|     { | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @@ -27,7 +27,7 @@ public class FileOperationException extends IOException | ||||
|         this.filename = filename; | ||||
|     } | ||||
|  | ||||
|     public FileOperationException( String message ) | ||||
|     public FileOperationException( @Nonnull String message ) | ||||
|     { | ||||
|         super( Objects.requireNonNull( message, "message cannot be null" ) ); | ||||
|         this.filename = null; | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.nio.channels.Channels; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
| @@ -93,4 +94,18 @@ public interface IMount | ||||
|     { | ||||
|         return Channels.newChannel( openForRead( path ) ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get attributes about the given file. | ||||
|      * | ||||
|      * @param path The path to query. | ||||
|      * @return File attributes for the given file. | ||||
|      * @throws IOException If the file does not exist, or attributes could not be fetched. | ||||
|      */ | ||||
|     @Nonnull | ||||
|     default BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         if( !exists( path ) ) throw new FileOperationException( path, "No such file" ); | ||||
|         return new FileAttributes( isDirectory( path ), getSize( path ) ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import java.io.IOException; | ||||
| import java.io.OutputStream; | ||||
| import java.nio.channels.Channels; | ||||
| import java.nio.channels.WritableByteChannel; | ||||
| import java.util.OptionalLong; | ||||
|  | ||||
| /** | ||||
|  * Represents a part of a virtual filesystem that can be mounted onto a computer using {@link IComputerAccess#mount(String, IMount)} | ||||
| @@ -105,4 +106,16 @@ public interface IWritableMount extends IMount | ||||
|      * @throws IOException If the remaining space could not be computed. | ||||
|      */ | ||||
|     long getRemainingSpace() throws IOException; | ||||
|  | ||||
|     /** | ||||
|      * Get the capacity of this mount. This should be equal to the size of all files/directories on this mount, minus | ||||
|      * the {@link #getRemainingSpace()}. | ||||
|      * | ||||
|      * @return The capacity of this mount, in bytes. | ||||
|      */ | ||||
|     @Nonnull | ||||
|     default OptionalLong getCapacity() | ||||
|     { | ||||
|         return OptionalLong.empty(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,194 +6,323 @@ | ||||
| package dan200.computercraft.client.gui; | ||||
|  | ||||
| import com.mojang.blaze3d.platform.GlStateManager; | ||||
| import dan200.computercraft.client.FrameInfo; | ||||
| import dan200.computercraft.core.terminal.Terminal; | ||||
| import dan200.computercraft.core.terminal.TextBuffer; | ||||
| import dan200.computercraft.shared.util.Colour; | ||||
| import dan200.computercraft.shared.util.Palette; | ||||
| import net.minecraft.client.Minecraft; | ||||
| import net.minecraft.client.renderer.BufferBuilder; | ||||
| import net.minecraft.client.renderer.Tessellator; | ||||
| import net.minecraft.client.renderer.texture.TextureManager; | ||||
| import net.minecraft.client.renderer.vertex.DefaultVertexFormats; | ||||
| import net.minecraft.client.renderer.vertex.VertexFormat; | ||||
| import net.minecraft.util.ResourceLocation; | ||||
| import org.lwjgl.opengl.GL11; | ||||
|  | ||||
| import java.util.Arrays; | ||||
| import javax.annotation.Nonnull; | ||||
| import javax.annotation.Nullable; | ||||
|  | ||||
| public final class FixedWidthFontRenderer | ||||
| { | ||||
|     private static final ResourceLocation FONT = new ResourceLocation( "computercraft", "textures/gui/term_font.png" ); | ||||
|     public static final ResourceLocation BACKGROUND = new ResourceLocation( "computercraft", "textures/gui/term_background.png" ); | ||||
|  | ||||
|     /** | ||||
|      * Like {@link DefaultVertexFormats#POSITION_TEX_COLOR}, but flipped. This is backported from 1.15, hence the | ||||
|      * custom format. | ||||
|      */ | ||||
|     public static final VertexFormat POSITION_COLOR_TEX = new VertexFormat(); | ||||
|  | ||||
|     static | ||||
|     { | ||||
|         POSITION_COLOR_TEX.addElement( DefaultVertexFormats.POSITION_3F ); | ||||
|         POSITION_COLOR_TEX.addElement( DefaultVertexFormats.COLOR_4UB ); | ||||
|         POSITION_COLOR_TEX.addElement( DefaultVertexFormats.TEX_2F ); | ||||
|     } | ||||
|  | ||||
|     public static final int FONT_HEIGHT = 9; | ||||
|     public static final int FONT_WIDTH = 6; | ||||
|     public static final float WIDTH = 256.0f; | ||||
|  | ||||
|     private static FixedWidthFontRenderer instance; | ||||
|  | ||||
|     public static FixedWidthFontRenderer instance() | ||||
|     { | ||||
|         if( instance != null ) return instance; | ||||
|         return instance = new FixedWidthFontRenderer(); | ||||
|     } | ||||
|  | ||||
|     private final TextureManager m_textureManager; | ||||
|     public static final float BACKGROUND_START = (WIDTH - 6.0f) / WIDTH; | ||||
|     public static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH; | ||||
|  | ||||
|     private FixedWidthFontRenderer() | ||||
|     { | ||||
|         m_textureManager = Minecraft.getInstance().getTextureManager(); | ||||
|     } | ||||
|  | ||||
|     private static void greyscaleify( double[] rgb ) | ||||
|     private static float toGreyscale( double[] rgb ) | ||||
|     { | ||||
|         Arrays.fill( rgb, (rgb[0] + rgb[1] + rgb[2]) / 3.0f ); | ||||
|         return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3); | ||||
|     } | ||||
|  | ||||
|     private void drawChar( BufferBuilder renderer, double x, double y, int index, int color, Palette p, boolean greyscale ) | ||||
|     private static int getColour( char c ) | ||||
|     { | ||||
|         int i = "0123456789abcdef".indexOf( c ); | ||||
|         return i < 0 ? 0 : 15 - i; | ||||
|     } | ||||
|  | ||||
|     private static void drawChar( BufferBuilder buffer, float x, float y, int index, float r, float g, float b ) | ||||
|     { | ||||
|         // Short circuit to avoid the common case - the texture should be blank here after all. | ||||
|         if( index == '\0' || index == ' ' ) return; | ||||
|  | ||||
|         int column = index % 16; | ||||
|         int row = index / 16; | ||||
|  | ||||
|         double[] colour = p.getColour( 15 - color ); | ||||
|         if( greyscale ) | ||||
|         { | ||||
|             greyscaleify( colour ); | ||||
|         } | ||||
|         float r = (float) colour[0]; | ||||
|         float g = (float) colour[1]; | ||||
|         float b = (float) colour[2]; | ||||
|  | ||||
|         int xStart = 1 + column * (FONT_WIDTH + 2); | ||||
|         int yStart = 1 + row * (FONT_HEIGHT + 2); | ||||
|  | ||||
|         renderer.pos( x, y, 0.0 ).tex( xStart / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x, y + FONT_HEIGHT, 0.0 ).tex( xStart / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x + FONT_WIDTH, y, 0.0 ).tex( (xStart + FONT_WIDTH) / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x + FONT_WIDTH, y, 0.0 ).tex( (xStart + FONT_WIDTH) / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x, y + FONT_HEIGHT, 0.0 ).tex( xStart / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x + FONT_WIDTH, y + FONT_HEIGHT, 0.0 ).tex( (xStart + FONT_WIDTH) / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         buffer.pos( x, y, 0f ).color( r, g, b, 1.0f ).tex( xStart / WIDTH, yStart / WIDTH ).endVertex(); | ||||
|         buffer.pos( x, y + FONT_HEIGHT, 0f ).color( r, g, b, 1.0f ).tex( xStart / WIDTH, (yStart + FONT_HEIGHT) / WIDTH ).endVertex(); | ||||
|         buffer.pos( x + FONT_WIDTH, y, 0f ).color( r, g, b, 1.0f ).tex( (xStart + FONT_WIDTH) / WIDTH, yStart / WIDTH ).endVertex(); | ||||
|         buffer.pos( x + FONT_WIDTH, y, 0f ).color( r, g, b, 1.0f ).tex( (xStart + FONT_WIDTH) / WIDTH, yStart / WIDTH ).endVertex(); | ||||
|         buffer.pos( x, y + FONT_HEIGHT, 0f ).color( r, g, b, 1.0f ).tex( xStart / WIDTH, (yStart + FONT_HEIGHT) / WIDTH ).endVertex(); | ||||
|         buffer.pos( x + FONT_WIDTH, y + FONT_HEIGHT, 0f ).color( r, g, b, 1.0f ).tex( (xStart + FONT_WIDTH) / WIDTH, (yStart + FONT_HEIGHT) / WIDTH ).endVertex(); | ||||
|     } | ||||
|  | ||||
|     private void drawQuad( BufferBuilder renderer, double x, double y, int color, double width, Palette p, boolean greyscale ) | ||||
|     private static void drawQuad( BufferBuilder buffer, float x, float y, float width, float height, float r, float g, float b ) | ||||
|     { | ||||
|         double[] colour = p.getColour( 15 - color ); | ||||
|         buffer.pos( x, y, 0 ).color( r, g, b, 1.0f ).tex( BACKGROUND_START, BACKGROUND_START ).endVertex(); | ||||
|         buffer.pos( x, y + height, 0 ).color( r, g, b, 1.0f ).tex( BACKGROUND_START, BACKGROUND_END ).endVertex(); | ||||
|         buffer.pos( x + width, y, 0 ).color( r, g, b, 1.0f ).tex( BACKGROUND_END, BACKGROUND_START ).endVertex(); | ||||
|         buffer.pos( x + width, y, 0 ).color( r, g, b, 1.0f ).tex( BACKGROUND_END, BACKGROUND_START ).endVertex(); | ||||
|         buffer.pos( x, y + height, 0 ).color( r, g, b, 1.0f ).tex( BACKGROUND_START, BACKGROUND_END ).endVertex(); | ||||
|         buffer.pos( x + width, y + height, 0 ).color( r, g, b, 1.0f ).tex( BACKGROUND_END, BACKGROUND_END ).endVertex(); | ||||
|     } | ||||
|  | ||||
|     private static void drawQuad( BufferBuilder buffer, float x, float y, float width, float height, Palette palette, boolean greyscale, char colourIndex ) | ||||
|     { | ||||
|         double[] colour = palette.getColour( getColour( colourIndex ) ); | ||||
|         float r, g, b; | ||||
|         if( greyscale ) | ||||
|         { | ||||
|             greyscaleify( colour ); | ||||
|             r = g = b = toGreyscale( colour ); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             r = (float) colour[0]; | ||||
|             g = (float) colour[1]; | ||||
|             b = (float) colour[2]; | ||||
|         } | ||||
|         float r = (float) colour[0]; | ||||
|         float g = (float) colour[1]; | ||||
|         float b = (float) colour[2]; | ||||
|  | ||||
|         renderer.pos( x, y, 0.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x + width, y, 0.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x + width, y, 0.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         renderer.pos( x + width, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|         drawQuad( buffer, x, y, width, height, r, g, b ); | ||||
|     } | ||||
|  | ||||
|     private boolean isGreyScale( int colour ) | ||||
|     private static void drawBackground( | ||||
|         @Nonnull BufferBuilder renderer, float x, float y, | ||||
|         @Nonnull TextBuffer backgroundColour, @Nonnull Palette palette, boolean greyscale, | ||||
|         float leftMarginSize, float rightMarginSize, float height | ||||
|     ) | ||||
|     { | ||||
|         return colour == 0 || colour == 15 || colour == 7 || colour == 8; | ||||
|     } | ||||
|         if( leftMarginSize > 0 ) | ||||
|         { | ||||
|             drawQuad( renderer, x - leftMarginSize, y, leftMarginSize, height, palette, greyscale, backgroundColour.charAt( 0 ) ); | ||||
|         } | ||||
|  | ||||
|     public void drawStringBackgroundPart( int x, int y, TextBuffer backgroundColour, double leftMarginSize, double rightMarginSize, boolean greyScale, Palette p ) | ||||
|     { | ||||
|         // Draw the quads | ||||
|         Tessellator tessellator = Tessellator.getInstance(); | ||||
|         BufferBuilder renderer = tessellator.getBuffer(); | ||||
|         renderer.begin( GL11.GL_TRIANGLES, DefaultVertexFormats.POSITION_COLOR ); | ||||
|         if( leftMarginSize > 0.0 ) | ||||
|         if( rightMarginSize > 0 ) | ||||
|         { | ||||
|             int colour1 = "0123456789abcdef".indexOf( backgroundColour.charAt( 0 ) ); | ||||
|             if( colour1 < 0 || (greyScale && !isGreyScale( colour1 )) ) | ||||
|             { | ||||
|                 colour1 = 15; | ||||
|             } | ||||
|             drawQuad( renderer, x - leftMarginSize, y, colour1, leftMarginSize, p, greyScale ); | ||||
|         } | ||||
|         if( rightMarginSize > 0.0 ) | ||||
|         { | ||||
|             int colour2 = "0123456789abcdef".indexOf( backgroundColour.charAt( backgroundColour.length() - 1 ) ); | ||||
|             if( colour2 < 0 || (greyScale && !isGreyScale( colour2 )) ) | ||||
|             { | ||||
|                 colour2 = 15; | ||||
|             } | ||||
|             drawQuad( renderer, x + backgroundColour.length() * FONT_WIDTH, y, colour2, rightMarginSize, p, greyScale ); | ||||
|             drawQuad( renderer, x + backgroundColour.length() * FONT_WIDTH, y, rightMarginSize, height, palette, greyscale, backgroundColour.charAt( backgroundColour.length() - 1 ) ); | ||||
|         } | ||||
|  | ||||
|         // Batch together runs of identical background cells. | ||||
|         int blockStart = 0; | ||||
|         char blockColour = '\0'; | ||||
|         for( int i = 0; i < backgroundColour.length(); i++ ) | ||||
|         { | ||||
|             int colour = "0123456789abcdef".indexOf( backgroundColour.charAt( i ) ); | ||||
|             if( colour < 0 || (greyScale && !isGreyScale( colour )) ) | ||||
|             char colourIndex = backgroundColour.charAt( i ); | ||||
|             if( colourIndex == blockColour ) continue; | ||||
|  | ||||
|             if( blockColour != '\0' ) | ||||
|             { | ||||
|                 colour = 15; | ||||
|                 drawQuad( renderer, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (i - blockStart), height, palette, greyscale, blockColour ); | ||||
|             } | ||||
|             drawQuad( renderer, x + i * FONT_WIDTH, y, colour, FONT_WIDTH, p, greyScale ); | ||||
|  | ||||
|             blockColour = colourIndex; | ||||
|             blockStart = i; | ||||
|         } | ||||
|  | ||||
|         if( blockColour != '\0' ) | ||||
|         { | ||||
|             drawQuad( renderer, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (backgroundColour.length() - blockStart), height, palette, greyscale, blockColour ); | ||||
|         } | ||||
|         GlStateManager.disableTexture(); | ||||
|         tessellator.draw(); | ||||
|         GlStateManager.enableTexture(); | ||||
|     } | ||||
|  | ||||
|     public void drawStringTextPart( int x, int y, TextBuffer s, TextBuffer textColour, boolean greyScale, Palette p ) | ||||
|     public static void drawString( | ||||
|         @Nonnull BufferBuilder renderer, float x, float y, | ||||
|         @Nonnull TextBuffer text, @Nonnull TextBuffer textColour, @Nullable TextBuffer backgroundColour, | ||||
|         @Nonnull Palette palette, boolean greyscale, float leftMarginSize, float rightMarginSize | ||||
|     ) | ||||
|     { | ||||
|         // Draw the quads | ||||
|         Tessellator tessellator = Tessellator.getInstance(); | ||||
|         BufferBuilder renderer = tessellator.getBuffer(); | ||||
|         renderer.begin( GL11.GL_TRIANGLES, DefaultVertexFormats.POSITION_TEX_COLOR ); | ||||
|         for( int i = 0; i < s.length(); i++ ) | ||||
|         if( backgroundColour != null ) | ||||
|         { | ||||
|             // Switch colour | ||||
|             int colour = "0123456789abcdef".indexOf( textColour.charAt( i ) ); | ||||
|             if( colour < 0 || (greyScale && !isGreyScale( colour )) ) | ||||
|             drawBackground( renderer, x, y, backgroundColour, palette, greyscale, leftMarginSize, rightMarginSize, FONT_HEIGHT ); | ||||
|         } | ||||
|  | ||||
|         for( int i = 0; i < text.length(); i++ ) | ||||
|         { | ||||
|             double[] colour = palette.getColour( getColour( textColour.charAt( i ) ) ); | ||||
|             float r, g, b; | ||||
|             if( greyscale ) | ||||
|             { | ||||
|                 colour = 0; | ||||
|                 r = g = b = toGreyscale( colour ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 r = (float) colour[0]; | ||||
|                 g = (float) colour[1]; | ||||
|                 b = (float) colour[2]; | ||||
|             } | ||||
|  | ||||
|             // Draw char | ||||
|             int index = s.charAt( i ); | ||||
|             if( index < 0 || index > 255 ) | ||||
|             { | ||||
|                 index = '?'; | ||||
|             } | ||||
|             drawChar( renderer, x + i * FONT_WIDTH, y, index, colour, p, greyScale ); | ||||
|             int index = text.charAt( i ); | ||||
|             if( index > 255 ) index = '?'; | ||||
|             drawChar( renderer, x + i * FONT_WIDTH, y, index, r, g, b ); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public static void drawString( | ||||
|         float x, float y, @Nonnull TextBuffer text, @Nonnull TextBuffer textColour, @Nullable TextBuffer backgroundColour, | ||||
|         @Nonnull Palette palette, boolean greyscale, float leftMarginSize, float rightMarginSize | ||||
|     ) | ||||
|     { | ||||
|         bindFont(); | ||||
|  | ||||
|         Tessellator tessellator = Tessellator.getInstance(); | ||||
|         BufferBuilder buffer = tessellator.getBuffer(); | ||||
|         begin( buffer ); | ||||
|         drawString( buffer, x, y, text, textColour, backgroundColour, palette, greyscale, leftMarginSize, rightMarginSize ); | ||||
|         tessellator.draw(); | ||||
|     } | ||||
|  | ||||
|     public void drawString( TextBuffer s, int x, int y, TextBuffer textColour, TextBuffer backgroundColour, double leftMarginSize, double rightMarginSize, boolean greyScale, Palette p ) | ||||
|     public static void drawTerminalWithoutCursor( | ||||
|         @Nonnull BufferBuilder buffer, float x, float y, | ||||
|         @Nonnull Terminal terminal, boolean greyscale, | ||||
|         float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize | ||||
|     ) | ||||
|     { | ||||
|         // Draw background | ||||
|         if( backgroundColour != null ) | ||||
|         Palette palette = terminal.getPalette(); | ||||
|         int height = terminal.getHeight(); | ||||
|  | ||||
|         // Top and bottom margins | ||||
|         drawBackground( | ||||
|             buffer, x, y - topMarginSize, | ||||
|             terminal.getBackgroundColourLine( 0 ), palette, greyscale, | ||||
|             leftMarginSize, rightMarginSize, topMarginSize | ||||
|         ); | ||||
|  | ||||
|         drawBackground( | ||||
|             buffer, x, y + height * FONT_HEIGHT, | ||||
|             terminal.getBackgroundColourLine( height - 1 ), palette, greyscale, | ||||
|             leftMarginSize, rightMarginSize, bottomMarginSize | ||||
|         ); | ||||
|  | ||||
|         // The main text | ||||
|         for( int i = 0; i < height; i++ ) | ||||
|         { | ||||
|             // Bind the background texture | ||||
|             m_textureManager.bindTexture( BACKGROUND ); | ||||
|  | ||||
|             // Draw the quads | ||||
|             drawStringBackgroundPart( x, y, backgroundColour, leftMarginSize, rightMarginSize, greyScale, p ); | ||||
|         } | ||||
|  | ||||
|         // Draw text | ||||
|         if( s != null && textColour != null ) | ||||
|         { | ||||
|             // Bind the font texture | ||||
|             bindFont(); | ||||
|  | ||||
|             // Draw the quads | ||||
|             drawStringTextPart( x, y, s, textColour, greyScale, p ); | ||||
|             drawString( | ||||
|                 buffer, x, y + FixedWidthFontRenderer.FONT_HEIGHT * i, | ||||
|                 terminal.getLine( i ), terminal.getTextColourLine( i ), terminal.getBackgroundColourLine( i ), | ||||
|                 palette, greyscale, leftMarginSize, rightMarginSize | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public int getStringWidth( String s ) | ||||
|     public static void drawCursor( | ||||
|         @Nonnull BufferBuilder buffer, float x, float y, | ||||
|         @Nonnull Terminal terminal, boolean greyscale | ||||
|     ) | ||||
|     { | ||||
|         if( s == null ) | ||||
|         Palette palette = terminal.getPalette(); | ||||
|         int width = terminal.getWidth(); | ||||
|         int height = terminal.getHeight(); | ||||
|  | ||||
|         int cursorX = terminal.getCursorX(); | ||||
|         int cursorY = terminal.getCursorY(); | ||||
|         if( terminal.getCursorBlink() && cursorX >= 0 && cursorX < width && cursorY >= 0 && cursorY < height && FrameInfo.getGlobalCursorBlink() ) | ||||
|         { | ||||
|             return 0; | ||||
|             double[] colour = palette.getColour( 15 - terminal.getTextColour() ); | ||||
|             float r, g, b; | ||||
|             if( greyscale ) | ||||
|             { | ||||
|                 r = g = b = toGreyscale( colour ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 r = (float) colour[0]; | ||||
|                 g = (float) colour[1]; | ||||
|                 b = (float) colour[2]; | ||||
|             } | ||||
|  | ||||
|             drawChar( buffer, x + cursorX * FONT_WIDTH, y + cursorY * FONT_HEIGHT, '_', r, g, b ); | ||||
|         } | ||||
|         return s.length() * FONT_WIDTH; | ||||
|     } | ||||
|  | ||||
|     public void bindFont() | ||||
|     public static void drawTerminal( | ||||
|         @Nonnull BufferBuilder buffer, float x, float y, | ||||
|         @Nonnull Terminal terminal, boolean greyscale, | ||||
|         float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize | ||||
|     ) | ||||
|     { | ||||
|         m_textureManager.bindTexture( FONT ); | ||||
|         drawTerminalWithoutCursor( buffer, x, y, terminal, greyscale, topMarginSize, bottomMarginSize, leftMarginSize, rightMarginSize ); | ||||
|         drawCursor( buffer, x, y, terminal, greyscale ); | ||||
|     } | ||||
|  | ||||
|     public static void drawTerminal( | ||||
|         float x, float y, @Nonnull Terminal terminal, boolean greyscale, | ||||
|         float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize | ||||
|     ) | ||||
|     { | ||||
|         bindFont(); | ||||
|  | ||||
|         Tessellator tessellator = Tessellator.getInstance(); | ||||
|         BufferBuilder buffer = tessellator.getBuffer(); | ||||
|         begin( buffer ); | ||||
|         drawTerminal( buffer, x, y, terminal, greyscale, topMarginSize, bottomMarginSize, leftMarginSize, rightMarginSize ); | ||||
|         tessellator.draw(); | ||||
|     } | ||||
|  | ||||
|     public static void drawEmptyTerminal( float x, float y, float width, float height ) | ||||
|     { | ||||
|         bindFont(); | ||||
|  | ||||
|         Tessellator tessellator = Tessellator.getInstance(); | ||||
|         BufferBuilder buffer = tessellator.getBuffer(); | ||||
|         begin( buffer ); | ||||
|  | ||||
|         Colour colour = Colour.Black; | ||||
|         drawQuad( buffer, x, y, width, height, colour.getR(), colour.getG(), colour.getB() ); | ||||
|  | ||||
|         tessellator.draw(); | ||||
|     } | ||||
|  | ||||
|     public static void drawBlocker( @Nonnull BufferBuilder buffer, float x, float y, float width, float height ) | ||||
|     { | ||||
|         Colour colour = Colour.Black; | ||||
|         drawQuad( buffer, x, y, width, height, colour.getR(), colour.getG(), colour.getB() ); | ||||
|     } | ||||
|  | ||||
|     public static void drawBlocker( float x, float y, float width, float height ) | ||||
|     { | ||||
|         bindFont(); | ||||
|  | ||||
|         Tessellator tessellator = Tessellator.getInstance(); | ||||
|         BufferBuilder buffer = tessellator.getBuffer(); | ||||
|         begin( buffer ); | ||||
|         drawBlocker( buffer, x, y, width, height ); | ||||
|         tessellator.draw(); | ||||
|     } | ||||
|  | ||||
|     public static void bindFont() | ||||
|     { | ||||
|         Minecraft.getInstance().getTextureManager().bindTexture( FONT ); | ||||
|         GlStateManager.texParameter( GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP ); | ||||
|  | ||||
|         GlStateManager.enableTexture(); | ||||
|     } | ||||
|  | ||||
|     public static void begin( BufferBuilder buffer ) | ||||
|     { | ||||
|         buffer.begin( GL11.GL_TRIANGLES, POSITION_COLOR_TEX ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,29 +5,18 @@ | ||||
|  */ | ||||
| package dan200.computercraft.client.gui.widgets; | ||||
|  | ||||
| import com.mojang.blaze3d.platform.GlStateManager; | ||||
| import dan200.computercraft.client.FrameInfo; | ||||
| import dan200.computercraft.client.gui.FixedWidthFontRenderer; | ||||
| import dan200.computercraft.core.terminal.Terminal; | ||||
| import dan200.computercraft.core.terminal.TextBuffer; | ||||
| import dan200.computercraft.shared.computer.core.ClientComputer; | ||||
| import dan200.computercraft.shared.computer.core.IComputer; | ||||
| import dan200.computercraft.shared.util.Colour; | ||||
| import dan200.computercraft.shared.util.Palette; | ||||
| import net.minecraft.client.Minecraft; | ||||
| import net.minecraft.client.gui.IGuiEventListener; | ||||
| import net.minecraft.client.renderer.BufferBuilder; | ||||
| import net.minecraft.client.renderer.Tessellator; | ||||
| import net.minecraft.client.renderer.vertex.DefaultVertexFormats; | ||||
| import net.minecraft.util.SharedConstants; | ||||
| import org.lwjgl.glfw.GLFW; | ||||
| import org.lwjgl.opengl.GL11; | ||||
|  | ||||
| import java.util.BitSet; | ||||
| import java.util.function.Supplier; | ||||
|  | ||||
| import static dan200.computercraft.client.gui.FixedWidthFontRenderer.BACKGROUND; | ||||
|  | ||||
| public class WidgetTerminal implements IGuiEventListener | ||||
| { | ||||
|     private static final float TERMINATE_TIME = 0.5f; | ||||
| @@ -329,89 +318,19 @@ public class WidgetTerminal implements IGuiEventListener | ||||
|             Terminal terminal = computer != null ? computer.getTerminal() : null; | ||||
|             if( terminal != null ) | ||||
|             { | ||||
|                 // Draw the terminal | ||||
|                 boolean greyscale = !computer.isColour(); | ||||
|                 Palette palette = terminal.getPalette(); | ||||
|  | ||||
|                 // Get the data from the terminal first | ||||
|                 // Unfortunately we have to keep the lock for the whole of drawing, so the text doesn't change under us. | ||||
|                 FixedWidthFontRenderer fontRenderer = FixedWidthFontRenderer.instance(); | ||||
|                 boolean tblink = terminal.getCursorBlink() && FrameInfo.getGlobalCursorBlink(); | ||||
|                 int tw = terminal.getWidth(); | ||||
|                 int th = terminal.getHeight(); | ||||
|                 int tx = terminal.getCursorX(); | ||||
|                 int ty = terminal.getCursorY(); | ||||
|  | ||||
|                 // Draw margins | ||||
|                 TextBuffer emptyLine = new TextBuffer( ' ', tw ); | ||||
|                 if( topMargin > 0 ) | ||||
|                 { | ||||
|                     fontRenderer.drawString( emptyLine, originX, originY - topMargin, | ||||
|                         terminal.getTextColourLine( 0 ), terminal.getBackgroundColourLine( 0 ), | ||||
|                         leftMargin, rightMargin, greyscale, palette ); | ||||
|                 } | ||||
|  | ||||
|                 if( bottomMargin > 0 ) | ||||
|                 { | ||||
|                     fontRenderer.drawString( emptyLine, originX, originY + bottomMargin + (th - 1) * FixedWidthFontRenderer.FONT_HEIGHT, | ||||
|                         terminal.getTextColourLine( th - 1 ), terminal.getBackgroundColourLine( th - 1 ), | ||||
|                         leftMargin, rightMargin, greyscale, palette ); | ||||
|                 } | ||||
|  | ||||
|                 // Draw lines | ||||
|                 int y = originY; | ||||
|                 for( int line = 0; line < th; line++ ) | ||||
|                 { | ||||
|                     TextBuffer text = terminal.getLine( line ); | ||||
|                     TextBuffer colour = terminal.getTextColourLine( line ); | ||||
|                     TextBuffer backgroundColour = terminal.getBackgroundColourLine( line ); | ||||
|                     fontRenderer.drawString( text, originX, y, colour, backgroundColour, leftMargin, rightMargin, greyscale, palette ); | ||||
|                     y += FixedWidthFontRenderer.FONT_HEIGHT; | ||||
|                 } | ||||
|  | ||||
|                 if( tblink && tx >= 0 && ty >= 0 && tx < tw && ty < th ) | ||||
|                 { | ||||
|                     TextBuffer cursor = new TextBuffer( '_', 1 ); | ||||
|                     TextBuffer cursorColour = new TextBuffer( "0123456789abcdef".charAt( terminal.getTextColour() ), 1 ); | ||||
|  | ||||
|                     fontRenderer.drawString( | ||||
|                         cursor, | ||||
|                         originX + FixedWidthFontRenderer.FONT_WIDTH * tx, | ||||
|                         originY + FixedWidthFontRenderer.FONT_HEIGHT * ty, | ||||
|                         cursorColour, null, | ||||
|                         0, 0, | ||||
|                         greyscale, | ||||
|                         palette | ||||
|                     ); | ||||
|                 } | ||||
|                 FixedWidthFontRenderer.drawTerminal( | ||||
|                     originX, originY, | ||||
|                     terminal, !computer.isColour(), topMargin, bottomMargin, leftMargin, rightMargin | ||||
|                 ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // Draw a black background | ||||
|                 Colour black = Colour.Black; | ||||
|                 GlStateManager.color4f( black.getR(), black.getG(), black.getB(), 1.0f ); | ||||
|                 try | ||||
|                 { | ||||
|                     int x = originX - leftMargin; | ||||
|                     int y = originY - rightMargin; | ||||
|                     int width = termWidth * FixedWidthFontRenderer.FONT_WIDTH + leftMargin + rightMargin; | ||||
|                     int height = termHeight * FixedWidthFontRenderer.FONT_HEIGHT + topMargin + bottomMargin; | ||||
|                 int x = originX - leftMargin; | ||||
|                 int y = originY - rightMargin; | ||||
|                 int width = termWidth * FixedWidthFontRenderer.FONT_WIDTH + leftMargin + rightMargin; | ||||
|                 int height = termHeight * FixedWidthFontRenderer.FONT_HEIGHT + topMargin + bottomMargin; | ||||
|  | ||||
|                     client.getTextureManager().bindTexture( BACKGROUND ); | ||||
|  | ||||
|                     Tessellator tesslector = Tessellator.getInstance(); | ||||
|                     BufferBuilder buffer = tesslector.getBuffer(); | ||||
|                     buffer.begin( GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX ); | ||||
|                     buffer.pos( x, y + height, 0 ).tex( 0 / 256.0, height / 256.0 ).endVertex(); | ||||
|                     buffer.pos( x + width, y + height, 0 ).tex( width / 256.0, height / 256.0 ).endVertex(); | ||||
|                     buffer.pos( x + width, y, 0 ).tex( width / 256.0, 0 / 256.0 ).endVertex(); | ||||
|                     buffer.pos( x, y, 0 ).tex( 0 / 256.0, 0 / 256.0 ).endVertex(); | ||||
|                     tesslector.draw(); | ||||
|                 } | ||||
|                 finally | ||||
|                 { | ||||
|                     GlStateManager.color4f( 1.0f, 1.0f, 1.0f, 1.0f ); | ||||
|                 } | ||||
|                 FixedWidthFontRenderer.drawEmptyTerminal( x, y, width, height ); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -7,15 +7,12 @@ package dan200.computercraft.client.render; | ||||
|  | ||||
| import com.mojang.blaze3d.platform.GlStateManager; | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import dan200.computercraft.client.FrameInfo; | ||||
| import dan200.computercraft.client.gui.FixedWidthFontRenderer; | ||||
| import dan200.computercraft.core.terminal.Terminal; | ||||
| import dan200.computercraft.core.terminal.TextBuffer; | ||||
| import dan200.computercraft.shared.computer.core.ClientComputer; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| import dan200.computercraft.shared.pocket.items.ItemPocketComputer; | ||||
| import dan200.computercraft.shared.util.Colour; | ||||
| import dan200.computercraft.shared.util.Palette; | ||||
| import net.minecraft.client.Minecraft; | ||||
| import net.minecraft.client.renderer.BufferBuilder; | ||||
| import net.minecraft.client.renderer.Tessellator; | ||||
| @@ -27,7 +24,8 @@ import net.minecraftforge.eventbus.api.SubscribeEvent; | ||||
| import net.minecraftforge.fml.common.Mod; | ||||
| import org.lwjgl.opengl.GL11; | ||||
|  | ||||
| import static dan200.computercraft.client.gui.FixedWidthFontRenderer.*; | ||||
| import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT; | ||||
| import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_WIDTH; | ||||
| import static dan200.computercraft.client.gui.GuiComputer.*; | ||||
|  | ||||
| /** | ||||
| @@ -105,21 +103,11 @@ public final class ItemPocketRenderer extends ItemMapLikeRenderer | ||||
|  | ||||
|         if( computer != null && terminal != null ) | ||||
|         { | ||||
|             // If we've a computer and terminal then attempt to render it. | ||||
|             renderTerminal( terminal, !computer.isColour(), width, height ); | ||||
|             FixedWidthFontRenderer.drawTerminal( MARGIN, MARGIN, terminal, !computer.isColour(), MARGIN, MARGIN, MARGIN, MARGIN ); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Otherwise render a plain background | ||||
|             Minecraft.getInstance().getTextureManager().bindTexture( BACKGROUND ); | ||||
|  | ||||
|             Tessellator tessellator = Tessellator.getInstance(); | ||||
|             BufferBuilder buffer = tessellator.getBuffer(); | ||||
|  | ||||
|             Colour black = Colour.Black; | ||||
|             buffer.begin( GL11.GL_QUADS, DefaultVertexFormats.POSITION ); | ||||
|             renderTexture( buffer, 0, 0, 0, 0, width, height, black.getR(), black.getG(), black.getB() ); | ||||
|             tessellator.draw(); | ||||
|             FixedWidthFontRenderer.drawEmptyTerminal( 0, 0, width, height ); | ||||
|         } | ||||
|  | ||||
|         GlStateManager.enableDepthTest(); | ||||
| @@ -190,53 +178,6 @@ public final class ItemPocketRenderer extends ItemMapLikeRenderer | ||||
|         GlStateManager.enableTexture(); | ||||
|     } | ||||
|  | ||||
|     private static void renderTerminal( Terminal terminal, boolean greyscale, int width, int height ) | ||||
|     { | ||||
|         synchronized( terminal ) | ||||
|         { | ||||
|             int termWidth = terminal.getWidth(); | ||||
|             int termHeight = terminal.getHeight(); | ||||
|  | ||||
|             FixedWidthFontRenderer fontRenderer = FixedWidthFontRenderer.instance(); | ||||
|             Palette palette = terminal.getPalette(); | ||||
|  | ||||
|             // Render top/bottom borders | ||||
|             TextBuffer emptyLine = new TextBuffer( ' ', termWidth ); | ||||
|             fontRenderer.drawString( | ||||
|                 emptyLine, MARGIN, 0, | ||||
|                 terminal.getTextColourLine( 0 ), terminal.getBackgroundColourLine( 0 ), MARGIN, MARGIN, greyscale, palette | ||||
|             ); | ||||
|             fontRenderer.drawString( | ||||
|                 emptyLine, MARGIN, 2 * MARGIN + (termHeight - 1) * FixedWidthFontRenderer.FONT_HEIGHT, | ||||
|                 terminal.getTextColourLine( termHeight - 1 ), terminal.getBackgroundColourLine( termHeight - 1 ), MARGIN, MARGIN, greyscale, palette | ||||
|             ); | ||||
|  | ||||
|             // Render the actual text | ||||
|             for( int line = 0; line < termWidth; line++ ) | ||||
|             { | ||||
|                 TextBuffer text = terminal.getLine( line ); | ||||
|                 TextBuffer colour = terminal.getTextColourLine( line ); | ||||
|                 TextBuffer backgroundColour = terminal.getBackgroundColourLine( line ); | ||||
|                 fontRenderer.drawString( | ||||
|                     text, MARGIN, MARGIN + line * FONT_HEIGHT, | ||||
|                     colour, backgroundColour, MARGIN, MARGIN, greyscale, palette | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             // And render the cursor; | ||||
|             int tx = terminal.getCursorX(), ty = terminal.getCursorY(); | ||||
|             if( terminal.getCursorBlink() && FrameInfo.getGlobalCursorBlink() && | ||||
|                 tx >= 0 && ty >= 0 && tx < termWidth && ty < termHeight ) | ||||
|             { | ||||
|                 TextBuffer cursorColour = new TextBuffer( "0123456789abcdef".charAt( terminal.getTextColour() ), 1 ); | ||||
|                 fontRenderer.drawString( | ||||
|                     new TextBuffer( '_', 1 ), MARGIN + FONT_WIDTH * tx, MARGIN + FONT_HEIGHT * ty, | ||||
|                     cursorColour, null, 0, 0, greyscale, palette | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static void renderTexture( BufferBuilder builder, int x, int y, int textureX, int textureY, int width, int height, float r, float g, float b ) | ||||
|     { | ||||
|         renderTexture( builder, x, y, textureX, textureY, width, height, width, height, r, g, b ); | ||||
|   | ||||
| @@ -63,11 +63,12 @@ public final class PrintoutRenderer | ||||
|  | ||||
|     public static void drawText( int x, int y, int start, TextBuffer[] text, TextBuffer[] colours ) | ||||
|     { | ||||
|         FixedWidthFontRenderer fontRenderer = FixedWidthFontRenderer.instance(); | ||||
|  | ||||
|         for( int line = 0; line < LINES_PER_PAGE && line < text.length; line++ ) | ||||
|         { | ||||
|             fontRenderer.drawString( text[start + line], x, y + line * FONT_HEIGHT, colours[start + line], null, 0, 0, false, Palette.DEFAULT ); | ||||
|             FixedWidthFontRenderer.drawString( | ||||
|                 x, y + line * FONT_HEIGHT, text[start + line], colours[start + line], null, Palette.DEFAULT, | ||||
|                 false, 0, 0 | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -78,11 +79,13 @@ public final class PrintoutRenderer | ||||
|         GlStateManager.enableTexture(); | ||||
|         GlStateManager.blendFuncSeparate( SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.ONE, DestFactor.ZERO ); | ||||
|  | ||||
|         FixedWidthFontRenderer fontRenderer = FixedWidthFontRenderer.instance(); | ||||
|  | ||||
|         for( int line = 0; line < LINES_PER_PAGE && line < text.length; line++ ) | ||||
|         { | ||||
|             fontRenderer.drawString( new TextBuffer( text[start + line] ), x, y + line * FONT_HEIGHT, new TextBuffer( colours[start + line] ), null, 0, 0, false, Palette.DEFAULT ); | ||||
|             FixedWidthFontRenderer.drawString( | ||||
|                 x, y + line * FONT_HEIGHT, | ||||
|                 new TextBuffer( text[start + line] ), new TextBuffer( colours[start + line] ), | ||||
|                 null, Palette.DEFAULT, false, 0, 0 | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -10,33 +10,29 @@ import com.mojang.blaze3d.platform.GlStateManager; | ||||
| import dan200.computercraft.client.FrameInfo; | ||||
| import dan200.computercraft.client.gui.FixedWidthFontRenderer; | ||||
| import dan200.computercraft.core.terminal.Terminal; | ||||
| import dan200.computercraft.core.terminal.TextBuffer; | ||||
| import dan200.computercraft.shared.peripheral.monitor.ClientMonitor; | ||||
| import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; | ||||
| import dan200.computercraft.shared.peripheral.monitor.TileMonitor; | ||||
| import dan200.computercraft.shared.util.Colour; | ||||
| import dan200.computercraft.shared.util.DirectionUtil; | ||||
| import dan200.computercraft.shared.util.Palette; | ||||
| import net.minecraft.client.Minecraft; | ||||
| import net.minecraft.client.renderer.BufferBuilder; | ||||
| import net.minecraft.client.renderer.Tessellator; | ||||
| import net.minecraft.client.renderer.tileentity.TileEntityRenderer; | ||||
| import net.minecraft.client.renderer.vertex.DefaultVertexFormats; | ||||
| import net.minecraft.client.renderer.vertex.VertexBuffer; | ||||
| import net.minecraft.util.Direction; | ||||
| import net.minecraft.util.math.BlockPos; | ||||
| import org.lwjgl.opengl.GL11; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
|  | ||||
| import static dan200.computercraft.shared.peripheral.monitor.TileMonitor.RENDER_MARGIN; | ||||
|  | ||||
| public class TileEntityMonitorRenderer extends TileEntityRenderer<TileMonitor> | ||||
| { | ||||
|     @Override | ||||
|     public void render( TileMonitor tileEntity, double posX, double posY, double posZ, float f, int i ) | ||||
|     { | ||||
|         if( tileEntity != null ) | ||||
|         { | ||||
|             renderMonitorAt( tileEntity, posX, posY, posZ, f, i ); | ||||
|         } | ||||
|     } | ||||
|     private static final float MARGIN = (float) (TileMonitor.RENDER_MARGIN * 1.1); | ||||
|  | ||||
|     private static void renderMonitorAt( TileMonitor monitor, double posX, double posY, double posZ, float f, int i ) | ||||
|     @Override | ||||
|     public void render( @Nonnull TileMonitor monitor, double posX, double posY, double posZ, float f, int i ) | ||||
|     { | ||||
|         // Render from the origin monitor | ||||
|         ClientMonitor originTerminal = monitor.getClientMonitor(); | ||||
| @@ -69,223 +65,129 @@ public class TileEntityMonitorRenderer extends TileEntityRenderer<TileMonitor> | ||||
|         float pitch = DirectionUtil.toPitchAngle( front ); | ||||
|  | ||||
|         GlStateManager.pushMatrix(); | ||||
|         try | ||||
|  | ||||
|         // Setup initial transform | ||||
|         GlStateManager.translated( posX + 0.5, posY + 0.5, posZ + 0.5 ); | ||||
|         GlStateManager.rotatef( -yaw, 0.0f, 1.0f, 0.0f ); | ||||
|         GlStateManager.rotatef( pitch, 1.0f, 0.0f, 0.0f ); | ||||
|         GlStateManager.translated( | ||||
|             -0.5 + TileMonitor.RENDER_BORDER + RENDER_MARGIN, | ||||
|             origin.getHeight() - 0.5 - (TileMonitor.RENDER_BORDER + RENDER_MARGIN) + 0, | ||||
|             0.5 | ||||
|         ); | ||||
|         double xSize = origin.getWidth() - 2.0 * (RENDER_MARGIN + TileMonitor.RENDER_BORDER); | ||||
|         double ySize = origin.getHeight() - 2.0 * (RENDER_MARGIN + TileMonitor.RENDER_BORDER); | ||||
|  | ||||
|         // Get renderers | ||||
|         Minecraft mc = Minecraft.getInstance(); | ||||
|  | ||||
|         // Set up render state for monitors. We disable writing to the depth buffer (we draw a "blocker" later), | ||||
|         // and setup lighting so that we render with a glow. | ||||
|         GlStateManager.depthMask( false ); | ||||
|         GLX.glMultiTexCoord2f( GLX.GL_TEXTURE1, 0xFFFF, 0xFFFF ); | ||||
|         GlStateManager.disableLighting(); | ||||
|         mc.gameRenderer.disableLightmap(); | ||||
|  | ||||
|         Terminal terminal = originTerminal.getTerminal(); | ||||
|         if( terminal != null ) | ||||
|         { | ||||
|             // Setup initial transform | ||||
|             GlStateManager.translated( posX + 0.5, posY + 0.5, posZ + 0.5 ); | ||||
|             GlStateManager.rotatef( -yaw, 0.0f, 1.0f, 0.0f ); | ||||
|             GlStateManager.rotatef( pitch, 1.0f, 0.0f, 0.0f ); | ||||
|             GlStateManager.translated( | ||||
|                 -0.5 + TileMonitor.RENDER_BORDER + TileMonitor.RENDER_MARGIN, | ||||
|                 origin.getHeight() - 0.5 - (TileMonitor.RENDER_BORDER + TileMonitor.RENDER_MARGIN) + 0, | ||||
|                 0.5 | ||||
|             ); | ||||
|             double xSize = origin.getWidth() - 2.0 * (TileMonitor.RENDER_MARGIN + TileMonitor.RENDER_BORDER); | ||||
|             double ySize = origin.getHeight() - 2.0 * (TileMonitor.RENDER_MARGIN + TileMonitor.RENDER_BORDER); | ||||
|             // Draw a terminal | ||||
|             double xScale = xSize / (terminal.getWidth() * FixedWidthFontRenderer.FONT_WIDTH); | ||||
|             double yScale = ySize / (terminal.getHeight() * FixedWidthFontRenderer.FONT_HEIGHT); | ||||
|  | ||||
|             // Get renderers | ||||
|             Minecraft mc = Minecraft.getInstance(); | ||||
|             Tessellator tessellator = Tessellator.getInstance(); | ||||
|             BufferBuilder renderer = tessellator.getBuffer(); | ||||
|             GlStateManager.pushMatrix(); | ||||
|             GlStateManager.scaled( (float) xScale, (float) -yScale, 1.0f ); | ||||
|  | ||||
|             // Get terminal | ||||
|             boolean redraw = originTerminal.pollTerminalChanged(); | ||||
|             renderTerminal( originTerminal, (float) (MARGIN / xScale), (float) (MARGIN / yScale) ); | ||||
|  | ||||
|             // Draw the contents | ||||
|             GlStateManager.depthMask( false ); | ||||
|             GLX.glMultiTexCoord2f( GLX.GL_TEXTURE1, 0xFFFF, 0xFFFF ); | ||||
|             GlStateManager.disableLighting(); | ||||
|             mc.gameRenderer.disableLightmap(); | ||||
|             try | ||||
|             { | ||||
|                 Terminal terminal = originTerminal.getTerminal(); | ||||
|                 if( terminal != null ) | ||||
|                 { | ||||
|                     Palette palette = terminal.getPalette(); | ||||
|  | ||||
|                     // Allocate display lists | ||||
|                     if( originTerminal.renderDisplayLists == null ) | ||||
|                     { | ||||
|                         originTerminal.createLists(); | ||||
|                         redraw = true; | ||||
|                     } | ||||
|  | ||||
|                     // Draw a terminal | ||||
|                     boolean greyscale = !originTerminal.isColour(); | ||||
|                     int width = terminal.getWidth(); | ||||
|                     int height = terminal.getHeight(); | ||||
|                     int cursorX = terminal.getCursorX(); | ||||
|                     int cursorY = terminal.getCursorY(); | ||||
|                     FixedWidthFontRenderer fontRenderer = FixedWidthFontRenderer.instance(); | ||||
|  | ||||
|                     GlStateManager.pushMatrix(); | ||||
|                     try | ||||
|                     { | ||||
|                         double xScale = xSize / (width * FixedWidthFontRenderer.FONT_WIDTH); | ||||
|                         double yScale = ySize / (height * FixedWidthFontRenderer.FONT_HEIGHT); | ||||
|                         GlStateManager.scaled( xScale, -yScale, 1.0 ); | ||||
|  | ||||
|                         // Draw background | ||||
|                         mc.getTextureManager().bindTexture( FixedWidthFontRenderer.BACKGROUND ); | ||||
|                         if( redraw ) | ||||
|                         { | ||||
|                             // Build background display list | ||||
|                             GlStateManager.newList( originTerminal.renderDisplayLists[0], GL11.GL_COMPILE ); | ||||
|                             try | ||||
|                             { | ||||
|                                 double marginXSize = TileMonitor.RENDER_MARGIN / xScale; | ||||
|                                 double marginYSize = TileMonitor.RENDER_MARGIN / yScale; | ||||
|                                 double marginSquash = marginYSize / FixedWidthFontRenderer.FONT_HEIGHT; | ||||
|  | ||||
|                                 // Top and bottom margins | ||||
|                                 GlStateManager.pushMatrix(); | ||||
|                                 try | ||||
|                                 { | ||||
|                                     GlStateManager.scaled( 1.0, marginSquash, 1.0 ); | ||||
|                                     GlStateManager.translated( 0.0, -marginYSize / marginSquash, 0.0 ); | ||||
|                                     fontRenderer.drawStringBackgroundPart( 0, 0, terminal.getBackgroundColourLine( 0 ), marginXSize, marginXSize, greyscale, palette ); | ||||
|                                     GlStateManager.translated( 0.0, (marginYSize + height * FixedWidthFontRenderer.FONT_HEIGHT) / marginSquash, 0.0 ); | ||||
|                                     fontRenderer.drawStringBackgroundPart( 0, 0, terminal.getBackgroundColourLine( height - 1 ), marginXSize, marginXSize, greyscale, palette ); | ||||
|                                 } | ||||
|                                 finally | ||||
|                                 { | ||||
|                                     GlStateManager.popMatrix(); | ||||
|                                 } | ||||
|  | ||||
|                                 // Backgrounds | ||||
|                                 for( int y = 0; y < height; y++ ) | ||||
|                                 { | ||||
|                                     fontRenderer.drawStringBackgroundPart( | ||||
|                                         0, FixedWidthFontRenderer.FONT_HEIGHT * y, | ||||
|                                         terminal.getBackgroundColourLine( y ), | ||||
|                                         marginXSize, marginXSize, | ||||
|                                         greyscale, | ||||
|                                         palette | ||||
|                                     ); | ||||
|                                 } | ||||
|                             } | ||||
|                             finally | ||||
|                             { | ||||
|                                 GlStateManager.endList(); | ||||
|                             } | ||||
|                         } | ||||
|                         GlStateManager.callList( originTerminal.renderDisplayLists[0] ); | ||||
|                         GlStateManager.clearCurrentColor(); | ||||
|  | ||||
|                         // Draw text | ||||
|                         fontRenderer.bindFont(); | ||||
|                         if( redraw ) | ||||
|                         { | ||||
|                             // Build text display list | ||||
|                             GlStateManager.newList( originTerminal.renderDisplayLists[1], GL11.GL_COMPILE ); | ||||
|                             try | ||||
|                             { | ||||
|                                 // Lines | ||||
|                                 for( int y = 0; y < height; y++ ) | ||||
|                                 { | ||||
|                                     fontRenderer.drawStringTextPart( | ||||
|                                         0, FixedWidthFontRenderer.FONT_HEIGHT * y, | ||||
|                                         terminal.getLine( y ), | ||||
|                                         terminal.getTextColourLine( y ), | ||||
|                                         greyscale, | ||||
|                                         palette | ||||
|                                     ); | ||||
|                                 } | ||||
|                             } | ||||
|                             finally | ||||
|                             { | ||||
|                                 GlStateManager.endList(); | ||||
|                             } | ||||
|                         } | ||||
|                         GlStateManager.callList( originTerminal.renderDisplayLists[1] ); | ||||
|                         GlStateManager.clearCurrentColor(); | ||||
|  | ||||
|                         // Draw cursor | ||||
|                         fontRenderer.bindFont(); | ||||
|                         if( redraw ) | ||||
|                         { | ||||
|                             // Build cursor display list | ||||
|                             GlStateManager.newList( originTerminal.renderDisplayLists[2], GL11.GL_COMPILE ); | ||||
|                             try | ||||
|                             { | ||||
|                                 // Cursor | ||||
|                                 if( terminal.getCursorBlink() && cursorX >= 0 && cursorX < width && cursorY >= 0 && cursorY < height ) | ||||
|                                 { | ||||
|                                     TextBuffer cursor = new TextBuffer( "_" ); | ||||
|                                     TextBuffer cursorColour = new TextBuffer( "0123456789abcdef".charAt( terminal.getTextColour() ), 1 ); | ||||
|                                     fontRenderer.drawString( | ||||
|                                         cursor, | ||||
|                                         FixedWidthFontRenderer.FONT_WIDTH * cursorX, | ||||
|                                         FixedWidthFontRenderer.FONT_HEIGHT * cursorY, | ||||
|                                         cursorColour, null, | ||||
|                                         0, 0, | ||||
|                                         greyscale, | ||||
|                                         palette | ||||
|                                     ); | ||||
|                                 } | ||||
|                             } | ||||
|                             finally | ||||
|                             { | ||||
|                                 GlStateManager.endList(); | ||||
|                             } | ||||
|                         } | ||||
|                         if( FrameInfo.getGlobalCursorBlink() ) | ||||
|                         { | ||||
|                             GlStateManager.callList( originTerminal.renderDisplayLists[2] ); | ||||
|                             GlStateManager.clearCurrentColor(); | ||||
|                         } | ||||
|                     } | ||||
|                     finally | ||||
|                     { | ||||
|                         GlStateManager.popMatrix(); | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // Draw a big black quad | ||||
|                     mc.getTextureManager().bindTexture( FixedWidthFontRenderer.BACKGROUND ); | ||||
|                     final Colour colour = Colour.Black; | ||||
|  | ||||
|                     final float r = colour.getR(); | ||||
|                     final float g = colour.getG(); | ||||
|                     final float b = colour.getB(); | ||||
|  | ||||
|                     renderer.begin( GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR ); | ||||
|                     renderer.pos( -TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0D ).tex( 0.0, 0.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|                     renderer.pos( -TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).tex( 0.0, 1.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|                     renderer.pos( xSize + TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0D ).tex( 1.0, 0.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|                     renderer.pos( xSize + TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).tex( 1.0, 1.0 ).color( r, g, b, 1.0f ).endVertex(); | ||||
|                     tessellator.draw(); | ||||
|                 } | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 GlStateManager.depthMask( true ); | ||||
|                 mc.gameRenderer.enableLightmap(); | ||||
|                 GlStateManager.enableLighting(); | ||||
|             } | ||||
|  | ||||
|             // Draw the depth blocker | ||||
|             GlStateManager.colorMask( false, false, false, false ); | ||||
|             try | ||||
|             { | ||||
|                 mc.getTextureManager().bindTexture( FixedWidthFontRenderer.BACKGROUND ); | ||||
|                 renderer.begin( GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION ); | ||||
|                 renderer.pos( -TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0 ).endVertex(); | ||||
|                 renderer.pos( -TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).endVertex(); | ||||
|                 renderer.pos( xSize + TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0 ).endVertex(); | ||||
|                 renderer.pos( xSize + TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).endVertex(); | ||||
|                 tessellator.draw(); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 GlStateManager.colorMask( true, true, true, true ); | ||||
|             } | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             GlStateManager.color4f( 1.0f, 1.0f, 1.0f, 1.0f ); | ||||
|             GlStateManager.popMatrix(); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             FixedWidthFontRenderer.drawEmptyTerminal( | ||||
|                 -MARGIN, MARGIN, | ||||
|                 (float) (xSize + 2 * MARGIN), (float) -(ySize + MARGIN * 2) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // Tear down render state for monitors. | ||||
|         GlStateManager.depthMask( true ); | ||||
|         mc.gameRenderer.enableLightmap(); | ||||
|         GlStateManager.enableLighting(); | ||||
|  | ||||
|         // Draw the depth blocker | ||||
|         GlStateManager.colorMask( false, false, false, false ); | ||||
|         FixedWidthFontRenderer.drawBlocker( | ||||
|             (float) -TileMonitor.RENDER_MARGIN, (float) TileMonitor.RENDER_MARGIN, | ||||
|             (float) (xSize + 2 * TileMonitor.RENDER_MARGIN), (float) -(ySize + TileMonitor.RENDER_MARGIN * 2) | ||||
|         ); | ||||
|         GlStateManager.colorMask( true, true, true, true ); | ||||
|  | ||||
|         GlStateManager.popMatrix(); | ||||
|     } | ||||
|  | ||||
|     private static void renderTerminal( ClientMonitor monitor, float xMargin, float yMargin ) | ||||
|     { | ||||
|         Tessellator tessellator = Tessellator.getInstance(); | ||||
|         BufferBuilder buffer = tessellator.getBuffer(); | ||||
|  | ||||
|         boolean redraw = monitor.pollTerminalChanged(); | ||||
|  | ||||
|         // Setup the buffers if needed. We get the renderer here, to avoid the (unlikely) race condition between | ||||
|         // creating the buffers and rendering. | ||||
|         MonitorRenderer renderer = MonitorRenderer.current(); | ||||
|         if( monitor.createBuffer( renderer ) ) redraw = true; | ||||
|  | ||||
|         FixedWidthFontRenderer.bindFont(); | ||||
|  | ||||
|         switch( renderer ) | ||||
|         { | ||||
|             case VBO: | ||||
|             { | ||||
|                 VertexBuffer vbo = monitor.buffer; | ||||
|                 if( redraw ) | ||||
|                 { | ||||
|                     renderTerminalTo( monitor, buffer, xMargin, yMargin ); | ||||
|                     buffer.finishDrawing(); | ||||
|                     buffer.reset(); | ||||
|                     vbo.bufferData( buffer.getByteBuffer() ); | ||||
|                 } | ||||
|  | ||||
|                 vbo.bindBuffer(); | ||||
|                 setupBufferFormat(); | ||||
|                 vbo.drawArrays( GL11.GL_TRIANGLES ); | ||||
|                 VertexBuffer.unbindBuffer(); | ||||
|  | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // We don't draw the cursor with a buffer, as it's dynamic and so we'll end up refreshing far more than is | ||||
|         // reasonable. | ||||
|         FixedWidthFontRenderer.begin( buffer ); | ||||
|         FixedWidthFontRenderer.drawCursor( buffer, 0, 0, monitor.getTerminal(), !monitor.isColour() ); | ||||
|         tessellator.draw(); | ||||
|     } | ||||
|  | ||||
|     private static void renderTerminalTo( ClientMonitor monitor, BufferBuilder buffer, float xMargin, float yMargin ) | ||||
|     { | ||||
|         FixedWidthFontRenderer.begin( buffer ); | ||||
|         FixedWidthFontRenderer.drawTerminalWithoutCursor( | ||||
|             buffer, 0, 0, | ||||
|             monitor.getTerminal(), !monitor.isColour(), yMargin, yMargin, xMargin, xMargin | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public static void setupBufferFormat() | ||||
|     { | ||||
|         int stride = FixedWidthFontRenderer.POSITION_COLOR_TEX.getSize(); | ||||
|         GlStateManager.vertexPointer( 3, GL11.GL_FLOAT, stride, 0 ); | ||||
|         GlStateManager.enableClientState( GL11.GL_VERTEX_ARRAY ); | ||||
|  | ||||
|         GlStateManager.colorPointer( 4, GL11.GL_UNSIGNED_BYTE, stride, 12 ); | ||||
|         GlStateManager.enableClientState( GL11.GL_COLOR_ARRAY ); | ||||
|  | ||||
|         GlStateManager.texCoordPointer( 2, GL11.GL_FLOAT, stride, 16 ); | ||||
|         GlStateManager.enableClientState( GL11.GL_TEXTURE_COORD_ARRAY ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -22,6 +22,11 @@ import java.io.BufferedReader; | ||||
| import java.io.BufferedWriter; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.channels.WritableByteChannel; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.nio.file.attribute.FileTime; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.OptionalLong; | ||||
| import java.util.function.Function; | ||||
|  | ||||
| import static dan200.computercraft.api.lua.ArgumentHelper.getString; | ||||
| @@ -76,6 +81,8 @@ public class FSAPI implements ILuaAPI | ||||
|             "getFreeSpace", | ||||
|             "find", | ||||
|             "getDir", | ||||
|             "getCapacity", | ||||
|             "attributes", | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -315,9 +322,8 @@ public class FSAPI implements ILuaAPI | ||||
|                     throw new LuaException( e.getMessage() ); | ||||
|                 } | ||||
|             } | ||||
|             case 14: | ||||
|             case 14: // find | ||||
|             { | ||||
|                 // find | ||||
|                 String path = getString( args, 0 ); | ||||
|                 try | ||||
|                 { | ||||
| @@ -329,15 +335,50 @@ public class FSAPI implements ILuaAPI | ||||
|                     throw new LuaException( e.getMessage() ); | ||||
|                 } | ||||
|             } | ||||
|             case 15: | ||||
|             case 15: // getDir | ||||
|             { | ||||
|                 // getDir | ||||
|                 String path = getString( args, 0 ); | ||||
|                 return new Object[] { FileSystem.getDirectory( path ) }; | ||||
|             } | ||||
|             case 16: // getCapacity | ||||
|             { | ||||
|                 String path = getString( args, 0 ); | ||||
|                 try | ||||
|                 { | ||||
|                     OptionalLong capacity = m_fileSystem.getCapacity( path ); | ||||
|                     return new Object[] { capacity.isPresent() ? capacity.getAsLong() : null }; | ||||
|                 } | ||||
|                 catch( FileSystemException e ) | ||||
|                 { | ||||
|                     throw new LuaException( e.getMessage() ); | ||||
|                 } | ||||
|             } | ||||
|             case 17: // attributes | ||||
|             { | ||||
|                 String path = getString( args, 0 ); | ||||
|                 try | ||||
|                 { | ||||
|                     BasicFileAttributes attributes = m_fileSystem.getAttributes( path ); | ||||
|                     Map<String, Object> result = new HashMap<>(); | ||||
|                     result.put( "modification", getFileTime( attributes.lastModifiedTime() ) ); | ||||
|                     result.put( "created", getFileTime( attributes.creationTime() ) ); | ||||
|                     result.put( "size", attributes.isDirectory() ? 0 : attributes.size() ); | ||||
|                     result.put( "isDir", attributes.isDirectory() ); | ||||
|                     return new Object[] { result }; | ||||
|                 } | ||||
|                 catch( FileSystemException e ) | ||||
|                 { | ||||
|                     throw new LuaException( e.getMessage() ); | ||||
|                 } | ||||
|             } | ||||
|             default: | ||||
|                 assert false; | ||||
|                 return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static long getFileTime( FileTime time ) | ||||
|     { | ||||
|         return time == null ? 0 : time.toMillis(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,35 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
|  | ||||
| package dan200.computercraft.core.apis; | ||||
|  | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
|  | ||||
| import javax.annotation.Nullable; | ||||
|  | ||||
| /** | ||||
|  * A Lua exception which does not contain its stack trace. | ||||
|  */ | ||||
| public class FastLuaException extends LuaException | ||||
| { | ||||
|     private static final long serialVersionUID = 5957864899303561143L; | ||||
|  | ||||
|     public FastLuaException( @Nullable String message ) | ||||
|     { | ||||
|         super( message ); | ||||
|     } | ||||
|  | ||||
|     public FastLuaException( @Nullable String message, int level ) | ||||
|     { | ||||
|         super( message, level ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public synchronized Throwable fillInStackTrace() | ||||
|     { | ||||
|         return this; | ||||
|     } | ||||
| } | ||||
| @@ -18,6 +18,8 @@ import javax.annotation.Nullable; | ||||
|  | ||||
| public interface IAPIEnvironment | ||||
| { | ||||
|     String TIMER_EVENT = "timer"; | ||||
|  | ||||
|     @FunctionalInterface | ||||
|     interface IPeripheralChangeListener | ||||
|     { | ||||
| @@ -64,6 +66,10 @@ public interface IAPIEnvironment | ||||
|  | ||||
|     void setLabel( @Nullable String label ); | ||||
|  | ||||
|     int startTimer( long ticks ); | ||||
|  | ||||
|     void cancelTimer( int id ); | ||||
|  | ||||
|     void addTrackingChange( @Nonnull TrackingField field, long change ); | ||||
|  | ||||
|     default void addTrackingChange( @Nonnull TrackingField field ) | ||||
|   | ||||
| @@ -9,6 +9,8 @@ import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.api.lua.ILuaContext; | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
| import dan200.computercraft.shared.util.StringUtil; | ||||
| import it.unimi.dsi.fastutil.ints.Int2ObjectMap; | ||||
| import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
| import java.time.Instant; | ||||
| @@ -24,24 +26,12 @@ public class OSAPI implements ILuaAPI | ||||
| { | ||||
|     private IAPIEnvironment m_apiEnvironment; | ||||
|  | ||||
|     private final Map<Integer, Timer> m_timers; | ||||
|     private final Map<Integer, Alarm> m_alarms; | ||||
|     private final Int2ObjectMap<Alarm> m_alarms = new Int2ObjectOpenHashMap<>(); | ||||
|     private int m_clock; | ||||
|     private double m_time; | ||||
|     private int m_day; | ||||
|  | ||||
|     private int m_nextTimerToken; | ||||
|     private int m_nextAlarmToken; | ||||
|  | ||||
|     private static class Timer | ||||
|     { | ||||
|         int m_ticksLeft; | ||||
|  | ||||
|         Timer( int ticksLeft ) | ||||
|         { | ||||
|             m_ticksLeft = ticksLeft; | ||||
|         } | ||||
|     } | ||||
|     private int m_nextAlarmToken = 0; | ||||
|  | ||||
|     private static class Alarm implements Comparable<Alarm> | ||||
|     { | ||||
| @@ -66,10 +56,6 @@ public class OSAPI implements ILuaAPI | ||||
|     public OSAPI( IAPIEnvironment environment ) | ||||
|     { | ||||
|         m_apiEnvironment = environment; | ||||
|         m_nextTimerToken = 0; | ||||
|         m_nextAlarmToken = 0; | ||||
|         m_timers = new HashMap<>(); | ||||
|         m_alarms = new HashMap<>(); | ||||
|     } | ||||
|  | ||||
|     // ILuaAPI implementation | ||||
| @@ -87,11 +73,6 @@ public class OSAPI implements ILuaAPI | ||||
|         m_day = m_apiEnvironment.getComputerEnvironment().getDay(); | ||||
|         m_clock = 0; | ||||
|  | ||||
|         synchronized( m_timers ) | ||||
|         { | ||||
|             m_timers.clear(); | ||||
|         } | ||||
|  | ||||
|         synchronized( m_alarms ) | ||||
|         { | ||||
|             m_alarms.clear(); | ||||
| @@ -101,26 +82,7 @@ public class OSAPI implements ILuaAPI | ||||
|     @Override | ||||
|     public void update() | ||||
|     { | ||||
|         synchronized( m_timers ) | ||||
|         { | ||||
|             // Update the clock | ||||
|             m_clock++; | ||||
|  | ||||
|             // Countdown all of our active timers | ||||
|             Iterator<Map.Entry<Integer, Timer>> it = m_timers.entrySet().iterator(); | ||||
|             while( it.hasNext() ) | ||||
|             { | ||||
|                 Map.Entry<Integer, Timer> entry = it.next(); | ||||
|                 Timer timer = entry.getValue(); | ||||
|                 timer.m_ticksLeft--; | ||||
|                 if( timer.m_ticksLeft <= 0 ) | ||||
|                 { | ||||
|                     // Queue the "timer" event | ||||
|                     queueLuaEvent( "timer", new Object[] { entry.getKey() } ); | ||||
|                     it.remove(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         m_clock++; | ||||
|  | ||||
|         // Wait for all of our alarms | ||||
|         synchronized( m_alarms ) | ||||
| @@ -155,11 +117,6 @@ public class OSAPI implements ILuaAPI | ||||
|     @Override | ||||
|     public void shutdown() | ||||
|     { | ||||
|         synchronized( m_timers ) | ||||
|         { | ||||
|             m_timers.clear(); | ||||
|         } | ||||
|  | ||||
|         synchronized( m_alarms ) | ||||
|         { | ||||
|             m_alarms.clear(); | ||||
| @@ -229,11 +186,8 @@ public class OSAPI implements ILuaAPI | ||||
|             { | ||||
|                 // startTimer | ||||
|                 double timer = getFiniteDouble( args, 0 ); | ||||
|                 synchronized( m_timers ) | ||||
|                 { | ||||
|                     m_timers.put( m_nextTimerToken, new Timer( (int) Math.round( timer / 0.05 ) ) ); | ||||
|                     return new Object[] { m_nextTimerToken++ }; | ||||
|                 } | ||||
|                 int id = m_apiEnvironment.startTimer( Math.round( timer / 0.05 ) ); | ||||
|                 return new Object[] { id }; | ||||
|             } | ||||
|             case 2: | ||||
|             { | ||||
| @@ -278,10 +232,7 @@ public class OSAPI implements ILuaAPI | ||||
|                 return null; | ||||
|             } | ||||
|             case 10: // clock | ||||
|                 synchronized( m_timers ) | ||||
|                 { | ||||
|                     return new Object[] { m_clock * 0.05 }; | ||||
|                 } | ||||
|                 return new Object[] { m_clock * 0.05 }; | ||||
|             case 11: | ||||
|             { | ||||
|                 // time | ||||
| @@ -345,10 +296,7 @@ public class OSAPI implements ILuaAPI | ||||
|             { | ||||
|                 // cancelTimer | ||||
|                 int token = getInt( args, 0 ); | ||||
|                 synchronized( m_timers ) | ||||
|                 { | ||||
|                     m_timers.remove( token ); | ||||
|                 } | ||||
|                 m_apiEnvironment.cancelTimer( token ); | ||||
|                 return null; | ||||
|             } | ||||
|             case 14: | ||||
|   | ||||
| @@ -383,22 +383,30 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange | ||||
|                 String methodName = getString( args, 1 ); | ||||
|                 Object[] methodArgs = Arrays.copyOfRange( args, 2, args.length ); | ||||
|  | ||||
|                 if( side != null ) | ||||
|                 if( side == null ) throw new LuaException( "No peripheral attached" ); | ||||
|  | ||||
|                 PeripheralWrapper p; | ||||
|                 synchronized( m_peripherals ) | ||||
|                 { | ||||
|                     PeripheralWrapper p; | ||||
|                     synchronized( m_peripherals ) | ||||
|                     { | ||||
|                         p = m_peripherals[side.ordinal()]; | ||||
|                     } | ||||
|                     if( p != null ) | ||||
|                     { | ||||
|                         return p.call( context, methodName, methodArgs ); | ||||
|                     } | ||||
|                     p = m_peripherals[side.ordinal()]; | ||||
|                 } | ||||
|                 if( p == null ) throw new LuaException( "No peripheral attached" ); | ||||
|  | ||||
|                 try | ||||
|                 { | ||||
|                     return p.call( context, methodName, methodArgs ); | ||||
|                 } | ||||
|                 catch( LuaException e ) | ||||
|                 { | ||||
|                     // We increase the error level by one in order to shift the error level to where peripheral.call was | ||||
|                     // invoked. It would be possible to do it in Lua code, but would add significantly more overhead. | ||||
|                     if( e.getLevel() > 0 ) throw new FastLuaException( e.getMessage(), e.getLevel() + 1 ); | ||||
|                     throw e; | ||||
|                 } | ||||
|                 throw new LuaException( "No peripheral attached" ); | ||||
|             } | ||||
|             default: | ||||
|                 return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -40,7 +40,11 @@ import java.util.concurrent.Future; | ||||
|  */ | ||||
| public class Websocket extends Resource<Websocket> | ||||
| { | ||||
|     public static final int MAX_MESSAGE_SIZE = 64 * 1024; | ||||
|     /** | ||||
|      * We declare the maximum size to be 2^30 bytes. While messages can be much longer, we set an arbitrary limit as | ||||
|      * working with larger messages (especially within a Lua VM) is absurd. | ||||
|      */ | ||||
|     public static final int MAX_MESSAGE_SIZE = 1 << 30; | ||||
|  | ||||
|     static final String SUCCESS_EVENT = "websocket_success"; | ||||
|     static final String FAILURE_EVENT = "websocket_failure"; | ||||
|   | ||||
| @@ -7,6 +7,7 @@ package dan200.computercraft.core.apis.http.websocket; | ||||
|  | ||||
| import com.google.common.base.Objects; | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import dan200.computercraft.api.lua.ArgumentHelper; | ||||
| import dan200.computercraft.api.lua.ILuaContext; | ||||
| import dan200.computercraft.api.lua.ILuaObject; | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
| @@ -23,6 +24,7 @@ import java.io.Closeable; | ||||
| import java.util.Arrays; | ||||
|  | ||||
| import static dan200.computercraft.api.lua.ArgumentHelper.optBoolean; | ||||
| import static dan200.computercraft.core.apis.IAPIEnvironment.TIMER_EVENT; | ||||
| import static dan200.computercraft.core.apis.http.websocket.Websocket.CLOSE_EVENT; | ||||
| import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT; | ||||
|  | ||||
| @@ -53,7 +55,21 @@ public class WebsocketHandle implements ILuaObject, Closeable | ||||
|         switch( method ) | ||||
|         { | ||||
|             case 0: // receive | ||||
|             { | ||||
|                 checkOpen(); | ||||
|                 int timeoutId; | ||||
|                 if( arguments.length <= 0 || arguments[0] == null ) | ||||
|                 { | ||||
|                     // We do this rather odd argument validation to ensure we can tell the difference between a | ||||
|                     // negative timeout and an absent one. | ||||
|                     timeoutId = -1; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     double timeout = ArgumentHelper.getFiniteDouble( arguments, 0 ); | ||||
|                     timeoutId = websocket.environment().startTimer( Math.round( timeout / 0.05 ) ); | ||||
|                 } | ||||
|  | ||||
|                 while( true ) | ||||
|                 { | ||||
|                     Object[] event = context.pullEvent( null ); | ||||
| @@ -63,9 +79,17 @@ public class WebsocketHandle implements ILuaObject, Closeable | ||||
|                     } | ||||
|                     else if( event.length >= 2 && Objects.equal( event[0], CLOSE_EVENT ) && Objects.equal( event[1], websocket.address() ) && closed ) | ||||
|                     { | ||||
|                         // If the socket is closed abort. | ||||
|                         return null; | ||||
|                     } | ||||
|                     else if( event.length >= 2 && timeoutId != -1 && Objects.equal( event[0], TIMER_EVENT ) | ||||
|                         && event[1] instanceof Number && ((Number) event[1]).intValue() == timeoutId ) | ||||
|                     { | ||||
|                         // If we received a matching timer event then abort. | ||||
|                         return null; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             case 1: // send | ||||
|             { | ||||
|   | ||||
| @@ -181,7 +181,7 @@ public class Computer | ||||
|         executor.tick(); | ||||
|  | ||||
|         // Update the environment's internal state. | ||||
|         internalEnvironment.update(); | ||||
|         internalEnvironment.tick(); | ||||
|  | ||||
|         // Propagate the environment's output to the world. | ||||
|         if( internalEnvironment.updateOutput() ) externalOutputChanged.set( true ); | ||||
|   | ||||
| @@ -426,6 +426,7 @@ final class ComputerExecutor | ||||
|             } | ||||
|  | ||||
|             // Init APIs | ||||
|             computer.getEnvironment().reset(); | ||||
|             for( ILuaAPI api : apis ) api.startup(); | ||||
|  | ||||
|             // Init lua | ||||
| @@ -469,6 +470,7 @@ final class ComputerExecutor | ||||
|  | ||||
|             // Shutdown our APIs | ||||
|             for( ILuaAPI api : apis ) api.shutdown(); | ||||
|             computer.getEnvironment().reset(); | ||||
|  | ||||
|             // Unload filesystem | ||||
|             if( fileSystem != null ) | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|  */ | ||||
| package dan200.computercraft.core.computer; | ||||
|  | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.api.peripheral.IPeripheral; | ||||
| import dan200.computercraft.api.peripheral.IWorkMonitor; | ||||
| import dan200.computercraft.core.apis.IAPIEnvironment; | ||||
| @@ -12,9 +13,12 @@ import dan200.computercraft.core.filesystem.FileSystem; | ||||
| import dan200.computercraft.core.terminal.Terminal; | ||||
| import dan200.computercraft.core.tracking.Tracking; | ||||
| import dan200.computercraft.core.tracking.TrackingField; | ||||
| import it.unimi.dsi.fastutil.ints.Int2ObjectMap; | ||||
| import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
| import java.util.Arrays; | ||||
| import java.util.Iterator; | ||||
|  | ||||
| /** | ||||
|  * Represents the "environment" that a {@link Computer} exists in. | ||||
| @@ -53,6 +57,9 @@ public final class Environment implements IAPIEnvironment | ||||
|     private final IPeripheral[] peripherals = new IPeripheral[ComputerSide.COUNT]; | ||||
|     private IPeripheralChangeListener peripheralListener = null; | ||||
|  | ||||
|     private final Int2ObjectMap<Timer> timers = new Int2ObjectOpenHashMap<>(); | ||||
|     private int nextTimerToken = 0; | ||||
|  | ||||
|     Environment( Computer computer ) | ||||
|     { | ||||
|         this.computer = computer; | ||||
| @@ -198,17 +205,47 @@ public final class Environment implements IAPIEnvironment | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called on the main thread to update the internal state of the computer. | ||||
|      * Called when the computer starts up or shuts down, to reset any internal state. | ||||
|      * | ||||
|      * This just queues a {@code redstone} event if the input has changed. | ||||
|      * @see ILuaAPI#startup() | ||||
|      * @see ILuaAPI#shutdown() | ||||
|      */ | ||||
|     void update() | ||||
|     void reset() | ||||
|     { | ||||
|         synchronized( timers ) | ||||
|         { | ||||
|             timers.clear(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called on the main thread to update the internal state of the computer. | ||||
|      */ | ||||
|     void tick() | ||||
|     { | ||||
|         if( inputChanged ) | ||||
|         { | ||||
|             inputChanged = false; | ||||
|             queueEvent( "redstone", null ); | ||||
|         } | ||||
|  | ||||
|         synchronized( timers ) | ||||
|         { | ||||
|             // Countdown all of our active timers | ||||
|             Iterator<Int2ObjectMap.Entry<Timer>> it = timers.int2ObjectEntrySet().iterator(); | ||||
|             while( it.hasNext() ) | ||||
|             { | ||||
|                 Int2ObjectMap.Entry<Timer> entry = it.next(); | ||||
|                 Timer timer = entry.getValue(); | ||||
|                 timer.ticksLeft--; | ||||
|                 if( timer.ticksLeft <= 0 ) | ||||
|                 { | ||||
|                     // Queue the "timer" event | ||||
|                     queueEvent( TIMER_EVENT, new Object[] { entry.getIntKey() } ); | ||||
|                     it.remove(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -303,9 +340,38 @@ public final class Environment implements IAPIEnvironment | ||||
|         computer.setLabel( label ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int startTimer( long ticks ) | ||||
|     { | ||||
|         synchronized( timers ) | ||||
|         { | ||||
|             timers.put( nextTimerToken, new Timer( ticks ) ); | ||||
|             return nextTimerToken++; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void cancelTimer( int id ) | ||||
|     { | ||||
|         synchronized( timers ) | ||||
|         { | ||||
|             timers.remove( id ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void addTrackingChange( @Nonnull TrackingField field, long change ) | ||||
|     { | ||||
|         Tracking.addValue( computer, field, change ); | ||||
|     } | ||||
|  | ||||
|     private static class Timer | ||||
|     { | ||||
|         long ticksLeft; | ||||
|  | ||||
|         Timer( long ticksLeft ) | ||||
|         { | ||||
|             this.ticksLeft = ticksLeft; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import javax.annotation.Nonnull; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| @@ -143,4 +144,19 @@ public class ComboMount implements IMount | ||||
|         } | ||||
|         throw new FileOperationException( path, "No such file" ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         for( int i = m_parts.length - 1; i >= 0; --i ) | ||||
|         { | ||||
|             IMount part = m_parts[i]; | ||||
|             if( part.exists( path ) && !part.isDirectory( path ) ) | ||||
|             { | ||||
|                 return part.getAttributes( path ); | ||||
|             } | ||||
|         } | ||||
|         throw new FileOperationException( path, "No such file" ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
| package dan200.computercraft.core.filesystem; | ||||
|  | ||||
| import com.google.common.collect.Sets; | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import dan200.computercraft.api.filesystem.FileOperationException; | ||||
| import dan200.computercraft.api.filesystem.IWritableMount; | ||||
|  | ||||
| @@ -13,11 +14,11 @@ import javax.annotation.Nonnull; | ||||
| import java.io.*; | ||||
| import java.nio.ByteBuffer; | ||||
| import java.nio.channels.*; | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.OpenOption; | ||||
| import java.nio.file.StandardOpenOption; | ||||
| import java.nio.file.*; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.OptionalLong; | ||||
| import java.util.Set; | ||||
|  | ||||
| public class FileMount implements IWritableMount | ||||
| @@ -224,6 +225,19 @@ public class FileMount implements IWritableMount | ||||
|         throw new FileOperationException( path, "No such file" ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         if( created() ) | ||||
|         { | ||||
|             File file = getRealPath( path ); | ||||
|             if( file.exists() ) return Files.readAttributes( file.toPath(), BasicFileAttributes.class ); | ||||
|         } | ||||
|  | ||||
|         throw new FileOperationException( path, "No such file" ); | ||||
|     } | ||||
|  | ||||
|     // IWritableMount implementation | ||||
|  | ||||
|     @Override | ||||
| @@ -360,6 +374,13 @@ public class FileMount implements IWritableMount | ||||
|         return Math.max( m_capacity - m_usedSpace, 0 ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public OptionalLong getCapacity() | ||||
|     { | ||||
|         return OptionalLong.of( m_capacity - MINIMUM_FILE_SIZE ); | ||||
|     } | ||||
|  | ||||
|     private File getRealPath( String path ) | ||||
|     { | ||||
|         return new File( m_rootPath, path ); | ||||
| @@ -382,23 +403,46 @@ public class FileMount implements IWritableMount | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class Visitor extends SimpleFileVisitor<Path> | ||||
|     { | ||||
|         long size; | ||||
|  | ||||
|         @Override | ||||
|         public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs ) | ||||
|         { | ||||
|             size += MINIMUM_FILE_SIZE; | ||||
|             return FileVisitResult.CONTINUE; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) | ||||
|         { | ||||
|             size += Math.max( attrs.size(), MINIMUM_FILE_SIZE ); | ||||
|             return FileVisitResult.CONTINUE; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public FileVisitResult visitFileFailed( Path file, IOException exc ) | ||||
|         { | ||||
|             ComputerCraft.log.error( "Error computing file size for {}", file, exc ); | ||||
|             return FileVisitResult.CONTINUE; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static long measureUsedSpace( File file ) | ||||
|     { | ||||
|         if( !file.exists() ) return 0; | ||||
|  | ||||
|         if( file.isDirectory() ) | ||||
|         try | ||||
|         { | ||||
|             long size = MINIMUM_FILE_SIZE; | ||||
|             String[] contents = file.list(); | ||||
|             for( String content : contents ) | ||||
|             { | ||||
|                 size += measureUsedSpace( new File( file, content ) ); | ||||
|             } | ||||
|             return size; | ||||
|             Visitor visitor = new Visitor(); | ||||
|             Files.walkFileTree( file.toPath(), visitor ); | ||||
|             return visitor.size; | ||||
|         } | ||||
|         else | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             return Math.max( file.length(), MINIMUM_FILE_SIZE ); | ||||
|             ComputerCraft.log.error( "Error computing file size for {}", file, e ); | ||||
|             return 0; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ package dan200.computercraft.core.filesystem; | ||||
|  | ||||
| import com.google.common.io.ByteStreams; | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import dan200.computercraft.api.filesystem.FileOperationException; | ||||
| import dan200.computercraft.api.filesystem.IFileSystem; | ||||
| import dan200.computercraft.api.filesystem.IMount; | ||||
| import dan200.computercraft.api.filesystem.IWritableMount; | ||||
| @@ -23,309 +22,23 @@ import java.nio.channels.Channel; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.channels.WritableByteChannel; | ||||
| import java.nio.file.AccessDeniedException; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.util.*; | ||||
| import java.util.function.Function; | ||||
| import java.util.regex.Pattern; | ||||
|  | ||||
| public class FileSystem | ||||
| { | ||||
|     private static class MountWrapper | ||||
|     { | ||||
|         private String m_label; | ||||
|         private String m_location; | ||||
|  | ||||
|         private IMount m_mount; | ||||
|         private IWritableMount m_writableMount; | ||||
|  | ||||
|         MountWrapper( String label, String location, IMount mount ) | ||||
|         { | ||||
|             m_label = label; | ||||
|             m_location = location; | ||||
|             m_mount = mount; | ||||
|             m_writableMount = null; | ||||
|         } | ||||
|  | ||||
|         MountWrapper( String label, String location, IWritableMount mount ) | ||||
|         { | ||||
|             this( label, location, (IMount) mount ); | ||||
|             m_writableMount = mount; | ||||
|         } | ||||
|  | ||||
|         public String getLabel() | ||||
|         { | ||||
|             return m_label; | ||||
|         } | ||||
|  | ||||
|         public String getLocation() | ||||
|         { | ||||
|             return m_location; | ||||
|         } | ||||
|  | ||||
|         public long getFreeSpace() | ||||
|         { | ||||
|             if( m_writableMount == null ) | ||||
|             { | ||||
|                 return 0; | ||||
|             } | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 return m_writableMount.getRemainingSpace(); | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 return 0; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public boolean isReadOnly( String path ) | ||||
|         { | ||||
|             return m_writableMount == null; | ||||
|         } | ||||
|  | ||||
|         // IMount forwarders: | ||||
|  | ||||
|         public boolean exists( String path ) throws FileSystemException | ||||
|         { | ||||
|             path = toLocal( path ); | ||||
|             try | ||||
|             { | ||||
|                 return m_mount.exists( path ); | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw new FileSystemException( e.getMessage() ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public boolean isDirectory( String path ) throws FileSystemException | ||||
|         { | ||||
|             path = toLocal( path ); | ||||
|             try | ||||
|             { | ||||
|                 return m_mount.exists( path ) && m_mount.isDirectory( path ); | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw localExceptionOf( e ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void list( String path, List<String> contents ) throws FileSystemException | ||||
|         { | ||||
|             path = toLocal( path ); | ||||
|             try | ||||
|             { | ||||
|                 if( m_mount.exists( path ) && m_mount.isDirectory( path ) ) | ||||
|                 { | ||||
|                     m_mount.list( path, contents ); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw localExceptionOf( path, "Not a directory" ); | ||||
|                 } | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw localExceptionOf( e ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public long getSize( String path ) throws FileSystemException | ||||
|         { | ||||
|             path = toLocal( path ); | ||||
|             try | ||||
|             { | ||||
|                 if( m_mount.exists( path ) ) | ||||
|                 { | ||||
|                     if( m_mount.isDirectory( path ) ) | ||||
|                     { | ||||
|                         return 0; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         return m_mount.getSize( path ); | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw localExceptionOf( path, "No such file" ); | ||||
|                 } | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw localExceptionOf( e ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public ReadableByteChannel openForRead( String path ) throws FileSystemException | ||||
|         { | ||||
|             path = toLocal( path ); | ||||
|             try | ||||
|             { | ||||
|                 if( m_mount.exists( path ) && !m_mount.isDirectory( path ) ) | ||||
|                 { | ||||
|                     return m_mount.openChannelForRead( path ); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw localExceptionOf( path, "No such file" ); | ||||
|                 } | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw localExceptionOf( e ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // IWritableMount forwarders: | ||||
|  | ||||
|         public void makeDirectory( String path ) throws FileSystemException | ||||
|         { | ||||
|             if( m_writableMount == null ) throw exceptionOf( path, "Access denied" ); | ||||
|  | ||||
|             path = toLocal( path ); | ||||
|             try | ||||
|             { | ||||
|                 if( m_mount.exists( path ) ) | ||||
|                 { | ||||
|                     if( !m_mount.isDirectory( path ) ) throw localExceptionOf( path, "File exists" ); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     m_writableMount.makeDirectory( path ); | ||||
|                 } | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw localExceptionOf( e ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void delete( String path ) throws FileSystemException | ||||
|         { | ||||
|             if( m_writableMount == null ) throw exceptionOf( path, "Access denied" ); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 path = toLocal( path ); | ||||
|                 if( m_mount.exists( path ) ) | ||||
|                 { | ||||
|                     m_writableMount.delete( path ); | ||||
|                 } | ||||
|             } | ||||
|             catch( AccessDeniedException e ) | ||||
|             { | ||||
|                 throw new FileSystemException( "Access denied" ); | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw localExceptionOf( e ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public WritableByteChannel openForWrite( String path ) throws FileSystemException | ||||
|         { | ||||
|             if( m_writableMount == null ) throw exceptionOf( path, "Access denied" ); | ||||
|  | ||||
|             path = toLocal( path ); | ||||
|             try | ||||
|             { | ||||
|                 if( m_mount.exists( path ) && m_mount.isDirectory( path ) ) | ||||
|                 { | ||||
|                     throw localExceptionOf( path, "Cannot write to directory" ); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     if( !path.isEmpty() ) | ||||
|                     { | ||||
|                         String dir = getDirectory( path ); | ||||
|                         if( !dir.isEmpty() && !m_mount.exists( path ) ) | ||||
|                         { | ||||
|                             m_writableMount.makeDirectory( dir ); | ||||
|                         } | ||||
|                     } | ||||
|                     return m_writableMount.openChannelForWrite( path ); | ||||
|                 } | ||||
|             } | ||||
|             catch( AccessDeniedException e ) | ||||
|             { | ||||
|                 throw new FileSystemException( "Access denied" ); | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw localExceptionOf( e ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public WritableByteChannel openForAppend( String path ) throws FileSystemException | ||||
|         { | ||||
|             if( m_writableMount == null ) throw exceptionOf( path, "Access denied" ); | ||||
|  | ||||
|             path = toLocal( path ); | ||||
|             try | ||||
|             { | ||||
|                 if( !m_mount.exists( path ) ) | ||||
|                 { | ||||
|                     if( !path.isEmpty() ) | ||||
|                     { | ||||
|                         String dir = getDirectory( path ); | ||||
|                         if( !dir.isEmpty() && !m_mount.exists( path ) ) | ||||
|                         { | ||||
|                             m_writableMount.makeDirectory( dir ); | ||||
|                         } | ||||
|                     } | ||||
|                     return m_writableMount.openChannelForWrite( path ); | ||||
|                 } | ||||
|                 else if( m_mount.isDirectory( path ) ) | ||||
|                 { | ||||
|                     throw localExceptionOf( path, "Cannot write to directory" ); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     return m_writableMount.openChannelForAppend( path ); | ||||
|                 } | ||||
|             } | ||||
|             catch( AccessDeniedException e ) | ||||
|             { | ||||
|                 throw new FileSystemException( "Access denied" ); | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
|                 throw localExceptionOf( e ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private String toLocal( String path ) | ||||
|         { | ||||
|             return FileSystem.toLocal( path, m_location ); | ||||
|         } | ||||
|  | ||||
|         private FileSystemException localExceptionOf( IOException e ) | ||||
|         { | ||||
|             if( !m_location.isEmpty() && e instanceof FileOperationException ) | ||||
|             { | ||||
|                 FileOperationException ex = (FileOperationException) e; | ||||
|                 if( ex.getFilename() != null ) return localExceptionOf( ex.getFilename(), ex.getMessage() ); | ||||
|             } | ||||
|  | ||||
|             return new FileSystemException( e.getMessage() ); | ||||
|         } | ||||
|  | ||||
|         private FileSystemException localExceptionOf( String path, String message ) | ||||
|         { | ||||
|             if( !m_location.isEmpty() ) path = path.isEmpty() ? m_location : m_location + "/" + path; | ||||
|             return exceptionOf( path, message ); | ||||
|         } | ||||
|  | ||||
|         private static FileSystemException exceptionOf( String path, String message ) | ||||
|         { | ||||
|             return new FileSystemException( "/" + path + ": " + message ); | ||||
|         } | ||||
|     } | ||||
|     /** | ||||
|      * Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into. | ||||
|      * | ||||
|      * This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This | ||||
|      * exists to prevent it overflowing if it ever gets into an infinite loop. | ||||
|      */ | ||||
|     private static final int MAX_COPY_DEPTH = 128; | ||||
|  | ||||
|     private final FileSystemWrapperMount m_wrapper = new FileSystemWrapperMount( this ); | ||||
|     private final Map<String, MountWrapper> m_mounts = new HashMap<>(); | ||||
|     private final Map<String, MountWrapper> mounts = new HashMap<>(); | ||||
|  | ||||
|     private final HashMap<WeakReference<FileSystemWrapper<?>>, ChannelWrapper<?>> m_openFiles = new HashMap<>(); | ||||
|     private final ReferenceQueue<FileSystemWrapper<?>> m_openFileQueue = new ReferenceQueue<>(); | ||||
| @@ -355,10 +68,7 @@ public class FileSystem | ||||
|     { | ||||
|         if( mount == null ) throw new NullPointerException(); | ||||
|         location = sanitizePath( location ); | ||||
|         if( location.contains( ".." ) ) | ||||
|         { | ||||
|             throw new FileSystemException( "Cannot mount below the root" ); | ||||
|         } | ||||
|         if( location.contains( ".." ) ) throw new FileSystemException( "Cannot mount below the root" ); | ||||
|         mount( new MountWrapper( label, location, mount ) ); | ||||
|     } | ||||
|  | ||||
| @@ -379,14 +89,13 @@ public class FileSystem | ||||
|     private synchronized void mount( MountWrapper wrapper ) | ||||
|     { | ||||
|         String location = wrapper.getLocation(); | ||||
|         m_mounts.remove( location ); | ||||
|         m_mounts.put( location, wrapper ); | ||||
|         mounts.remove( location ); | ||||
|         mounts.put( location, wrapper ); | ||||
|     } | ||||
|  | ||||
|     public synchronized void unmount( String path ) | ||||
|     { | ||||
|         path = sanitizePath( path ); | ||||
|         m_mounts.remove( path ); | ||||
|         mounts.remove( sanitizePath( path ) ); | ||||
|     } | ||||
|  | ||||
|     public synchronized String combine( String path, String childPath ) | ||||
| @@ -430,27 +139,20 @@ public class FileSystem | ||||
|     public static String getName( String path ) | ||||
|     { | ||||
|         path = sanitizePath( path, true ); | ||||
|         if( path.isEmpty() ) | ||||
|         { | ||||
|             return "root"; | ||||
|         } | ||||
|         if( path.isEmpty() ) return "root"; | ||||
|  | ||||
|         int lastSlash = path.lastIndexOf( '/' ); | ||||
|         if( lastSlash >= 0 ) | ||||
|         { | ||||
|             return path.substring( lastSlash + 1 ); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             return path; | ||||
|         } | ||||
|         return lastSlash >= 0 ? path.substring( lastSlash + 1 ) : path; | ||||
|     } | ||||
|  | ||||
|     public synchronized long getSize( String path ) throws FileSystemException | ||||
|     { | ||||
|         path = sanitizePath( path ); | ||||
|         MountWrapper mount = getMount( path ); | ||||
|         return mount.getSize( path ); | ||||
|         return getMount( sanitizePath( path ) ).getSize( sanitizePath( path ) ); | ||||
|     } | ||||
|  | ||||
|     public synchronized BasicFileAttributes getAttributes( String path ) throws FileSystemException | ||||
|     { | ||||
|         return getMount( sanitizePath( path ) ).getAttributes( sanitizePath( path ) ); | ||||
|     } | ||||
|  | ||||
|     public synchronized String[] list( String path ) throws FileSystemException | ||||
| @@ -463,7 +165,7 @@ public class FileSystem | ||||
|         mount.list( path, list ); | ||||
|  | ||||
|         // Add any mounts that are mounted at this location | ||||
|         for( MountWrapper otherMount : m_mounts.values() ) | ||||
|         for( MountWrapper otherMount : mounts.values() ) | ||||
|         { | ||||
|             if( getDirectory( otherMount.getLocation() ).equals( path ) ) | ||||
|             { | ||||
| @@ -611,15 +313,13 @@ public class FileSystem | ||||
|         { | ||||
|             throw new FileSystemException( "/" + sourcePath + ": Can't copy a directory inside itself" ); | ||||
|         } | ||||
|         copyRecursive( sourcePath, getMount( sourcePath ), destPath, getMount( destPath ) ); | ||||
|         copyRecursive( sourcePath, getMount( sourcePath ), destPath, getMount( destPath ), 0 ); | ||||
|     } | ||||
|  | ||||
|     private synchronized void copyRecursive( String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount ) throws FileSystemException | ||||
|     private synchronized void copyRecursive( String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, int depth ) throws FileSystemException | ||||
|     { | ||||
|         if( !sourceMount.exists( sourcePath ) ) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|         if( !sourceMount.exists( sourcePath ) ) return; | ||||
|         if( depth >= MAX_COPY_DEPTH ) throw new FileSystemException( "Too many directories to copy" ); | ||||
|  | ||||
|         if( sourceMount.isDirectory( sourcePath ) ) | ||||
|         { | ||||
| @@ -634,7 +334,8 @@ public class FileSystem | ||||
|             { | ||||
|                 copyRecursive( | ||||
|                     combine( sourcePath, child ), sourceMount, | ||||
|                     combine( destinationPath, child ), destinationMount | ||||
|                     combine( destinationPath, child ), destinationMount, | ||||
|                     depth + 1 | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| @@ -726,17 +427,25 @@ public class FileSystem | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     public long getFreeSpace( String path ) throws FileSystemException | ||||
|     public synchronized long getFreeSpace( String path ) throws FileSystemException | ||||
|     { | ||||
|         path = sanitizePath( path ); | ||||
|         MountWrapper mount = getMount( path ); | ||||
|         return mount.getFreeSpace(); | ||||
|     } | ||||
|  | ||||
|     private MountWrapper getMount( String path ) throws FileSystemException | ||||
|     @Nonnull | ||||
|     public synchronized OptionalLong getCapacity( String path ) throws FileSystemException | ||||
|     { | ||||
|         path = sanitizePath( path ); | ||||
|         MountWrapper mount = getMount( path ); | ||||
|         return mount.getCapacity(); | ||||
|     } | ||||
|  | ||||
|     private synchronized MountWrapper getMount( String path ) throws FileSystemException | ||||
|     { | ||||
|         // Return the deepest mount that contains a given path | ||||
|         Iterator<MountWrapper> it = m_mounts.values().iterator(); | ||||
|         Iterator<MountWrapper> it = mounts.values().iterator(); | ||||
|         MountWrapper match = null; | ||||
|         int matchLength = 999; | ||||
|         while( it.hasNext() ) | ||||
| @@ -854,8 +563,8 @@ public class FileSystem | ||||
|  | ||||
|     public static boolean contains( String pathA, String pathB ) | ||||
|     { | ||||
|         pathA = sanitizePath( pathA ); | ||||
|         pathB = sanitizePath( pathB ); | ||||
|         pathA = sanitizePath( pathA ).toLowerCase( Locale.ROOT ); | ||||
|         pathB = sanitizePath( pathB ).toLowerCase( Locale.ROOT ); | ||||
|  | ||||
|         if( pathB.equals( ".." ) ) | ||||
|         { | ||||
|   | ||||
| @@ -23,6 +23,9 @@ import java.lang.ref.ReferenceQueue; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.nio.channels.Channels; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.nio.file.attribute.FileTime; | ||||
| import java.time.Instant; | ||||
| import java.util.Enumeration; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| @@ -221,6 +224,20 @@ public class JarMount implements IMount | ||||
|         throw new FileOperationException( path, "No such file" ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         FileEntry file = get( path ); | ||||
|         if( file != null ) | ||||
|         { | ||||
|             ZipEntry entry = zip.getEntry( file.path ); | ||||
|             if( entry != null ) return new ZipEntryAttributes( entry ); | ||||
|         } | ||||
|  | ||||
|         throw new FileOperationException( path, "No such file" ); | ||||
|     } | ||||
|  | ||||
|     private static class FileEntry | ||||
|     { | ||||
|         String path; | ||||
| @@ -261,4 +278,76 @@ public class JarMount implements IMount | ||||
|         Reference<? extends JarMount> next; | ||||
|         while( (next = MOUNT_QUEUE.poll()) != null ) IoUtil.closeQuietly( ((MountReference) next).file ); | ||||
|     } | ||||
|  | ||||
|     private static class ZipEntryAttributes implements BasicFileAttributes | ||||
|     { | ||||
|         private final ZipEntry entry; | ||||
|  | ||||
|         ZipEntryAttributes( ZipEntry entry ) | ||||
|         { | ||||
|             this.entry = entry; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public FileTime lastModifiedTime() | ||||
|         { | ||||
|             return orEpoch( entry.getLastModifiedTime() ); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public FileTime lastAccessTime() | ||||
|         { | ||||
|             return orEpoch( entry.getLastAccessTime() ); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public FileTime creationTime() | ||||
|         { | ||||
|             FileTime time = entry.getCreationTime(); | ||||
|             return time == null ? lastModifiedTime() : time; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean isRegularFile() | ||||
|         { | ||||
|             return !entry.isDirectory(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean isDirectory() | ||||
|         { | ||||
|             return entry.isDirectory(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean isSymbolicLink() | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean isOther() | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public long size() | ||||
|         { | ||||
|             return entry.getSize(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public Object fileKey() | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         private static final FileTime EPOCH = FileTime.from( Instant.EPOCH ); | ||||
|  | ||||
|         private static FileTime orEpoch( FileTime time ) | ||||
|         { | ||||
|             return time == null ? EPOCH : time; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,312 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.core.filesystem; | ||||
|  | ||||
| import dan200.computercraft.api.filesystem.FileOperationException; | ||||
| import dan200.computercraft.api.filesystem.IMount; | ||||
| import dan200.computercraft.api.filesystem.IWritableMount; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
| import java.io.IOException; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.channels.WritableByteChannel; | ||||
| import java.nio.file.AccessDeniedException; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.util.List; | ||||
| import java.util.OptionalLong; | ||||
|  | ||||
| class MountWrapper | ||||
| { | ||||
|     private String label; | ||||
|     private String location; | ||||
|  | ||||
|     private IMount mount; | ||||
|     private IWritableMount writableMount; | ||||
|  | ||||
|     MountWrapper( String label, String location, IMount mount ) | ||||
|     { | ||||
|         this.label = label; | ||||
|         this.location = location; | ||||
|         this.mount = mount; | ||||
|         writableMount = null; | ||||
|     } | ||||
|  | ||||
|     MountWrapper( String label, String location, IWritableMount mount ) | ||||
|     { | ||||
|         this( label, location, (IMount) mount ); | ||||
|         writableMount = mount; | ||||
|     } | ||||
|  | ||||
|     public String getLabel() | ||||
|     { | ||||
|         return label; | ||||
|     } | ||||
|  | ||||
|     public String getLocation() | ||||
|     { | ||||
|         return location; | ||||
|     } | ||||
|  | ||||
|     public long getFreeSpace() | ||||
|     { | ||||
|         if( writableMount == null ) return 0; | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             return writableMount.getRemainingSpace(); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             return 0; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public OptionalLong getCapacity() | ||||
|     { | ||||
|         return writableMount == null ? OptionalLong.empty() : writableMount.getCapacity(); | ||||
|     } | ||||
|  | ||||
|     public boolean isReadOnly( String path ) | ||||
|     { | ||||
|         return writableMount == null; | ||||
|     } | ||||
|  | ||||
|     public boolean exists( String path ) throws FileSystemException | ||||
|     { | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             return mount.exists( path ); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw new FileSystemException( e.getMessage() ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public boolean isDirectory( String path ) throws FileSystemException | ||||
|     { | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             return mount.exists( path ) && mount.isDirectory( path ); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void list( String path, List<String> contents ) throws FileSystemException | ||||
|     { | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             if( !mount.exists( path ) || !mount.isDirectory( path ) ) | ||||
|             { | ||||
|                 throw localExceptionOf( path, "Not a directory" ); | ||||
|             } | ||||
|  | ||||
|             mount.list( path, contents ); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public long getSize( String path ) throws FileSystemException | ||||
|     { | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             if( !mount.exists( path ) ) throw localExceptionOf( path, "No such file" ); | ||||
|             return mount.isDirectory( path ) ? 0 : mount.getSize( path ); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     public BasicFileAttributes getAttributes( String path ) throws FileSystemException | ||||
|     { | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             if( !mount.exists( path ) ) throw localExceptionOf( path, "No such file" ); | ||||
|             return mount.getAttributes( path ); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public ReadableByteChannel openForRead( String path ) throws FileSystemException | ||||
|     { | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             if( mount.exists( path ) && !mount.isDirectory( path ) ) | ||||
|             { | ||||
|                 return mount.openChannelForRead( path ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 throw localExceptionOf( path, "No such file" ); | ||||
|             } | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void makeDirectory( String path ) throws FileSystemException | ||||
|     { | ||||
|         if( writableMount == null ) throw exceptionOf( path, "Access denied" ); | ||||
|  | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             if( mount.exists( path ) ) | ||||
|             { | ||||
|                 if( !mount.isDirectory( path ) ) throw localExceptionOf( path, "File exists" ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 writableMount.makeDirectory( path ); | ||||
|             } | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void delete( String path ) throws FileSystemException | ||||
|     { | ||||
|         if( writableMount == null ) throw exceptionOf( path, "Access denied" ); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             path = toLocal( path ); | ||||
|             if( mount.exists( path ) ) | ||||
|             { | ||||
|                 writableMount.delete( path ); | ||||
|             } | ||||
|         } | ||||
|         catch( AccessDeniedException e ) | ||||
|         { | ||||
|             throw new FileSystemException( "Access denied" ); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public WritableByteChannel openForWrite( String path ) throws FileSystemException | ||||
|     { | ||||
|         if( writableMount == null ) throw exceptionOf( path, "Access denied" ); | ||||
|  | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             if( mount.exists( path ) && mount.isDirectory( path ) ) | ||||
|             { | ||||
|                 throw localExceptionOf( path, "Cannot write to directory" ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if( !path.isEmpty() ) | ||||
|                 { | ||||
|                     String dir = FileSystem.getDirectory( path ); | ||||
|                     if( !dir.isEmpty() && !mount.exists( path ) ) | ||||
|                     { | ||||
|                         writableMount.makeDirectory( dir ); | ||||
|                     } | ||||
|                 } | ||||
|                 return writableMount.openChannelForWrite( path ); | ||||
|             } | ||||
|         } | ||||
|         catch( AccessDeniedException e ) | ||||
|         { | ||||
|             throw new FileSystemException( "Access denied" ); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public WritableByteChannel openForAppend( String path ) throws FileSystemException | ||||
|     { | ||||
|         if( writableMount == null ) throw exceptionOf( path, "Access denied" ); | ||||
|  | ||||
|         path = toLocal( path ); | ||||
|         try | ||||
|         { | ||||
|             if( !mount.exists( path ) ) | ||||
|             { | ||||
|                 if( !path.isEmpty() ) | ||||
|                 { | ||||
|                     String dir = FileSystem.getDirectory( path ); | ||||
|                     if( !dir.isEmpty() && !mount.exists( path ) ) | ||||
|                     { | ||||
|                         writableMount.makeDirectory( dir ); | ||||
|                     } | ||||
|                 } | ||||
|                 return writableMount.openChannelForWrite( path ); | ||||
|             } | ||||
|             else if( mount.isDirectory( path ) ) | ||||
|             { | ||||
|                 throw localExceptionOf( path, "Cannot write to directory" ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 return writableMount.openChannelForAppend( path ); | ||||
|             } | ||||
|         } | ||||
|         catch( AccessDeniedException e ) | ||||
|         { | ||||
|             throw new FileSystemException( "Access denied" ); | ||||
|         } | ||||
|         catch( IOException e ) | ||||
|         { | ||||
|             throw localExceptionOf( e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private String toLocal( String path ) | ||||
|     { | ||||
|         return FileSystem.toLocal( path, location ); | ||||
|     } | ||||
|  | ||||
|     private FileSystemException localExceptionOf( IOException e ) | ||||
|     { | ||||
|         if( !location.isEmpty() && e instanceof FileOperationException ) | ||||
|         { | ||||
|             FileOperationException ex = (FileOperationException) e; | ||||
|             if( ex.getFilename() != null ) return localExceptionOf( ex.getFilename(), ex.getMessage() ); | ||||
|         } | ||||
|  | ||||
|         return new FileSystemException( e.getMessage() ); | ||||
|     } | ||||
|  | ||||
|     private FileSystemException localExceptionOf( String path, String message ) | ||||
|     { | ||||
|         if( !location.isEmpty() ) path = path.isEmpty() ? location : location + "/" + path; | ||||
|         return exceptionOf( path, message ); | ||||
|     } | ||||
|  | ||||
|     private static FileSystemException exceptionOf( String path, String message ) | ||||
|     { | ||||
|         return new FileSystemException( "/" + path + ": " + message ); | ||||
|     } | ||||
| } | ||||
| @@ -11,43 +11,42 @@ import javax.annotation.Nonnull; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.nio.channels.ReadableByteChannel; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.util.List; | ||||
|  | ||||
| public class SubMount implements IMount | ||||
| { | ||||
|     private IMount m_parent; | ||||
|     private String m_subPath; | ||||
|     private IMount parent; | ||||
|     private String subPath; | ||||
|  | ||||
|     public SubMount( IMount parent, String subPath ) | ||||
|     { | ||||
|         m_parent = parent; | ||||
|         m_subPath = subPath; | ||||
|         this.parent = parent; | ||||
|         this.subPath = subPath; | ||||
|     } | ||||
|  | ||||
|     // IMount implementation | ||||
|  | ||||
|     @Override | ||||
|     public boolean exists( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         return m_parent.exists( getFullPath( path ) ); | ||||
|         return parent.exists( getFullPath( path ) ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isDirectory( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         return m_parent.isDirectory( getFullPath( path ) ); | ||||
|         return parent.isDirectory( getFullPath( path ) ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException | ||||
|     { | ||||
|         m_parent.list( getFullPath( path ), contents ); | ||||
|         parent.list( getFullPath( path ), contents ); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long getSize( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         return m_parent.getSize( getFullPath( path ) ); | ||||
|         return parent.getSize( getFullPath( path ) ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
| @@ -55,25 +54,25 @@ public class SubMount implements IMount | ||||
|     @Deprecated | ||||
|     public InputStream openForRead( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         return m_parent.openForRead( getFullPath( path ) ); | ||||
|         return parent.openForRead( getFullPath( path ) ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         return m_parent.openChannelForRead( getFullPath( path ) ); | ||||
|         return parent.openChannelForRead( getFullPath( path ) ); | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     @Override | ||||
|     public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException | ||||
|     { | ||||
|         return parent.getAttributes( getFullPath( path ) ); | ||||
|     } | ||||
|  | ||||
|     private String getFullPath( String path ) | ||||
|     { | ||||
|         if( path.isEmpty() ) | ||||
|         { | ||||
|             return m_subPath; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             return m_subPath + "/" + path; | ||||
|         } | ||||
|         return path.isEmpty() ? subPath : subPath + "/" + path; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -92,6 +92,7 @@ public class CobaltLuaMachine implements ILuaMachine | ||||
|         m_globals.load( state, new MathLib() ); | ||||
|         m_globals.load( state, new CoroutineLib() ); | ||||
|         m_globals.load( state, new Bit32Lib() ); | ||||
|         m_globals.load( state, new Utf8Lib() ); | ||||
|         if( ComputerCraft.debug_enable ) m_globals.load( state, new DebugLib() ); | ||||
|  | ||||
|         // Remove globals we don't want to expose | ||||
|   | ||||
| @@ -5,8 +5,9 @@ | ||||
|  */ | ||||
| package dan200.computercraft.shared.peripheral.monitor; | ||||
|  | ||||
| import com.mojang.blaze3d.platform.GlStateManager; | ||||
| import dan200.computercraft.client.gui.FixedWidthFontRenderer; | ||||
| import dan200.computercraft.shared.common.ClientTerminal; | ||||
| import net.minecraft.client.renderer.vertex.VertexBuffer; | ||||
| import net.minecraft.util.math.BlockPos; | ||||
| import net.minecraftforge.api.distmarker.Dist; | ||||
| import net.minecraftforge.api.distmarker.OnlyIn; | ||||
| @@ -15,7 +16,7 @@ import java.util.HashSet; | ||||
| import java.util.Iterator; | ||||
| import java.util.Set; | ||||
|  | ||||
| public class ClientMonitor extends ClientTerminal | ||||
| public final class ClientMonitor extends ClientTerminal | ||||
| { | ||||
|     private static final Set<ClientMonitor> allMonitors = new HashSet<>(); | ||||
|  | ||||
| @@ -23,7 +24,8 @@ public class ClientMonitor extends ClientTerminal | ||||
|  | ||||
|     public long lastRenderFrame = -1; | ||||
|     public BlockPos lastRenderPos = null; | ||||
|     public int[] renderDisplayLists = null; | ||||
|  | ||||
|     public VertexBuffer buffer; | ||||
|  | ||||
|     public ClientMonitor( boolean colour, TileMonitor origin ) | ||||
|     { | ||||
| @@ -36,41 +38,59 @@ public class ClientMonitor extends ClientTerminal | ||||
|         return origin; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create the appropriate buffer if needed. | ||||
|      * | ||||
|      * @param renderer The renderer to use. This can be fetched from {@link MonitorRenderer#current()}. | ||||
|      * @return If a buffer was created. This will return {@code false} if we already have an appropriate buffer, | ||||
|      * or this mode does not require one. | ||||
|      */ | ||||
|     @OnlyIn( Dist.CLIENT ) | ||||
|     public void createLists() | ||||
|     public boolean createBuffer( MonitorRenderer renderer ) | ||||
|     { | ||||
|         if( renderDisplayLists == null ) | ||||
|         switch( renderer ) | ||||
|         { | ||||
|             renderDisplayLists = new int[3]; | ||||
|             case VBO: | ||||
|                 if( buffer != null ) return false; | ||||
|  | ||||
|             for( int i = 0; i < renderDisplayLists.length; i++ ) | ||||
|             { | ||||
|                 renderDisplayLists[i] = GlStateManager.genLists( 1 ); | ||||
|             } | ||||
|                 deleteBuffers(); | ||||
|                 buffer = new VertexBuffer( FixedWidthFontRenderer.POSITION_COLOR_TEX ); | ||||
|                 addMonitor(); | ||||
|                 return true; | ||||
|  | ||||
|             synchronized( allMonitors ) | ||||
|             { | ||||
|                 allMonitors.add( this ); | ||||
|             } | ||||
|             default: | ||||
|                 return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void addMonitor() | ||||
|     { | ||||
|         synchronized( allMonitors ) | ||||
|         { | ||||
|             allMonitors.add( this ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void deleteBuffers() | ||||
|     { | ||||
|         if( buffer != null ) | ||||
|         { | ||||
|             buffer.deleteGlBuffers(); | ||||
|             buffer = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @OnlyIn( Dist.CLIENT ) | ||||
|     public void destroy() | ||||
|     { | ||||
|         if( renderDisplayLists != null ) | ||||
|         if( buffer != null ) | ||||
|         { | ||||
|             synchronized( allMonitors ) | ||||
|             { | ||||
|                 allMonitors.remove( this ); | ||||
|             } | ||||
|  | ||||
|             for( int list : renderDisplayLists ) | ||||
|             { | ||||
|                 GlStateManager.deleteLists( list, 1 ); | ||||
|             } | ||||
|  | ||||
|             renderDisplayLists = null; | ||||
|             deleteBuffers(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -82,14 +102,7 @@ public class ClientMonitor extends ClientTerminal | ||||
|             for( Iterator<ClientMonitor> iterator = allMonitors.iterator(); iterator.hasNext(); ) | ||||
|             { | ||||
|                 ClientMonitor monitor = iterator.next(); | ||||
|                 if( monitor.renderDisplayLists != null ) | ||||
|                 { | ||||
|                     for( int list : monitor.renderDisplayLists ) | ||||
|                     { | ||||
|                         GlStateManager.deleteLists( list, 1 ); | ||||
|                     } | ||||
|                     monitor.renderDisplayLists = null; | ||||
|                 } | ||||
|                 monitor.deleteBuffers(); | ||||
|  | ||||
|                 iterator.remove(); | ||||
|             } | ||||
|   | ||||
| @@ -0,0 +1,98 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
|  | ||||
| package dan200.computercraft.shared.peripheral.monitor; | ||||
|  | ||||
| import com.mojang.blaze3d.platform.GLX; | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import dan200.computercraft.client.render.TileEntityMonitorRenderer; | ||||
|  | ||||
| import javax.annotation.Nonnull; | ||||
| import java.util.Locale; | ||||
|  | ||||
| /** | ||||
|  * The render type to use for monitors. | ||||
|  * | ||||
|  * @see TileEntityMonitorRenderer | ||||
|  * @see ClientMonitor | ||||
|  */ | ||||
| public enum MonitorRenderer | ||||
| { | ||||
|     /** | ||||
|      * Determine the best monitor backend. | ||||
|      */ | ||||
|     BEST, | ||||
|  | ||||
|     /** | ||||
|      * Render using VBOs. | ||||
|      * | ||||
|      * @see net.minecraft.client.renderer.vertex.VertexBuffer | ||||
|      */ | ||||
|     VBO; | ||||
|  | ||||
|     private static final MonitorRenderer[] VALUES = values(); | ||||
|     public static final String[] NAMES; | ||||
|  | ||||
|     private final String displayName = "gui.computercraft:config.peripheral.monitor_renderer." + name().toLowerCase( Locale.ROOT ); | ||||
|  | ||||
|     static | ||||
|     { | ||||
|         NAMES = new String[VALUES.length]; | ||||
|         for( int i = 0; i < VALUES.length; i++ ) NAMES[i] = VALUES[i].displayName(); | ||||
|     } | ||||
|  | ||||
|     public String displayName() | ||||
|     { | ||||
|         return displayName; | ||||
|     } | ||||
|  | ||||
|     @Nonnull | ||||
|     public static MonitorRenderer ofString( String name ) | ||||
|     { | ||||
|         for( MonitorRenderer backend : VALUES ) | ||||
|         { | ||||
|             if( backend.displayName.equalsIgnoreCase( name ) || backend.name().equalsIgnoreCase( name ) ) | ||||
|             { | ||||
|                 return backend; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         ComputerCraft.log.warn( "Unknown monitor renderer {}. Falling back to default.", name ); | ||||
|         return BEST; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the current renderer to use. | ||||
|      * | ||||
|      * @return The current renderer. Will not return {@link MonitorRenderer#BEST}. | ||||
|      */ | ||||
|     @Nonnull | ||||
|     public static MonitorRenderer current() | ||||
|     { | ||||
|         MonitorRenderer current = ComputerCraft.monitorRenderer; | ||||
|         switch( current ) | ||||
|         { | ||||
|             case BEST: | ||||
|                 return best(); | ||||
|             case VBO: | ||||
|                 if( !GLX.useVbo() ) | ||||
|                 { | ||||
|                     ComputerCraft.log.warn( "VBOs are not supported on your graphics card. Falling back to default." ); | ||||
|                     ComputerCraft.monitorRenderer = BEST; | ||||
|                     return best(); | ||||
|                 } | ||||
|  | ||||
|                 return VBO; | ||||
|             default: | ||||
|                 return current; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static MonitorRenderer best() | ||||
|     { | ||||
|         return VBO; | ||||
|     } | ||||
| } | ||||
| @@ -48,13 +48,13 @@ public class Palette | ||||
|     { | ||||
|         if( i >= 0 && i < colours.length ) | ||||
|         { | ||||
|             setColour( i, Colour.values()[i] ); | ||||
|             setColour( i, Colour.VALUES[i] ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void resetColours() | ||||
|     { | ||||
|         for( int i = 0; i < Colour.values().length; i++ ) | ||||
|         for( int i = 0; i < Colour.VALUES.length; i++ ) | ||||
|         { | ||||
|             resetColour( i ); | ||||
|         } | ||||
|   | ||||
							
								
								
									
										48
									
								
								src/main/resources/assets/computercraft/lang/da_dk.lang
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/main/resources/assets/computercraft/lang/da_dk.lang
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| tile.computercraft:computer.name=Computer | ||||
| tile.computercraft:advanced_computer.name=Avanceret Computer | ||||
| tile.computercraft:drive.name=Diskdrev | ||||
| tile.computercraft:printer.name=Printer | ||||
| tile.computercraft:monitor.name=Skærm | ||||
| tile.computercraft:advanced_monitor.name=Avanceret Skærm | ||||
| tile.computercraft:wireless_modem.name=Trådløst Modem | ||||
| tile.computercraft:wired_modem.name=Kablet Modem | ||||
| tile.computercraft:cable.name=Netværkskabel | ||||
| tile.computercraft:command_computer.name=Kommandocomputer | ||||
| tile.computercraft:advanced_modem.name=Endermodem | ||||
| tile.computercraft:speaker.name=Højttaler | ||||
|  | ||||
| tile.computercraft:turtle.name=Turtle | ||||
| tile.computercraft:turtle.upgraded.name=%s Turtle | ||||
| tile.computercraft:turtle.upgraded_twice.name=%s %s Turtle | ||||
| tile.computercraft:advanced_turtle.name=Avanceret Turtle | ||||
| tile.computercraft:advanced_turtle.upgraded.name=Avanceret %s Turtle | ||||
| tile.computercraft:advanced_turtle.upgraded_twice.name=Avanceret %s %s Turtle | ||||
|  | ||||
| item.computercraft:disk.name=Floppydisk | ||||
| item.computercraft:treasure_disk.name=Floppydisk | ||||
| item.computercraft:page.name=Printet Side | ||||
| item.computercraft:pages.name=Printede Sider | ||||
| item.computercraft:book.name=Printet Bog | ||||
|  | ||||
| item.computercraft:pocket_computer.name=Lommecomputer | ||||
| item.computercraft:pocket_computer.upgraded.name=%s Lommecomputer | ||||
| item.computercraft:advanced_pocket_computer.name=Avanceret Lommecomputer | ||||
| item.computercraft:advanced_pocket_computer.upgraded.name=Avanceret %s Lommecomputer | ||||
|  | ||||
| upgrade.minecraft:diamond_sword.adjective=Kæmpende | ||||
| upgrade.minecraft:diamond_shovel.adjective=Gravende | ||||
| upgrade.minecraft:diamond_pickaxe.adjective=Brydende | ||||
| upgrade.minecraft:diamond_axe.adjective=Fældende | ||||
| upgrade.minecraft:diamond_hoe.adjective=Dyrkende | ||||
| upgrade.computercraft:wireless_modem.adjective=Trådløs | ||||
| upgrade.minecraft:crafting_table.adjective=Fremstillende | ||||
| upgrade.computercraft:advanced_modem.adjective=Endertrådløs | ||||
| upgrade.computercraft:speaker.adjective=Larmende | ||||
|  | ||||
| chat.computercraft.wired_modem.peripheral_connected=Perifer enhed "%s" koblet til netværk | ||||
| chat.computercraft.wired_modem.peripheral_disconnected=Perifer enhed "%s" koblet fra netværk | ||||
|  | ||||
| # Misc tooltips | ||||
| gui.computercraft.tooltip.copy=Kopier til udklipsholder | ||||
| gui.computercraft.tooltip.computer_id=(Computer-ID: %s) | ||||
| gui.computercraft.tooltip.disk_id=(Disk-ID: %s) | ||||
							
								
								
									
										195
									
								
								src/main/resources/assets/computercraft/lang/ko_kr.lang
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								src/main/resources/assets/computercraft/lang/ko_kr.lang
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | ||||
| itemGroup.computercraft=컴퓨터크래프트 | ||||
|  | ||||
| tile.computercraft:computer.name=컴퓨터 | ||||
| tile.computercraft:advanced_computer.name=고급 컴퓨터 | ||||
| tile.computercraft:drive.name=디스크 드라이브 | ||||
| tile.computercraft:printer.name=프린터 | ||||
| tile.computercraft:monitor.name=모니터 | ||||
| tile.computercraft:advanced_monitor.name=고급 모니터 | ||||
| tile.computercraft:wireless_modem.name=무선 모뎀 | ||||
| tile.computercraft:wired_modem.name=유선 모뎀 | ||||
| tile.computercraft:cable.name=네트워크 케이블 | ||||
| tile.computercraft:command_computer.name=명령 컴퓨터 | ||||
| tile.computercraft:advanced_modem.name=엔더 모뎀 | ||||
| tile.computercraft:speaker.name=스피커 | ||||
|  | ||||
| tile.computercraft:turtle.name=터틀 | ||||
| tile.computercraft:turtle.upgraded.name=%s 터틀 | ||||
| tile.computercraft:turtle.upgraded_twice.name=%s %s 터틀 | ||||
| tile.computercraft:advanced_turtle.name=고급 터틀 | ||||
| tile.computercraft:advanced_turtle.upgraded.name=고급 %s 터틀 | ||||
| tile.computercraft:advanced_turtle.upgraded_twice.name=고급 %s %s 터틀 | ||||
|  | ||||
| item.computercraft:disk.name=플로피 디스크 | ||||
| item.computercraft:treasure_disk.name=플로피 디스크 | ||||
| item.computercraft:page.name=인쇄된 페이지 | ||||
| item.computercraft:pages.name=인쇄된 페이지 모음 | ||||
| item.computercraft:book.name=인쇄된 책 | ||||
|  | ||||
| item.computercraft:pocket_computer.name=포켓 컴퓨터 | ||||
| item.computercraft:pocket_computer.upgraded.name=%s 포켓 컴퓨터 | ||||
| item.computercraft:advanced_pocket_computer.name=고급 포켓 컴퓨터 | ||||
| item.computercraft:advanced_pocket_computer.upgraded.name=고급 %s 포켓 컴퓨터 | ||||
|  | ||||
| upgrade.minecraft:diamond_sword.adjective=난투 | ||||
| upgrade.minecraft:diamond_shovel.adjective=굴착 | ||||
| upgrade.minecraft:diamond_pickaxe.adjective=채굴 | ||||
| upgrade.minecraft:diamond_axe.adjective=벌목 | ||||
| upgrade.minecraft:diamond_hoe.adjective=농업 | ||||
| upgrade.computercraft:wireless_modem.adjective=무선 | ||||
| upgrade.minecraft:crafting_table.adjective=조합 | ||||
| upgrade.computercraft:advanced_modem.adjective=엔더 | ||||
| upgrade.computercraft:speaker.adjective=소음 | ||||
|  | ||||
| chat.computercraft.wired_modem.peripheral_connected=주변 "%s"이 네트워크에 연결되었습니다. | ||||
| chat.computercraft.wired_modem.peripheral_disconnected=주변 "%s"이 네트워크로부터 분리되었습니다. | ||||
|  | ||||
| # Command descriptions, usage and any additional messages. | ||||
| commands.computercraft.synopsis=컴퓨터를 제어하기 위한 다양한 명령어 | ||||
| commands.computercraft.desc=/computercraft 명령어는 컴퓨터를 제어하고 상호작용하기 위한 다양한 디버깅 및 관리자 도구를 제공합니다. | ||||
|  | ||||
| commands.computercraft.help.synopsis=특정 명령어에 대한 도움말을 제공하기 | ||||
| commands.computercraft.help.desc= | ||||
| commands.computercraft.help.usage=[command] | ||||
| commands.computercraft.help.no_children=%s에는 하위 명령어가 없습니다. | ||||
| commands.computercraft.help.no_command='%s'라는 명령어가 없습니다. | ||||
|  | ||||
| commands.computercraft.dump.synopsis=컴퓨터의 상태를 보여주기 | ||||
| commands.computercraft.dump.desc=모든 시스템의 상태 또는 한 시스템에 대한 특정 정보를 표시합니다. 컴퓨터의 인스턴스 ID(예: 123)나 컴퓨터 ID(예: #123) 또는 라벨(예: "@My Computer")을 지정할 수 있습니다. | ||||
| commands.computercraft.dump.usage=[id] | ||||
| commands.computercraft.dump.action=이 컴퓨터에 대한 추가 정보를 봅니다. | ||||
|  | ||||
| commands.computercraft.shutdown.synopsis=시스템을 원격으로 종료하기 | ||||
| commands.computercraft.shutdown.desc=나열된 시스템 또는 지정된 시스템이 없는 경우 모두 종료합니다. 컴퓨터의 인스턴스 ID(예: 123)나 컴퓨터 ID(예: #123) 또는 라벨(예: "@My Computer")을 지정할 수 있습니다. | ||||
| commands.computercraft.shutdown.usage=[ids...] | ||||
| commands.computercraft.shutdown.done=%s/%s 컴퓨터 시스템 종료 | ||||
|  | ||||
| commands.computercraft.turn_on.synopsis=시스템을 원격으로 실행하기 | ||||
| commands.computercraft.turn_on.desc=나열된 컴퓨터를 실행합니다. 컴퓨터의 인스턴스 ID(예: 123)나 컴퓨터 ID(예: #123) 또는 라벨(예: "@My Computer")을 지정할 수 있습니다. | ||||
| commands.computercraft.turn_on.usage=[ids...] | ||||
| commands.computercraft.turn_on.done=%s/%s 컴퓨터 시스템 실행 | ||||
|  | ||||
| commands.computercraft.tp.synopsis=특정 컴퓨터로 순간이동하기 | ||||
| commands.computercraft.tp.desc=컴퓨터의 위치로 순간이동합니다. 컴퓨터의 인스턴스 ID(예: 123) 또는 컴퓨터 ID(예: #123)를 지정할 수 있습니다. | ||||
| commands.computercraft.tp.usage=<id> | ||||
| commands.computercraft.tp.action=이 컴퓨터로 순간이동하기 | ||||
| commands.computercraft.tp.not_entity=비플레이어한테 터미널을 열 수 없습니다. | ||||
| commands.computercraft.tp.not_there=월드에서 컴퓨터를 위치시킬 수 없습니다. | ||||
|  | ||||
| commands.computercraft.view.synopsis=컴퓨터의 터미널을 보기 | ||||
| commands.computercraft.view.desc=컴퓨터의 원격 제어를 허용하는 컴퓨터의 터미널을 엽니다. 이것은 터틀의 인벤토리에 대한 접근을 제공하지 않습니다. 컴퓨터의 인스턴스 ID(예: 123) 또는 컴퓨터 ID(예: #123)를 지정할 수 있습니다. | ||||
| commands.computercraft.view.usage=<id> | ||||
| commands.computercraft.view.action=이 컴퓨터를 봅니다. | ||||
| commands.computercraft.view.not_player=비플레이어한테 터미널을 열 수 없습니다. | ||||
|  | ||||
| commands.computercraft.track.synopsis=컴퓨터의 실행 시간을 추적하기 | ||||
| commands.computercraft.track.desc=컴퓨터가 실행되는 기간과 처리되는 이벤트 수를 추적합니다. 이는 /forge 트랙과 유사한 방법으로 정보를 제공하며 지연 로그에 유용할 수 있습니다. | ||||
|  | ||||
| commands.computercraft.track.start.synopsis=모든 컴퓨터의 추적을 시작하기 | ||||
| commands.computercraft.track.start.desc=모든 컴퓨터의 이벤트 및 실행 시간 추적을 시작합니다. 이는 이전 실행의 결과를 폐기할 것입니다. | ||||
| commands.computercraft.track.start.usage= | ||||
| commands.computercraft.track.start.stop=%s을(를) 실행하여 추적을 중지하고 결과를 확인합니다. | ||||
|  | ||||
| commands.computercraft.track.stop.synopsis=모든 컴퓨터의 추적을 중지하기 | ||||
| commands.computercraft.track.stop.desc=모든 컴퓨터의 이벤트 및 실행 시간 추적을 중지합니다. | ||||
| commands.computercraft.track.stop.usage= | ||||
| commands.computercraft.track.stop.action=추적을 중지하려면 클릭하세요. | ||||
| commands.computercraft.track.stop.not_enabled=현재 추적하는 컴퓨터가 없습니다. | ||||
|  | ||||
| commands.computercraft.track.dump.synopsis=최신 추적 결과를 덤프하기 | ||||
| commands.computercraft.track.dump.desc=최신 컴퓨터 추적의 결과를 덤프합니다. | ||||
| commands.computercraft.track.dump.usage=[kind] | ||||
| commands.computercraft.track.dump.no_timings=사용가능한 시간이 없습니다. | ||||
| commands.computercraft.track.dump.no_field=알 수 없는 필드 '%s' | ||||
| commands.computercraft.track.dump.computer=컴퓨터 | ||||
|  | ||||
| commands.computercraft.reload.synopsis=컴퓨터크래프트 구성파일을 리로드하기 | ||||
| commands.computercraft.reload.desc=컴퓨터크래프트 구성파일을 리로드합니다. | ||||
| commands.computercraft.reload.usage= | ||||
| commands.computercraft.reload.done=리로드된 구성 | ||||
|  | ||||
| commands.computercraft.queue.synopsis=computer_command 이벤트를 명령 컴퓨터에 보내기 | ||||
| commands.computercraft.queue.desc=computer_command 이벤트를 명령 컴퓨터로 전송하여 추가 인수를 전달합니다. 이는 대부분 지도 제작자를 위해 설계되었으며, 보다 컴퓨터 친화적인 버전의 /trigger 역할을 합니다. 어떤 플레이어든 명령을 실행할 수 있으며, 이는 텍스트 구성 요소의 클릭 이벤트를 통해 수행될 가능성이 가장 높습니다. | ||||
| commands.computercraft.queue.usage=<id> [args...] | ||||
|  | ||||
| commands.computercraft.generic.no_position=<no pos> | ||||
| commands.computercraft.generic.position=%s, %s, %s | ||||
| commands.computercraft.generic.yes=Y | ||||
| commands.computercraft.generic.no=N | ||||
| commands.computercraft.generic.exception=처리되지 않은 예외 (%s) | ||||
| commands.computercraft.generic.additional_rows=%d개의 추가 행... | ||||
|  | ||||
| commands.computercraft.argument.no_matching='%s'와 일치하는 컴퓨터가 없습니다. | ||||
| commands.computercraft.argument.many_matching='%s'와 일치하는 여러 컴퓨터 (인스턴스 %s) | ||||
| commands.computercraft.argument.not_number='%s'는 숫자가 아닙니다. | ||||
|  | ||||
| # Names for the various tracking fields. | ||||
| tracking_field.computercraft.tasks.name=작업 | ||||
| tracking_field.computercraft.total.name=전체 시간 | ||||
| tracking_field.computercraft.average.name=평균 시간 | ||||
| tracking_field.computercraft.max.name=최대 시간 | ||||
|  | ||||
| tracking_field.computercraft.server_count.name=서버 작업 수 | ||||
| tracking_field.computercraft.server_time.name=서버 작업 시간 | ||||
|  | ||||
| tracking_field.computercraft.peripheral.name=주변 호출 | ||||
| tracking_field.computercraft.fs.name=파일시스템 작업 | ||||
| tracking_field.computercraft.turtle.name=터틀 작업 | ||||
|  | ||||
| tracking_field.computercraft.http.name=HTTP 요청 | ||||
| tracking_field.computercraft.http_upload.name=HTTP 업로드 | ||||
| tracking_field.computercraft.http_download.name=HTTT 다운로드 | ||||
|  | ||||
| tracking_field.computercraft.websocket_incoming.name=웹소켓 수신 | ||||
| tracking_field.computercraft.websocket_outgoing.name=웹소켓 송신 | ||||
|  | ||||
| tracking_field.computercraft.coroutines_created.name=코루틴 생성됨 | ||||
| tracking_field.computercraft.coroutines_dead.name=코루틴 처리됨 | ||||
|  | ||||
| # Misc tooltips | ||||
| gui.computercraft.tooltip.copy=클립보드에 복사 | ||||
| gui.computercraft.tooltip.computer_id=(컴퓨터 ID: %s) | ||||
| gui.computercraft.tooltip.disk_id=(디스크 ID: %s) | ||||
|  | ||||
| # Config options | ||||
| gui.computercraft:config.computer_space_limit=컴퓨터 공간 제한 (바이트) | ||||
| gui.computercraft:config.floppy_space_limit=플로피 디스크 공간 제한 (바이트) | ||||
| gui.computercraft:config.maximum_open_files=컴퓨터당 최대 파일 열기 | ||||
| gui.computercraft:config.disable_lua51_features=Lua 5.1 기능 미사용 | ||||
| gui.computercraft:config.default_computer_settings=기본 컴퓨터 설정 | ||||
| gui.computercraft:config.debug_enabled=디버그 라이브러리 사용 | ||||
| gui.computercraft:config.log_computer_errors=컴퓨터 오류 로그 | ||||
|  | ||||
| gui.computercraft:config.execution=실행 | ||||
| gui.computercraft:config.execution.computer_threads=컴퓨터 쓰레드 | ||||
| gui.computercraft:config.execution.max_main_global_time=전역 시간 당 서버 제한 | ||||
| gui.computercraft:config.execution.max_main_computer_time=컴퓨터 시간 당 서버 제한 | ||||
|  | ||||
| gui.computercraft:config.http=HTTP | ||||
| gui.computercraft:config.http.enabled=HTTP API 사용하기 | ||||
| gui.computercraft:config.http.websocket_enabled=웹소켓 사용 | ||||
| gui.computercraft:config.http.allowed_domains=허용된 도메인 | ||||
| gui.computercraft:config.http.blocked_domains=차단된 도메인 | ||||
|  | ||||
| gui.computercraft:config.http.timeout=타임아웃 | ||||
| gui.computercraft:config.http.max_requests=최대 동시 요청 수 | ||||
| gui.computercraft:config.http.max_download=최대 응답 크기 | ||||
| gui.computercraft:config.http.max_upload=최대 요청 크기 | ||||
| gui.computercraft:config.http.max_websockets=최대 동시 웹소켓 수 | ||||
| gui.computercraft:config.http.max_websocket_message=최대 웹 포켓 메시지 크기 | ||||
|  | ||||
| gui.computercraft:config.peripheral=주변 | ||||
| gui.computercraft:config.peripheral.command_block_enabled=명령 블록 주변 장치 사용 | ||||
| gui.computercraft:config.peripheral.modem_range=모뎀 범위(기본값) | ||||
| gui.computercraft:config.peripheral.modem_high_altitude_range=모뎀 범위(높은 고도) | ||||
| gui.computercraft:config.peripheral.modem_range_during_storm=모뎀 범위(나쁜 날씨) | ||||
| gui.computercraft:config.peripheral.modem_high_altitude_range_during_storm=모뎀 범위(높은 고도, 나쁜 날씨) | ||||
| gui.computercraft:config.peripheral.max_notes_per_tick=컴퓨터가 한 번에 재생할 수 있는 최대 소리 수 | ||||
|  | ||||
| gui.computercraft:config.turtle=터틀 | ||||
| gui.computercraft:config.turtle.need_fuel=연료 사용 | ||||
| gui.computercraft:config.turtle.normal_fuel_limit=터틀 연료 제한 | ||||
| gui.computercraft:config.turtle.advanced_fuel_limit=고급 터틀 연료 제한 | ||||
| gui.computercraft:config.turtle.obey_block_protection=터틀이 블록 보호에 따르기 | ||||
| gui.computercraft:config.turtle.can_push=터틀이 엔티티 밀어내기 | ||||
| gui.computercraft:config.turtle.disabled_actions=터틀 액션 미사용 | ||||
| @@ -10,7 +10,7 @@ | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 2, 13, 14, 16 ], "texture": "#front" }, | ||||
|                 "up": { "uv": [ 2, 0, 14, 3 ], "texture": "#front" }, | ||||
|                 "north": { "uv": [ 2, 2, 14, 14 ], "texture": "#back" }, | ||||
|                 "north": { "uv": [ 2, 2, 14, 14 ], "texture": "#back", "cullface": "north" }, | ||||
|                 "south": { "uv": [ 2, 2, 14, 14 ], "texture": "#front" }, | ||||
|                 "west": { "uv": [ 0, 2, 3, 14 ], "texture": "#front" }, | ||||
|                 "east": { "uv": [ 13, 2, 16, 14 ], "texture": "#front" } | ||||
|   | ||||
| @@ -8,23 +8,23 @@ | ||||
|             "from": [ 2, 2, 2 ], | ||||
|             "to": [ 14, 14, 13 ], | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 2.75, 0, 5.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 5.75, 0, 8.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "north": { "uv": [ 8.5, 5.75, 11.5, 2.75 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 2.75, 5.75, 5.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 0, 5.75, 2.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 5.75, 5.75, 8.5, 2.75 ], "texture": "#texture" } | ||||
|                 "down": { "uv": [ 5.75, 2.75, 2.75, 0 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 8.75, 0, 5.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "north": { "uv": [ 11.5, 5.75, 8.5, 2.75 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 5.75, 5.75, 2.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 8.5, 5.75, 5.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 2.75, 5.75, 0, 2.75 ], "texture": "#texture" } | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             "from": [ 3, 6, 13 ], | ||||
|             "to": [ 13, 13, 15 ], | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 9.25, 0, 11.75, 0.5 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 11.75, 0, 14.25, 0.5 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 9.25, 2.25, 11.75, 0.5 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 8.75, 2.25, 9.25, 0.5 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 11.75, 2.25, 12.25, 0.5 ], "texture": "#texture" } | ||||
|                 "down": { "uv": [ 11.75, 0.5, 9.25, 0 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 14.25, 0, 11.75, 0.5 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 11.75, 2.25, 9.25, 0.5 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 12.25, 2.25, 11.75, 0.5 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 9.25, 2.25, 8.75, 0.5 ], "texture": "#texture" } | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|   | ||||
| @@ -8,46 +8,46 @@ | ||||
|             "from": [ 2, 2, 2 ], | ||||
|             "to": [ 14, 14, 13 ], | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 2.75, 5.75, 5.75, 8.5 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "up": { "uv": [ 5.75, 5.75, 8.75, 8.5 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "north": { "uv": [ 8.5, 11.5, 11.5, 8.5 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "south": { "uv": [ 2.75, 11.5, 5.75, 8.5 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "west": { "uv": [ 0, 11.5, 2.75, 8.5 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "east": { "uv": [ 5.75, 11.5, 8.5, 8.555 ], "texture": "#texture", "tintindex": 0 } | ||||
|                 "down": { "uv": [ 5.75, 8.5, 2.75, 5.75 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "up": { "uv": [ 8.75, 5.75, 5.75, 8.5 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "north": { "uv": [ 11.5, 11.5, 8.5, 8.5 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "south": { "uv": [ 5.75, 11.5, 2.75, 8.5 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "west": { "uv": [ 8.5, 11.5, 5.75, 8.555 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "east": { "uv": [ 2.75, 11.5, 0, 8.5 ], "texture": "#texture", "tintindex": 0 } | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             "from": [ 3, 6, 13 ], | ||||
|             "to": [ 13, 13, 15 ], | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 9.25, 5.75, 11.75, 6.25 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "up": { "uv": [ 11.75, 5.75, 14.25, 6.25 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "south": { "uv": [ 9.25, 8, 11.75, 6.25 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "west": { "uv": [ 8.75, 8, 9.25, 6.25 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "east": { "uv": [ 11.75, 8, 12.25, 6.25 ], "texture": "#texture", "tintindex": 0 } | ||||
|                 "down": { "uv": [ 11.75, 6.25, 9.25, 5.75 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "up": { "uv": [ 14.25, 5.75, 11.75, 6.25 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "south": { "uv": [ 11.75, 8, 9.25, 6.25 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "west": { "uv": [ 12.25, 8, 11.75, 6.25 ], "texture": "#texture", "tintindex": 0 }, | ||||
|                 "east": { "uv": [ 9.25, 8, 8.75, 6.25 ], "texture": "#texture", "tintindex": 0 } | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             "from": [ 2, 2, 2 ], | ||||
|             "to": [ 14, 14, 13 ], | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 2.75, 0, 5.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 5.75, 0, 8.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "north": { "uv": [ 8.5, 5.75, 11.5, 2.75 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 2.75, 5.75, 5.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 0, 5.75, 2.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 5.75, 5.75, 8.5, 2.75 ], "texture": "#texture" } | ||||
|                 "down": { "uv": [ 5.75, 2.75, 2.75, 0 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 8.75, 0, 5.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "north": { "uv": [ 11.5, 5.75, 8.5, 2.75 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 5.75, 5.75, 2.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 8.5, 5.75, 5.75, 2.75 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 2.75, 5.75, 0, 2.75 ], "texture": "#texture" } | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             "from": [ 3, 6, 13 ], | ||||
|             "to": [ 13, 13, 15 ], | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 9.25, 0, 11.75, 0.5 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 11.75, 0, 14.25, 0.5 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 9.25, 2.25, 11.75, 0.5 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 8.75, 2.25, 9.25, 0.5 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 11.75, 2.25, 12.25, 0.5 ], "texture": "#texture" } | ||||
|                 "down": { "uv": [ 11.75, 0.5, 9.25, 0 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 14.25, 0, 11.75, 0.5 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 11.75, 2.25, 9.25, 0.5 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 12.25, 2.25, 11.75, 0.5 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 9.25, 2.25, 8.75, 0.5 ], "texture": "#texture" } | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|   | ||||
| @@ -8,8 +8,8 @@ | ||||
|             "from": [ 0.5, 4.5, 3.5 ], | ||||
|             "to": [ 2, 12.5, 11.5 ], | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 13, 2, 16, 14 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 13, 2, 16, 14 ], "texture": "#texture" }, | ||||
|                 "down": { "uv": [ 2, 14, 14, 16 ], "texture": "#texture", "rotation": 270 }, | ||||
|                 "up": { "uv": [ 2, 0, 14, 3 ], "texture": "#texture", "rotation": 90 }, | ||||
|                 "north": { "uv": [ 0, 2, 3, 14 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 13, 2, 16, 14 ], "texture": "#texture" }, | ||||
|                 "west": { "uv": [ 2, 2, 14, 14 ], "texture": "#texture" } | ||||
|   | ||||
| @@ -8,8 +8,8 @@ | ||||
|             "from": [ 14, 4.5, 3.5 ], | ||||
|             "to": [ 15.5, 12.5, 11.5 ], | ||||
|             "faces": { | ||||
|                 "down": { "uv": [ 0, 2, 3, 14 ], "texture": "#texture" }, | ||||
|                 "up": { "uv": [ 0, 2, 3, 14 ], "texture": "#texture" }, | ||||
|                 "down": { "uv": [ 2, 14, 14, 16 ], "texture": "#texture", "rotation": 90 }, | ||||
|                 "up": { "uv": [ 2, 0, 14, 3 ], "texture": "#texture", "rotation": 270 }, | ||||
|                 "north": { "uv": [ 13, 2, 16, 14 ], "texture": "#texture" }, | ||||
|                 "south": { "uv": [ 0, 2, 3, 14 ], "texture": "#texture" }, | ||||
|                 "east": { "uv": [ 2, 2, 14, 14 ], "texture": "#texture" } | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 123 B | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.8 KiB | 
| @@ -21,7 +21,7 @@ if _VERSION == "Lua 5.1" then | ||||
|     local nativeloadstring = loadstring | ||||
|     local nativesetfenv = setfenv | ||||
|  | ||||
|     --- Historically load/loadstring would handle the chunk name as if it has | ||||
|     -- Historically load/loadstring would handle the chunk name as if it has | ||||
|     -- been prefixed with "=". We emulate that behaviour here. | ||||
|     local function prefix(chunkname) | ||||
|         if type(chunkname) ~= "string" then return chunkname end | ||||
| @@ -33,45 +33,43 @@ if _VERSION == "Lua 5.1" then | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     function load( x, name, mode, env ) | ||||
|     function load(x, name, mode, env) | ||||
|         expect(1, x, "function", "string") | ||||
|         expect(2, name, "string", "nil") | ||||
|         expect(3, mode, "string", "nil") | ||||
|         expect(4, env, "table", "nil") | ||||
|  | ||||
|         local ok, p1, p2 = pcall( function() | ||||
|         local ok, p1, p2 = pcall(function() | ||||
|             if type(x) == "string" then | ||||
|                 local result, err = nativeloadstring( x, name ) | ||||
|                 local result, err = nativeloadstring(x, name) | ||||
|                 if result then | ||||
|                     if env then | ||||
|                         env._ENV = env | ||||
|                         nativesetfenv( result, env ) | ||||
|                         nativesetfenv(result, env) | ||||
|                     end | ||||
|                     return result | ||||
|                 else | ||||
|                     return nil, err | ||||
|                 end | ||||
|             else | ||||
|                 local result, err = nativeload( x, name ) | ||||
|                 local result, err = nativeload(x, name) | ||||
|                 if result then | ||||
|                     if env then | ||||
|                         env._ENV = env | ||||
|                         nativesetfenv( result, env ) | ||||
|                         nativesetfenv(result, env) | ||||
|                     end | ||||
|                     return result | ||||
|                 else | ||||
|                     return nil, err | ||||
|                 end | ||||
|             end | ||||
|         end ) | ||||
|         end) | ||||
|         if ok then | ||||
|             return p1, p2 | ||||
|         else | ||||
|             error( p1, 2 ) | ||||
|             error(p1, 2) | ||||
|         end | ||||
|     end | ||||
|     table.unpack = unpack | ||||
|     table.pack = function( ... ) return { n = select( "#", ... ), ... } end | ||||
|  | ||||
|     if _CC_DISABLE_LUA51_FEATURES then | ||||
|         -- Remove the Lua 5.1 features that will be removed when we update to Lua 5.2, for compatibility testing. | ||||
| @@ -83,7 +81,7 @@ if _VERSION == "Lua 5.1" then | ||||
|         math.log10 = nil | ||||
|         table.maxn = nil | ||||
|     else | ||||
|         loadstring = function(string, chunkname) return nativeloadstring(string, prefix( chunkname )) end | ||||
|         loadstring = function(string, chunkname) return nativeloadstring(string, prefix(chunkname)) end | ||||
|  | ||||
|         -- Inject a stub for the old bit library | ||||
|         _G.bit = { | ||||
| @@ -98,97 +96,33 @@ if _VERSION == "Lua 5.1" then | ||||
|     end | ||||
| end | ||||
|  | ||||
| if _VERSION == "Lua 5.3" and not bit32 then | ||||
|     -- If we're on Lua 5.3, install the bit32 api from Lua 5.2 | ||||
|     -- (Loaded from a string so this file will still parse on <5.3 lua) | ||||
|     load( [[ | ||||
|         bit32 = {} | ||||
|  | ||||
|         function bit32.arshift( n, bits ) | ||||
|             if type(n) ~= "number" or type(bits) ~= "number" then | ||||
|                 error( "Expected number, number", 2 ) | ||||
|             end | ||||
|             return n >> bits | ||||
|         end | ||||
|  | ||||
|         function bit32.band( m, n ) | ||||
|             if type(m) ~= "number" or type(n) ~= "number" then | ||||
|                 error( "Expected number, number", 2 ) | ||||
|             end | ||||
|             return m & n | ||||
|         end | ||||
|  | ||||
|         function bit32.bnot( n ) | ||||
|             if type(n) ~= "number" then | ||||
|                 error( "Expected number", 2 ) | ||||
|             end | ||||
|             return ~n | ||||
|         end | ||||
|  | ||||
|         function bit32.bor( m, n ) | ||||
|             if type(m) ~= "number" or type(n) ~= "number" then | ||||
|                 error( "Expected number, number", 2 ) | ||||
|             end | ||||
|             return m | n | ||||
|         end | ||||
|  | ||||
|         function bit32.btest( m, n ) | ||||
|             if type(m) ~= "number" or type(n) ~= "number" then | ||||
|                 error( "Expected number, number", 2 ) | ||||
|             end | ||||
|             return (m & n) ~= 0 | ||||
|         end | ||||
|  | ||||
|         function bit32.bxor( m, n ) | ||||
|             if type(m) ~= "number" or type(n) ~= "number" then | ||||
|                 error( "Expected number, number", 2 ) | ||||
|             end | ||||
|             return m ~ n | ||||
|         end | ||||
|  | ||||
|         function bit32.lshift( n, bits ) | ||||
|             if type(n) ~= "number" or type(bits) ~= "number" then | ||||
|                 error( "Expected number, number", 2 ) | ||||
|             end | ||||
|             return n << bits | ||||
|         end | ||||
|  | ||||
|         function bit32.rshift( n, bits ) | ||||
|             if type(n) ~= "number" or type(bits) ~= "number" then | ||||
|                 error( "Expected number, number", 2 ) | ||||
|             end | ||||
|             return n >> bits | ||||
|         end | ||||
|     ]] )() | ||||
| end | ||||
|  | ||||
| -- Install lua parts of the os api | ||||
| function os.version() | ||||
|     return "CraftOS 1.8" | ||||
| end | ||||
|  | ||||
| function os.pullEventRaw( sFilter ) | ||||
|     return coroutine.yield( sFilter ) | ||||
| function os.pullEventRaw(sFilter) | ||||
|     return coroutine.yield(sFilter) | ||||
| end | ||||
|  | ||||
| function os.pullEvent( sFilter ) | ||||
|     local eventData = table.pack( os.pullEventRaw( sFilter ) ) | ||||
| function os.pullEvent(sFilter) | ||||
|     local eventData = table.pack(os.pullEventRaw(sFilter)) | ||||
|     if eventData[1] == "terminate" then | ||||
|         error( "Terminated", 0 ) | ||||
|         error("Terminated", 0) | ||||
|     end | ||||
|     return table.unpack( eventData, 1, eventData.n ) | ||||
|     return table.unpack(eventData, 1, eventData.n) | ||||
| end | ||||
|  | ||||
| -- Install globals | ||||
| function sleep( nTime ) | ||||
| function sleep(nTime) | ||||
|     expect(1, nTime, "number", "nil") | ||||
|     local timer = os.startTimer( nTime or 0 ) | ||||
|     local timer = os.startTimer(nTime or 0) | ||||
|     repeat | ||||
|         local _, param = os.pullEvent( "timer" ) | ||||
|         local _, param = os.pullEvent("timer") | ||||
|     until param == timer | ||||
| end | ||||
|  | ||||
| function write( sText ) | ||||
| function write(sText) | ||||
|     expect(1, sText, "string", "number") | ||||
|  | ||||
|     local w, h = term.getSize() | ||||
| @@ -209,32 +143,32 @@ function write( sText ) | ||||
|     -- Print the line with proper word wrapping | ||||
|     sText = tostring(sText) | ||||
|     while #sText > 0 do | ||||
|         local whitespace = string.match( sText, "^[ \t]+" ) | ||||
|         local whitespace = string.match(sText, "^[ \t]+") | ||||
|         if whitespace then | ||||
|             -- Print whitespace | ||||
|             term.write( whitespace ) | ||||
|             term.write(whitespace) | ||||
|             x, y = term.getCursorPos() | ||||
|             sText = string.sub( sText, #whitespace + 1 ) | ||||
|             sText = string.sub(sText, #whitespace + 1) | ||||
|         end | ||||
|  | ||||
|         local newline = string.match( sText, "^\n" ) | ||||
|         local newline = string.match(sText, "^\n") | ||||
|         if newline then | ||||
|             -- Print newlines | ||||
|             newLine() | ||||
|             sText = string.sub( sText, 2 ) | ||||
|             sText = string.sub(sText, 2) | ||||
|         end | ||||
|  | ||||
|         local text = string.match( sText, "^[^ \t\n]+" ) | ||||
|         local text = string.match(sText, "^[^ \t\n]+") | ||||
|         if text then | ||||
|             sText = string.sub( sText, #text + 1 ) | ||||
|             sText = string.sub(sText, #text + 1) | ||||
|             if #text > w then | ||||
|                 -- Print a multiline word | ||||
|                 while #text > 0 do | ||||
|                     if x > w then | ||||
|                         newLine() | ||||
|                     end | ||||
|                     term.write( text ) | ||||
|                     text = string.sub( text, w - x + 2 ) | ||||
|                     term.write(text) | ||||
|                     text = string.sub(text, w - x + 2) | ||||
|                     x, y = term.getCursorPos() | ||||
|                 end | ||||
|             else | ||||
| @@ -242,7 +176,7 @@ function write( sText ) | ||||
|                 if x + #text - 1 > w then | ||||
|                     newLine() | ||||
|                 end | ||||
|                 term.write( text ) | ||||
|                 term.write(text) | ||||
|                 x, y = term.getCursorPos() | ||||
|             end | ||||
|         end | ||||
| @@ -251,42 +185,42 @@ function write( sText ) | ||||
|     return nLinesPrinted | ||||
| end | ||||
|  | ||||
| function print( ... ) | ||||
| function print(...) | ||||
|     local nLinesPrinted = 0 | ||||
|     local nLimit = select("#", ... ) | ||||
|     local nLimit = select("#", ...) | ||||
|     for n = 1, nLimit do | ||||
|         local s = tostring( select( n, ... ) ) | ||||
|         local s = tostring(select(n, ...)) | ||||
|         if n < nLimit then | ||||
|             s = s .. "\t" | ||||
|         end | ||||
|         nLinesPrinted = nLinesPrinted + write( s ) | ||||
|         nLinesPrinted = nLinesPrinted + write(s) | ||||
|     end | ||||
|     nLinesPrinted = nLinesPrinted + write( "\n" ) | ||||
|     nLinesPrinted = nLinesPrinted + write("\n") | ||||
|     return nLinesPrinted | ||||
| end | ||||
|  | ||||
| function printError( ... ) | ||||
| function printError(...) | ||||
|     local oldColour | ||||
|     if term.isColour() then | ||||
|         oldColour = term.getTextColour() | ||||
|         term.setTextColour( colors.red ) | ||||
|         term.setTextColour(colors.red) | ||||
|     end | ||||
|     print( ... ) | ||||
|     print(...) | ||||
|     if term.isColour() then | ||||
|         term.setTextColour( oldColour ) | ||||
|         term.setTextColour(oldColour) | ||||
|     end | ||||
| end | ||||
|  | ||||
| function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
| function read(_sReplaceChar, _tHistory, _fnComplete, _sDefault) | ||||
|     expect(1, _sReplaceChar, "string", "nil") | ||||
|     expect(2, _tHistory, "table", "nil") | ||||
|     expect(3, _fnComplete, "function", "nil") | ||||
|     expect(4, _sDefault, "string", "nil") | ||||
|  | ||||
|     term.setCursorBlink( true ) | ||||
|     term.setCursorBlink(true) | ||||
|  | ||||
|     local sLine | ||||
|     if type( _sDefault ) == "string" then | ||||
|     if type(_sDefault) == "string" then | ||||
|         sLine = _sDefault | ||||
|     else | ||||
|         sLine = "" | ||||
| @@ -294,14 +228,14 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|     local nHistoryPos | ||||
|     local nPos, nScroll = #sLine, 0 | ||||
|     if _sReplaceChar then | ||||
|         _sReplaceChar = string.sub( _sReplaceChar, 1, 1 ) | ||||
|         _sReplaceChar = string.sub(_sReplaceChar, 1, 1) | ||||
|     end | ||||
|  | ||||
|     local tCompletions | ||||
|     local nCompletion | ||||
|     local function recomplete() | ||||
|         if _fnComplete and nPos == #sLine then | ||||
|             tCompletions = _fnComplete( sLine ) | ||||
|             tCompletions = _fnComplete(sLine) | ||||
|             if tCompletions and #tCompletions > 0 then | ||||
|                 nCompletion = 1 | ||||
|             else | ||||
| @@ -321,7 +255,7 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|     local w = term.getSize() | ||||
|     local sx = term.getCursorPos() | ||||
|  | ||||
|     local function redraw( _bClear ) | ||||
|     local function redraw(_bClear) | ||||
|         local cursor_pos = nPos - nScroll | ||||
|         if sx + cursor_pos >= w then | ||||
|             -- We've moved beyond the RHS, ensure we're on the edge. | ||||
| @@ -332,39 +266,39 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|         end | ||||
|  | ||||
|         local _, cy = term.getCursorPos() | ||||
|         term.setCursorPos( sx, cy ) | ||||
|         term.setCursorPos(sx, cy) | ||||
|         local sReplace = _bClear and " " or _sReplaceChar | ||||
|         if sReplace then | ||||
|             term.write( string.rep( sReplace, math.max( #sLine - nScroll, 0 ) ) ) | ||||
|             term.write(string.rep(sReplace, math.max(#sLine - nScroll, 0))) | ||||
|         else | ||||
|             term.write( string.sub( sLine, nScroll + 1 ) ) | ||||
|             term.write(string.sub(sLine, nScroll + 1)) | ||||
|         end | ||||
|  | ||||
|         if nCompletion then | ||||
|             local sCompletion = tCompletions[ nCompletion ] | ||||
|             local sCompletion = tCompletions[nCompletion] | ||||
|             local oldText, oldBg | ||||
|             if not _bClear then | ||||
|                 oldText = term.getTextColor() | ||||
|                 oldBg = term.getBackgroundColor() | ||||
|                 term.setTextColor( colors.white ) | ||||
|                 term.setBackgroundColor( colors.gray ) | ||||
|                 term.setTextColor(colors.white) | ||||
|                 term.setBackgroundColor(colors.gray) | ||||
|             end | ||||
|             if sReplace then | ||||
|                 term.write( string.rep( sReplace, #sCompletion ) ) | ||||
|                 term.write(string.rep(sReplace, #sCompletion)) | ||||
|             else | ||||
|                 term.write( sCompletion ) | ||||
|                 term.write(sCompletion) | ||||
|             end | ||||
|             if not _bClear then | ||||
|                 term.setTextColor( oldText ) | ||||
|                 term.setBackgroundColor( oldBg ) | ||||
|                 term.setTextColor(oldText) | ||||
|                 term.setBackgroundColor(oldBg) | ||||
|             end | ||||
|         end | ||||
|  | ||||
|         term.setCursorPos( sx + nPos - nScroll, cy ) | ||||
|         term.setCursorPos(sx + nPos - nScroll, cy) | ||||
|     end | ||||
|  | ||||
|     local function clear() | ||||
|         redraw( true ) | ||||
|         redraw(true) | ||||
|     end | ||||
|  | ||||
|     recomplete() | ||||
| @@ -376,7 +310,7 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|             clear() | ||||
|  | ||||
|             -- Find the common prefix of all the other suggestions which start with the same letter as the current one | ||||
|             local sCompletion = tCompletions[ nCompletion ] | ||||
|             local sCompletion = tCompletions[nCompletion] | ||||
|             sLine = sLine .. sCompletion | ||||
|             nPos = #sLine | ||||
|  | ||||
| @@ -390,7 +324,7 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|         if sEvent == "char" then | ||||
|             -- Typed key | ||||
|             clear() | ||||
|             sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 ) | ||||
|             sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1) | ||||
|             nPos = nPos + 1 | ||||
|             recomplete() | ||||
|             redraw() | ||||
| @@ -398,7 +332,7 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|         elseif sEvent == "paste" then | ||||
|             -- Pasted text | ||||
|             clear() | ||||
|             sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 ) | ||||
|             sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1) | ||||
|             nPos = nPos + #param | ||||
|             recomplete() | ||||
|             redraw() | ||||
| @@ -489,7 +423,7 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|                 -- Backspace | ||||
|                 if nPos > 0 then | ||||
|                     clear() | ||||
|                     sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 ) | ||||
|                     sLine = string.sub(sLine, 1, nPos - 1) .. string.sub(sLine, nPos + 1) | ||||
|                     nPos = nPos - 1 | ||||
|                     if nScroll > 0 then nScroll = nScroll - 1 end | ||||
|                     recomplete() | ||||
| @@ -509,7 +443,7 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|                 -- Delete | ||||
|                 if nPos < #sLine then | ||||
|                     clear() | ||||
|                     sLine = string.sub( sLine, 1, nPos ) .. string.sub( sLine, nPos + 2 ) | ||||
|                     sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2) | ||||
|                     recomplete() | ||||
|                     redraw() | ||||
|                 end | ||||
| @@ -546,14 +480,14 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) | ||||
|     end | ||||
|  | ||||
|     local _, cy = term.getCursorPos() | ||||
|     term.setCursorBlink( false ) | ||||
|     term.setCursorPos( w + 1, cy ) | ||||
|     term.setCursorBlink(false) | ||||
|     term.setCursorPos(w + 1, cy) | ||||
|     print() | ||||
|  | ||||
|     return sLine | ||||
| end | ||||
|  | ||||
| function loadfile( filename, mode, env ) | ||||
| function loadfile(filename, mode, env) | ||||
|     -- Support the previous `loadfile(filename, env)` form instead. | ||||
|     if type(mode) == "table" and env == nil then | ||||
|         mode, env = nil, mode | ||||
| @@ -563,81 +497,81 @@ function loadfile( filename, mode, env ) | ||||
|     expect(2, mode, "string", "nil") | ||||
|     expect(3, env, "table", "nil") | ||||
|  | ||||
|     local file = fs.open( filename, "r" ) | ||||
|     local file = fs.open(filename, "r") | ||||
|     if not file then return nil, "File not found" end | ||||
|  | ||||
|     local func, err = load( file.readAll(), "@" .. fs.getName( filename ), mode, env ) | ||||
|     local func, err = load(file.readAll(), "@" .. fs.getName(filename), mode, env) | ||||
|     file.close() | ||||
|     return func, err | ||||
| end | ||||
|  | ||||
| function dofile( _sFile ) | ||||
| function dofile(_sFile) | ||||
|     expect(1, _sFile, "string") | ||||
|  | ||||
|     local fnFile, e = loadfile( _sFile, nil, _G ) | ||||
|     local fnFile, e = loadfile(_sFile, nil, _G) | ||||
|     if fnFile then | ||||
|         return fnFile() | ||||
|     else | ||||
|         error( e, 2 ) | ||||
|         error(e, 2) | ||||
|     end | ||||
| end | ||||
|  | ||||
| -- Install the rest of the OS api | ||||
| function os.run( _tEnv, _sPath, ... ) | ||||
| function os.run(_tEnv, _sPath, ...) | ||||
|     expect(1, _tEnv, "table") | ||||
|     expect(2, _sPath, "string") | ||||
|  | ||||
|     local tArgs = table.pack( ... ) | ||||
|     local tArgs = table.pack(...) | ||||
|     local tEnv = _tEnv | ||||
|     setmetatable( tEnv, { __index = _G } ) | ||||
|     local fnFile, err = loadfile( _sPath, nil, tEnv ) | ||||
|     setmetatable(tEnv, { __index = _G }) | ||||
|     local fnFile, err = loadfile(_sPath, nil, tEnv) | ||||
|     if fnFile then | ||||
|         local ok, err = pcall( function() | ||||
|             fnFile( table.unpack( tArgs, 1, tArgs.n ) ) | ||||
|         end ) | ||||
|         local ok, err = pcall(function() | ||||
|             fnFile(table.unpack(tArgs, 1, tArgs.n)) | ||||
|         end) | ||||
|         if not ok then | ||||
|             if err and err ~= "" then | ||||
|                 printError( err ) | ||||
|                 printError(err) | ||||
|             end | ||||
|             return false | ||||
|         end | ||||
|         return true | ||||
|     end | ||||
|     if err and err ~= "" then | ||||
|         printError( err ) | ||||
|         printError(err) | ||||
|     end | ||||
|     return false | ||||
| end | ||||
|  | ||||
| local tAPIsLoading = {} | ||||
| function os.loadAPI( _sPath ) | ||||
| function os.loadAPI(_sPath) | ||||
|     expect(1, _sPath, "string") | ||||
|     local sName = fs.getName( _sPath ) | ||||
|     local sName = fs.getName(_sPath) | ||||
|     if sName:sub(-4) == ".lua" then | ||||
|         sName = sName:sub(1, -5) | ||||
|     end | ||||
|     if tAPIsLoading[sName] == true then | ||||
|         printError( "API " .. sName .. " is already being loaded" ) | ||||
|         printError("API " .. sName .. " is already being loaded") | ||||
|         return false | ||||
|     end | ||||
|     tAPIsLoading[sName] = true | ||||
|  | ||||
|     local tEnv = {} | ||||
|     setmetatable( tEnv, { __index = _G } ) | ||||
|     local fnAPI, err = loadfile( _sPath, nil, tEnv ) | ||||
|     setmetatable(tEnv, { __index = _G }) | ||||
|     local fnAPI, err = loadfile(_sPath, nil, tEnv) | ||||
|     if fnAPI then | ||||
|         local ok, err = pcall( fnAPI ) | ||||
|         local ok, err = pcall(fnAPI) | ||||
|         if not ok then | ||||
|             tAPIsLoading[sName] = nil | ||||
|             return error( "Failed to load API " .. sName .. " due to " .. err, 1 ) | ||||
|             return error("Failed to load API " .. sName .. " due to " .. err, 1) | ||||
|         end | ||||
|     else | ||||
|         tAPIsLoading[sName] = nil | ||||
|         return error( "Failed to load API " .. sName .. " due to " .. err, 1 ) | ||||
|         return error("Failed to load API " .. sName .. " due to " .. err, 1) | ||||
|     end | ||||
|  | ||||
|     local tAPI = {} | ||||
|     for k, v in pairs( tEnv ) do | ||||
|     for k, v in pairs(tEnv) do | ||||
|         if k ~= "_ENV" then | ||||
|             tAPI[k] =  v | ||||
|         end | ||||
| @@ -648,15 +582,15 @@ function os.loadAPI( _sPath ) | ||||
|     return true | ||||
| end | ||||
|  | ||||
| function os.unloadAPI( _sName ) | ||||
| function os.unloadAPI(_sName) | ||||
|     expect(1, _sName, "string") | ||||
|     if _sName ~= "_G" and type(_G[_sName]) == "table" then | ||||
|         _G[_sName] = nil | ||||
|     end | ||||
| end | ||||
|  | ||||
| function os.sleep( nTime ) | ||||
|     sleep( nTime ) | ||||
| function os.sleep(nTime) | ||||
|     sleep(nTime) | ||||
| end | ||||
|  | ||||
| local nativeShutdown = os.shutdown | ||||
| @@ -685,7 +619,7 @@ if http then | ||||
|         PATCH = true, TRACE = true, | ||||
|     } | ||||
|  | ||||
|     local function checkKey( options, key, ty, opt ) | ||||
|     local function checkKey(options, key, ty, opt) | ||||
|         local value = options[key] | ||||
|         local valueTy = type(value) | ||||
|  | ||||
| @@ -694,24 +628,24 @@ if http then | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local function checkOptions( options, body ) | ||||
|         checkKey( options, "url", "string") | ||||
|     local function checkOptions(options, body) | ||||
|         checkKey(options, "url", "string") | ||||
|         if body == false then | ||||
|           checkKey( options, "body", "nil" ) | ||||
|           checkKey(options, "body", "nil") | ||||
|         else | ||||
|           checkKey( options, "body", "string", not body ) | ||||
|           checkKey(options, "body", "string", not body) | ||||
|         end | ||||
|         checkKey( options, "headers", "table", true ) | ||||
|         checkKey( options, "method", "string", true ) | ||||
|         checkKey( options, "redirect", "boolean", true ) | ||||
|         checkKey(options, "headers", "table", true) | ||||
|         checkKey(options, "method", "string", true) | ||||
|         checkKey(options, "redirect", "boolean", true) | ||||
|  | ||||
|         if options.method and not methods[options.method] then | ||||
|             error( "Unsupported HTTP method", 3 ) | ||||
|             error("Unsupported HTTP method", 3) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local function wrapRequest( _url, ... ) | ||||
|         local ok, err = nativeHTTPRequest( ... ) | ||||
|     local function wrapRequest(_url, ...) | ||||
|         local ok, err = nativeHTTPRequest(...) | ||||
|         if ok then | ||||
|             while true do | ||||
|                 local event, param1, param2, param3 = os.pullEvent() | ||||
| @@ -725,35 +659,35 @@ if http then | ||||
|         return nil, err | ||||
|     end | ||||
|  | ||||
|     http.get = function( _url, _headers, _binary) | ||||
|         if type( _url ) == "table" then | ||||
|             checkOptions( _url, false ) | ||||
|             return wrapRequest( _url.url, _url ) | ||||
|     http.get = function(_url, _headers, _binary) | ||||
|         if type(_url) == "table" then | ||||
|             checkOptions(_url, false) | ||||
|             return wrapRequest(_url.url, _url) | ||||
|         end | ||||
|  | ||||
|         expect(1, _url, "string") | ||||
|         expect(2, _headers, "table", "nil") | ||||
|         expect(3, _binary, "boolean", "nil") | ||||
|         return wrapRequest( _url, _url, nil, _headers, _binary ) | ||||
|         return wrapRequest(_url, _url, nil, _headers, _binary) | ||||
|     end | ||||
|  | ||||
|     http.post = function( _url, _post, _headers, _binary) | ||||
|         if type( _url ) == "table" then | ||||
|             checkOptions( _url, true ) | ||||
|             return wrapRequest( _url.url, _url ) | ||||
|     http.post = function(_url, _post, _headers, _binary) | ||||
|         if type(_url) == "table" then | ||||
|             checkOptions(_url, true) | ||||
|             return wrapRequest(_url.url, _url) | ||||
|         end | ||||
|  | ||||
|         expect(1, _url, "string") | ||||
|         expect(2, _post, "string") | ||||
|         expect(3, _headers, "table", "nil") | ||||
|         expect(4, _binary, "boolean", "nil") | ||||
|         return wrapRequest( _url, _url, _post, _headers, _binary ) | ||||
|         return wrapRequest(_url, _url, _post, _headers, _binary) | ||||
|     end | ||||
|  | ||||
|     http.request = function( _url, _post, _headers, _binary ) | ||||
|     http.request = function(_url, _post, _headers, _binary) | ||||
|         local url | ||||
|         if type( _url ) == "table" then | ||||
|             checkOptions( _url ) | ||||
|         if type(_url) == "table" then | ||||
|             checkOptions(_url) | ||||
|             url = _url.url | ||||
|         else | ||||
|             expect(1, _url, "string") | ||||
| @@ -763,32 +697,32 @@ if http then | ||||
|             url = _url.url | ||||
|         end | ||||
|  | ||||
|         local ok, err = nativeHTTPRequest( _url, _post, _headers, _binary ) | ||||
|         local ok, err = nativeHTTPRequest(_url, _post, _headers, _binary) | ||||
|         if not ok then | ||||
|             os.queueEvent( "http_failure", url, err ) | ||||
|             os.queueEvent("http_failure", url, err) | ||||
|         end | ||||
|         return ok, err | ||||
|     end | ||||
|  | ||||
|     local nativeCheckURL = http.checkURL | ||||
|     http.checkURLAsync = nativeCheckURL | ||||
|     http.checkURL = function( _url ) | ||||
|         local ok, err = nativeCheckURL( _url ) | ||||
|     http.checkURL = function(_url) | ||||
|         local ok, err = nativeCheckURL(_url) | ||||
|         if not ok then return ok, err end | ||||
|  | ||||
|         while true do | ||||
|             local _, url, ok, err = os.pullEvent( "http_check" ) | ||||
|             local _, url, ok, err = os.pullEvent("http_check") | ||||
|             if url == _url then return ok, err end | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local nativeWebsocket = http.websocket | ||||
|     http.websocketAsync = nativeWebsocket | ||||
|     http.websocket = function( _url, _headers ) | ||||
|     http.websocket = function(_url, _headers) | ||||
|         expect(1, _url, "string") | ||||
|         expect(2, _headers, "table", "nil") | ||||
|  | ||||
|         local ok, err = nativeWebsocket( _url, _headers ) | ||||
|         local ok, err = nativeWebsocket(_url, _headers) | ||||
|         if not ok then return ok, err end | ||||
|  | ||||
|         while true do | ||||
| @@ -804,7 +738,7 @@ end | ||||
|  | ||||
| -- Install the lua part of the FS api | ||||
| local tEmpty = {} | ||||
| function fs.complete( sPath, sLocation, bIncludeFiles, bIncludeDirs ) | ||||
| function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs) | ||||
|     expect(1, sPath, "string") | ||||
|     expect(2, sLocation, "string") | ||||
|     expect(3, bIncludeFiles, "boolean", "nil") | ||||
| @@ -814,49 +748,49 @@ function fs.complete( sPath, sLocation, bIncludeFiles, bIncludeDirs ) | ||||
|     bIncludeDirs = bIncludeDirs ~= false | ||||
|     local sDir = sLocation | ||||
|     local nStart = 1 | ||||
|     local nSlash = string.find( sPath, "[/\\]", nStart ) | ||||
|     local nSlash = string.find(sPath, "[/\\]", nStart) | ||||
|     if nSlash == 1 then | ||||
|         sDir = "" | ||||
|         nStart = 2 | ||||
|     end | ||||
|     local sName | ||||
|     while not sName do | ||||
|         local nSlash = string.find( sPath, "[/\\]", nStart ) | ||||
|         local nSlash = string.find(sPath, "[/\\]", nStart) | ||||
|         if nSlash then | ||||
|             local sPart = string.sub( sPath, nStart, nSlash - 1 ) | ||||
|             sDir = fs.combine( sDir, sPart ) | ||||
|             local sPart = string.sub(sPath, nStart, nSlash - 1) | ||||
|             sDir = fs.combine(sDir, sPart) | ||||
|             nStart = nSlash + 1 | ||||
|         else | ||||
|             sName = string.sub( sPath, nStart ) | ||||
|             sName = string.sub(sPath, nStart) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     if fs.isDir( sDir ) then | ||||
|     if fs.isDir(sDir) then | ||||
|         local tResults = {} | ||||
|         if bIncludeDirs and sPath == "" then | ||||
|             table.insert( tResults, "." ) | ||||
|             table.insert(tResults, ".") | ||||
|         end | ||||
|         if sDir ~= "" then | ||||
|             if sPath == "" then | ||||
|                 table.insert( tResults, bIncludeDirs and ".." or "../" ) | ||||
|                 table.insert(tResults, bIncludeDirs and ".." or "../") | ||||
|             elseif sPath == "." then | ||||
|                 table.insert( tResults, bIncludeDirs and "." or "./" ) | ||||
|                 table.insert(tResults, bIncludeDirs and "." or "./") | ||||
|             end | ||||
|         end | ||||
|         local tFiles = fs.list( sDir ) | ||||
|         local tFiles = fs.list(sDir) | ||||
|         for n = 1, #tFiles do | ||||
|             local sFile = tFiles[n] | ||||
|             if #sFile >= #sName and string.sub( sFile, 1, #sName ) == sName then | ||||
|                 local bIsDir = fs.isDir( fs.combine( sDir, sFile ) ) | ||||
|                 local sResult = string.sub( sFile, #sName + 1 ) | ||||
|             if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName then | ||||
|                 local bIsDir = fs.isDir(fs.combine(sDir, sFile)) | ||||
|                 local sResult = string.sub(sFile, #sName + 1) | ||||
|                 if bIsDir then | ||||
|                     table.insert( tResults, sResult .. "/" ) | ||||
|                     table.insert(tResults, sResult .. "/") | ||||
|                     if bIncludeDirs and #sResult > 0 then | ||||
|                         table.insert( tResults, sResult ) | ||||
|                         table.insert(tResults, sResult) | ||||
|                     end | ||||
|                 else | ||||
|                     if bIncludeFiles and #sResult > 0 then | ||||
|                         table.insert( tResults, sResult ) | ||||
|                         table.insert(tResults, sResult) | ||||
|                     end | ||||
|                 end | ||||
|             end | ||||
| @@ -868,26 +802,26 @@ end | ||||
|  | ||||
| -- Load APIs | ||||
| local bAPIError = false | ||||
| local tApis = fs.list( "rom/apis" ) | ||||
| for _, sFile in ipairs( tApis ) do | ||||
|     if string.sub( sFile, 1, 1 ) ~= "." then | ||||
|         local sPath = fs.combine( "rom/apis", sFile ) | ||||
|         if not fs.isDir( sPath ) then | ||||
|             if not os.loadAPI( sPath ) then | ||||
| local tApis = fs.list("rom/apis") | ||||
| for _, sFile in ipairs(tApis) do | ||||
|     if string.sub(sFile, 1, 1) ~= "." then | ||||
|         local sPath = fs.combine("rom/apis", sFile) | ||||
|         if not fs.isDir(sPath) then | ||||
|             if not os.loadAPI(sPath) then | ||||
|                 bAPIError = true | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| if turtle and fs.isDir( "rom/apis/turtle" ) then | ||||
| if turtle and fs.isDir("rom/apis/turtle") then | ||||
|     -- Load turtle APIs | ||||
|     local tApis = fs.list( "rom/apis/turtle" ) | ||||
|     for _, sFile in ipairs( tApis ) do | ||||
|         if string.sub( sFile, 1, 1 ) ~= "." then | ||||
|             local sPath = fs.combine( "rom/apis/turtle", sFile ) | ||||
|             if not fs.isDir( sPath ) then | ||||
|                 if not os.loadAPI( sPath ) then | ||||
|     local tApis = fs.list("rom/apis/turtle") | ||||
|     for _, sFile in ipairs(tApis) do | ||||
|         if string.sub(sFile, 1, 1) ~= "." then | ||||
|             local sPath = fs.combine("rom/apis/turtle", sFile) | ||||
|             if not fs.isDir(sPath) then | ||||
|                 if not os.loadAPI(sPath) then | ||||
|                     bAPIError = true | ||||
|                 end | ||||
|             end | ||||
| @@ -895,14 +829,14 @@ if turtle and fs.isDir( "rom/apis/turtle" ) then | ||||
|     end | ||||
| end | ||||
|  | ||||
| if pocket and fs.isDir( "rom/apis/pocket" ) then | ||||
| if pocket and fs.isDir("rom/apis/pocket") then | ||||
|     -- Load pocket APIs | ||||
|     local tApis = fs.list( "rom/apis/pocket" ) | ||||
|     for _, sFile in ipairs( tApis ) do | ||||
|         if string.sub( sFile, 1, 1 ) ~= "." then | ||||
|             local sPath = fs.combine( "rom/apis/pocket", sFile ) | ||||
|             if not fs.isDir( sPath ) then | ||||
|                 if not os.loadAPI( sPath ) then | ||||
|     local tApis = fs.list("rom/apis/pocket") | ||||
|     for _, sFile in ipairs(tApis) do | ||||
|         if string.sub(sFile, 1, 1) ~= "." then | ||||
|             local sPath = fs.combine("rom/apis/pocket", sFile) | ||||
|             if not fs.isDir(sPath) then | ||||
|                 if not os.loadAPI(sPath) then | ||||
|                     bAPIError = true | ||||
|                 end | ||||
|             end | ||||
| @@ -910,18 +844,18 @@ if pocket and fs.isDir( "rom/apis/pocket" ) then | ||||
|     end | ||||
| end | ||||
|  | ||||
| if commands and fs.isDir( "rom/apis/command" ) then | ||||
| if commands and fs.isDir("rom/apis/command") then | ||||
|     -- Load command APIs | ||||
|     if os.loadAPI( "rom/apis/command/commands.lua" ) then | ||||
|     if os.loadAPI("rom/apis/command/commands.lua") then | ||||
|         -- Add a special case-insensitive metatable to the commands api | ||||
|         local tCaseInsensitiveMetatable = { | ||||
|             __index = function( table, key ) | ||||
|                 local value = rawget( table, key ) | ||||
|             __index = function(table, key) | ||||
|                 local value = rawget(table, key) | ||||
|                 if value ~= nil then | ||||
|                     return value | ||||
|                 end | ||||
|                 if type(key) == "string" then | ||||
|                     local value = rawget( table, string.lower(key) ) | ||||
|                     local value = rawget(table, string.lower(key)) | ||||
|                     if value ~= nil then | ||||
|                         return value | ||||
|                     end | ||||
| @@ -929,8 +863,8 @@ if commands and fs.isDir( "rom/apis/command" ) then | ||||
|                 return nil | ||||
|             end, | ||||
|         } | ||||
|         setmetatable( commands, tCaseInsensitiveMetatable ) | ||||
|         setmetatable( commands.async, tCaseInsensitiveMetatable ) | ||||
|         setmetatable(commands, tCaseInsensitiveMetatable) | ||||
|         setmetatable(commands.async, tCaseInsensitiveMetatable) | ||||
|  | ||||
|         -- Add global "exec" function | ||||
|         exec = commands.exec | ||||
| @@ -940,29 +874,77 @@ if commands and fs.isDir( "rom/apis/command" ) then | ||||
| end | ||||
|  | ||||
| if bAPIError then | ||||
|     print( "Press any key to continue" ) | ||||
|     os.pullEvent( "key" ) | ||||
|     print("Press any key to continue") | ||||
|     os.pullEvent("key") | ||||
|     term.clear() | ||||
|     term.setCursorPos( 1, 1 ) | ||||
|     term.setCursorPos(1, 1) | ||||
| end | ||||
|  | ||||
| -- Set default settings | ||||
| settings.set( "shell.allow_startup", true ) | ||||
| settings.set( "shell.allow_disk_startup", commands == nil ) | ||||
| settings.set( "shell.autocomplete", true ) | ||||
| settings.set( "edit.autocomplete", true ) | ||||
| settings.set( "edit.default_extension", "lua" ) | ||||
| settings.set( "paint.default_extension", "nfp" ) | ||||
| settings.set( "lua.autocomplete", true ) | ||||
| settings.set( "list.show_hidden", false ) | ||||
| settings.set( "motd.enable", false ) | ||||
| settings.set( "motd.path", "/rom/motd.txt:/motd.txt" ) | ||||
| settings.define("shell.allow_startup", { | ||||
|     default = true, | ||||
|     description = "Run startup files when the computer turns on.", | ||||
|     type = "boolean", | ||||
| }) | ||||
| settings.define("shell.allow_disk_startup", { | ||||
|     default = commands == nil, | ||||
|     description = "Run startup files from disk drives when the computer turns on.", | ||||
|     type = "boolean", | ||||
| }) | ||||
|  | ||||
| settings.define("shell.autocomplete", { | ||||
|     default = true, | ||||
|     description = "Autocomplete program and arguments in the shell.", | ||||
|     type = "boolean", | ||||
| }) | ||||
| settings.define("edit.autocomplete", { | ||||
|     default = true, | ||||
|     description = "Autocomplete API and function names in the editor.", | ||||
|         type = "boolean", | ||||
| }) | ||||
| settings.define("lua.autocomplete", { | ||||
|     default = true, | ||||
|     description = "Autocomplete API and function names in the Lua REPL.", | ||||
|         type = "boolean", | ||||
| }) | ||||
|  | ||||
| settings.define("edit.default_extension", { | ||||
|     default = "lua", | ||||
|     description = [[The file extension the editor will use if none is given. Set to "" to disable.]], | ||||
|     type = "string", | ||||
| }) | ||||
| settings.define("paint.default_extension", { | ||||
|     default = "nfp", | ||||
|     description = [[The file extension the paint program will use if none is given. Set to "" to disable.]], | ||||
|     type = "string", | ||||
| }) | ||||
|  | ||||
| settings.define("list.show_hidden", { | ||||
|     default = false, | ||||
|     description = [[Show hidden files (those starting with "." in the Lua REPL)]], | ||||
|     type = "boolean", | ||||
| }) | ||||
|  | ||||
| settings.define("motd.enable", { | ||||
|     default = false, | ||||
|     description = "Display a random message when the computer starts up.", | ||||
|     type = "boolean", | ||||
| }) | ||||
| settings.define("motd.path", { | ||||
|     default = "/rom/motd.txt:/motd.txt", | ||||
|     description = [[The path to load random messages from. Should be a colon (":") separated string of file paths.]], | ||||
|     type = "string", | ||||
| }) | ||||
| if term.isColour() then | ||||
|     settings.set( "bios.use_multishell", true ) | ||||
|     settings.define("bios.use_multishell", { | ||||
|         default = true, | ||||
|         description = [[Allow running multiple programs at once, through the use of the "fg" and "bg" programs.]], | ||||
|         type = "boolean", | ||||
|     }) | ||||
| end | ||||
| if _CC_DEFAULT_SETTINGS then | ||||
|     for sPair in string.gmatch( _CC_DEFAULT_SETTINGS, "[^,]+" ) do | ||||
|         local sName, sValue = string.match( sPair, "([^=]*)=(.*)" ) | ||||
|     for sPair in string.gmatch(_CC_DEFAULT_SETTINGS, "[^,]+") do | ||||
|         local sName, sValue = string.match(sPair, "([^=]*)=(.*)") | ||||
|         if sName and sValue then | ||||
|             local value | ||||
|             if sValue == "true" then | ||||
| @@ -977,46 +959,43 @@ if _CC_DEFAULT_SETTINGS then | ||||
|                 value = sValue | ||||
|             end | ||||
|             if value ~= nil then | ||||
|                 settings.set( sName, value ) | ||||
|                 settings.set(sName, value) | ||||
|             else | ||||
|                 settings.unset( sName ) | ||||
|                 settings.unset(sName) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| -- Load user settings | ||||
| if fs.exists( ".settings" ) then | ||||
|     settings.load( ".settings" ) | ||||
| if fs.exists(".settings") then | ||||
|     settings.load(".settings") | ||||
| end | ||||
|  | ||||
| -- Run the shell | ||||
| local ok, err = pcall( function() | ||||
|     parallel.waitForAny( | ||||
|         function() | ||||
|             local sShell | ||||
|             if term.isColour() and settings.get( "bios.use_multishell" ) then | ||||
|                 sShell = "rom/programs/advanced/multishell.lua" | ||||
|             else | ||||
|                 sShell = "rom/programs/shell.lua" | ||||
|             end | ||||
|             os.run( {}, sShell ) | ||||
|             os.run( {}, "rom/programs/shutdown.lua" ) | ||||
|         end, | ||||
|         function() | ||||
|             rednet.run() | ||||
|         end ) | ||||
| end ) | ||||
| local ok, err = pcall(parallel.waitForAny, | ||||
|     function() | ||||
|         local sShell | ||||
|         if term.isColour() and settings.get("bios.use_multishell") then | ||||
|             sShell = "rom/programs/advanced/multishell.lua" | ||||
|         else | ||||
|             sShell = "rom/programs/shell.lua" | ||||
|         end | ||||
|         os.run({}, sShell) | ||||
|         os.run({}, "rom/programs/shutdown.lua") | ||||
|     end, | ||||
|     rednet.run | ||||
| ) | ||||
|  | ||||
| -- If the shell errored, let the user read it. | ||||
| term.redirect( term.native() ) | ||||
| term.redirect(term.native()) | ||||
| if not ok then | ||||
|     printError( err ) | ||||
|     pcall( function() | ||||
|         term.setCursorBlink( false ) | ||||
|         print( "Press any key to continue" ) | ||||
|         os.pullEvent( "key" ) | ||||
|     end ) | ||||
|     printError(err) | ||||
|     pcall(function() | ||||
|         term.setCursorBlink(false) | ||||
|         print("Press any key to continue") | ||||
|         os.pullEvent("key") | ||||
|     end) | ||||
| end | ||||
|  | ||||
| -- End | ||||
|   | ||||
| @@ -1,24 +1,92 @@ | ||||
| --- The Colors API allows you to manipulate sets of colors. | ||||
| -- | ||||
| -- This is useful in conjunction with Bundled Cables from the RedPower mod, | ||||
| -- RedNet Cables from the MineFactory Reloaded mod, and colors on Advanced | ||||
| -- Computers and Advanced Monitors. | ||||
| -- | ||||
| -- For the non-American English version just replace @{colors} with @{colours} | ||||
| -- and it will use the other API, colours which is exactly the same, except in | ||||
| -- British English (e.g. @{colors.gray} is spelt @{colours.grey}). | ||||
| -- | ||||
| -- @see colours | ||||
| -- @module colors | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| -- Colors | ||||
| white = 1 | ||||
| orange = 2 | ||||
| magenta = 4 | ||||
| lightBlue = 8 | ||||
| yellow = 16 | ||||
| lime = 32 | ||||
| pink = 64 | ||||
| gray = 128 | ||||
| lightGray = 256 | ||||
| cyan = 512 | ||||
| purple = 1024 | ||||
| blue = 2048 | ||||
| brown = 4096 | ||||
| green = 8192 | ||||
| red = 16384 | ||||
| black = 32768 | ||||
| --- White: Written as `0` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #F0F0F0. | ||||
| white = 0x1 | ||||
|  | ||||
| function combine( ... ) | ||||
| --- Orange: Written as `1` in paint files and @{term.blit}, has a | ||||
| -- default terminal colour of #F2B233. | ||||
| orange = 0x2 | ||||
|  | ||||
| --- Magenta: Written as `2` in paint files and @{term.blit}, has a | ||||
| -- default terminal colour of #E57FD8. | ||||
| magenta = 0x4 | ||||
|  | ||||
| --- Light blue: Written as `3` in paint files and @{term.blit}, has a | ||||
| -- default terminal colour of #99B2F2. | ||||
| lightBlue = 0x8 | ||||
|  | ||||
| --- Yellow: Written as `4` in paint files and @{term.blit}, has a | ||||
| -- default terminal colour of #DEDE6C. | ||||
| yellow = 0x10 | ||||
|  | ||||
| --- Lime: Written as `5` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #7FCC19. | ||||
| lime = 0x20 | ||||
|  | ||||
| --- Pink. Written as `6` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #F2B2CC. | ||||
| pink = 0x40 | ||||
|  | ||||
| --- Gray: Written as `7` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #4C4C4C. | ||||
| gray = 0x80 | ||||
|  | ||||
| --- Light gray: Written as `8` in paint files and @{term.blit}, has a | ||||
| -- default terminal colour of #999999. | ||||
| lightGray = 0x100 | ||||
|  | ||||
| --- Cyan: Written as `9` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #4C99B2. | ||||
| cyan = 0x200 | ||||
|  | ||||
| --- Purple: Written as `a` in paint files and @{term.blit}, has a | ||||
| -- default terminal colour of #B266E5. | ||||
| purple = 0x400 | ||||
|  | ||||
| --- Blue: Written as `b` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #3366CC. | ||||
| blue = 0x800 | ||||
|  | ||||
| --- Brown: Written as `c` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #7F664C. | ||||
| brown = 0x1000 | ||||
|  | ||||
| --- Green: Written as `d` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #57A64E. | ||||
| green = 0x2000 | ||||
|  | ||||
| --- Red: Written as `e` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #CC4C4C. | ||||
| red = 0x4000 | ||||
|  | ||||
| --- Black: Written as `f` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #191919. | ||||
| black = 0x8000 | ||||
|  | ||||
| --- Combines a set of colors (or sets of colors) into a larger set. | ||||
| -- | ||||
| -- @tparam number ... The colors to combine. | ||||
| -- @treturn number The union of the color sets given in `...` | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- colors.combine(colors.white, colors.magenta, colours.lightBlue) | ||||
| -- -- => 13 | ||||
| -- ``` | ||||
| function combine(...) | ||||
|     local r = 0 | ||||
|     for i = 1, select('#', ...) do | ||||
|         local c = select(i, ...) | ||||
| @@ -28,7 +96,21 @@ function combine( ... ) | ||||
|     return r | ||||
| end | ||||
|  | ||||
| function subtract( colors, ... ) | ||||
| --- Removes one or more colors (or sets of colors) from an initial set. | ||||
| -- | ||||
| -- Each parameter beyond the first may be a single color or may be a set of | ||||
| -- colors (in the latter case, all colors in the set are removed from the | ||||
| -- original set). | ||||
| -- | ||||
| -- @tparam number colors The color from which to subtract. | ||||
| -- @tparam number ... The colors to subtract. | ||||
| -- @treturn number The resulting color. | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- colours.subtract(colours.lime, colours.orange, colours.white) | ||||
| -- -- => 32 | ||||
| -- ``` | ||||
| function subtract(colors, ...) | ||||
|     expect(1, colors, "number") | ||||
|     local r = colors | ||||
|     for i = 1, select('#', ...) do | ||||
| @@ -39,34 +121,91 @@ function subtract( colors, ... ) | ||||
|     return r | ||||
| end | ||||
|  | ||||
| function test( colors, color ) | ||||
| --- Tests whether `color` is contained within `colors`. | ||||
| -- | ||||
| -- @tparam number colors A color, or color set | ||||
| -- @tparam number color A color or set of colors that `colors` should contain. | ||||
| -- @treturn boolean If `colors` contains all colors within `color`. | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- colors.test(colors.combine(colors.white, colors.magenta, colours.lightBlue), colors.lightBlue) | ||||
| -- -- => true | ||||
| -- ``` | ||||
| function test(colors, color) | ||||
|     expect(1, colors, "number") | ||||
|     expect(2, color, "number") | ||||
|     return bit32.band(colors, color) == color | ||||
| end | ||||
|  | ||||
| function packRGB( r, g, b ) | ||||
| --- Combine a three-colour RGB value into one hexadecimal representation. | ||||
| -- | ||||
| -- @tparam number r The red channel, should be between 0 and 1. | ||||
| -- @tparam number g The red channel, should be between 0 and 1. | ||||
| -- @tparam number b The blue channel, should be between 0 and 1. | ||||
| -- @treturn number The combined hexadecimal colour. | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- colors.rgb(0.7, 0.2, 0.6) | ||||
| -- -- => 0xb23399 | ||||
| -- ``` | ||||
| function packRGB(r, g, b) | ||||
|     expect(1, r, "number") | ||||
|     expect(2, g, "number") | ||||
|     expect(3, b, "number") | ||||
|     return | ||||
|         bit32.band( r * 255, 0xFF ) * 2 ^ 16 + | ||||
|         bit32.band( g * 255, 0xFF ) * 2 ^ 8 + | ||||
|         bit32.band( b * 255, 0xFF ) | ||||
|         bit32.band(r * 255, 0xFF) * 2 ^ 16 + | ||||
|         bit32.band(g * 255, 0xFF) * 2 ^ 8 + | ||||
|         bit32.band(b * 255, 0xFF) | ||||
| end | ||||
|  | ||||
| function unpackRGB( rgb ) | ||||
| --- Separate a hexadecimal RGB colour into its three constituent channels. | ||||
| -- | ||||
| -- @tparam number rgb The combined hexadecimal colour. | ||||
| -- @treturn number The red channel, will be between 0 and 1. | ||||
| -- @treturn number The red channel, will be between 0 and 1. | ||||
| -- @treturn number The blue channel, will be between 0 and 1. | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- colors.rgb(0xb23399) | ||||
| -- -- => 0.7, 0.2, 0.6 | ||||
| -- ``` | ||||
| -- @see colors.packRGB | ||||
| function unpackRGB(rgb) | ||||
|     expect(1, rgb, "number") | ||||
|     return | ||||
|         bit32.band( bit32.rshift( rgb, 16 ), 0xFF ) / 255, | ||||
|         bit32.band( bit32.rshift( rgb, 8 ), 0xFF ) / 255, | ||||
|         bit32.band( rgb, 0xFF ) / 255 | ||||
|         bit32.band(bit32.rshift(rgb, 16), 0xFF) / 255, | ||||
|         bit32.band(bit32.rshift(rgb, 8), 0xFF) / 255, | ||||
|         bit32.band(rgb, 0xFF) / 255 | ||||
| end | ||||
|  | ||||
| function rgb8( r, g, b ) | ||||
| --- Either calls @{colors.packRGB} or @{colors.unpackRGB}, depending on how many | ||||
| -- arguments it receives. | ||||
| -- | ||||
| -- **Note:** This function is deprecated, and it is recommended you use the | ||||
| -- specific pack/unpack function directly. | ||||
| -- | ||||
| -- @tparam[1] number r The red channel, as an argument to @{colors.packRGB}. | ||||
| -- @tparam[1] number g The green channel, as an argument to @{colors.packRGB}. | ||||
| -- @tparam[1] number b The blue channel, as an argument to @{colors.packRGB}. | ||||
| -- @tparam[2] number rgb The combined hexadecimal color, as an argument to @{colors.unpackRGB}. | ||||
| -- @treturn[1] number The combined hexadecimal colour, as returned by @{colors.packRGB}. | ||||
| -- @treturn[2] number The red channel, as returned by @{colors.unpackRGB} | ||||
| -- @treturn[2] number The green channel, as returned by @{colors.unpackRGB} | ||||
| -- @treturn[2] number The blue channel, as returned by @{colors.unpackRGB} | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- colors.rgb(0xb23399) | ||||
| -- -- => 0.7, 0.2, 0.6 | ||||
| -- ``` | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- colors.rgb(0.7, 0.2, 0.6) | ||||
| -- -- => 0xb23399 | ||||
| -- ``` | ||||
| function rgb8(r, g, b) | ||||
|     if g == nil and b == nil then | ||||
|         return unpackRGB( r ) | ||||
|         return unpackRGB(r) | ||||
|     else | ||||
|         return packRGB( r, g, b ) | ||||
|         return packRGB(r, g, b) | ||||
|     end | ||||
| end | ||||
|   | ||||
| @@ -1,11 +1,23 @@ | ||||
| -- Colours (for lovers of british spelling) | ||||
| --- Colours for lovers of British spelling. | ||||
| -- | ||||
| -- @see colors | ||||
| -- @module colours | ||||
|  | ||||
| local colours = _ENV | ||||
| for k, v in pairs(colors) do | ||||
| 	colours[k] = v | ||||
| end | ||||
|  | ||||
| --- Grey. Written as `7` in paint files and @{term.blit}, has a default | ||||
| -- terminal colour of #4C4C4C. | ||||
| -- | ||||
| -- @see colors.gray | ||||
| colours.grey = colors.gray | ||||
| colours.gray = nil | ||||
| colours.gray = nil --- @local | ||||
|  | ||||
| --- Light grey. Written as `8` in paint files and @{term.blit}, has a | ||||
| -- default terminal colour of #999999. | ||||
| -- | ||||
| -- @see colors.lightGray | ||||
| colours.lightGrey = colors.lightGray | ||||
| colours.lightGray = nil | ||||
| colours.lightGray = nil --- @local | ||||
|   | ||||
| @@ -1,8 +1,26 @@ | ||||
| --- The commands API allows your system to directly execute [Minecraft | ||||
| -- commands][mc] and gather data from the results. | ||||
| -- | ||||
| -- While one may use @{commands.exec} directly to execute a command, the | ||||
| -- commands API also provides helper methods to execute every command. For | ||||
| -- instance, `commands.say("Hi!")` is equivalent to `commands.exec("say Hi!")`. | ||||
| -- | ||||
| -- @{commands.async} provides a similar interface to execute asynchronous | ||||
| -- commands. `commands.async.say("Hi!")` is equivalent to | ||||
| -- `commands.execAsync("Hi!")`. | ||||
| -- | ||||
| -- [mc]: https://minecraft.gamepedia.com/Commands | ||||
| -- | ||||
| -- @module commands | ||||
|  | ||||
| if not commands then | ||||
|     error( "Cannot load command API on normal computer", 2 ) | ||||
| end | ||||
|  | ||||
| --- The builtin commands API, without any generated command helper functions | ||||
| -- | ||||
| -- This may be useful if a built-in function (such as @{commands.list}) has been | ||||
| -- overwritten by a command. | ||||
| local native = commands.native or commands | ||||
|  | ||||
| local function collapseArgs( bJSONIsNBT, ... ) | ||||
|   | ||||
| @@ -1,87 +1,171 @@ | ||||
| --- The Disk API allows you to interact with disk drives. | ||||
| -- | ||||
| -- These functions can operate on locally attached or remote disk drives. To use | ||||
| -- a locally attached drive, specify “side” as one of the six sides | ||||
| -- (e.g. `left`); to use a remote disk drive, specify its name as printed when | ||||
| -- enabling its modem (e.g. `drive_0`). | ||||
| -- | ||||
| -- **Note:** All computers (except command computers), turtles and pocket | ||||
| -- computers can be placed within a disk drive to access it's internal storage | ||||
| -- like a disk. | ||||
| -- | ||||
| -- @module disk | ||||
|  | ||||
| local function isDrive( name ) | ||||
|     if type( name ) ~= "string" then | ||||
|         error( "bad argument #1 (expected string, got " .. type( name ) .. ")", 3 ) | ||||
| local function isDrive(name) | ||||
|     if type(name) ~= "string" then | ||||
|         error("bad argument #1 (expected string, got " .. type(name) .. ")", 3) | ||||
|     end | ||||
|     return peripheral.getType( name ) == "drive" | ||||
|     return peripheral.getType(name) == "drive" | ||||
| end | ||||
|  | ||||
| function isPresent( name ) | ||||
|     if isDrive( name ) then | ||||
|         return peripheral.call( name, "isDiskPresent" ) | ||||
| --- Checks whether any item at all is in the disk drive | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @treturn boolean If something is in the disk drive. | ||||
| -- @usage disk.isPresent(false) | ||||
| function isPresent(name) | ||||
|     if isDrive(name) then | ||||
|         return peripheral.call(name, "isDiskPresent") | ||||
|     end | ||||
|     return false | ||||
| end | ||||
|  | ||||
| function getLabel( name ) | ||||
|     if isDrive( name ) then | ||||
|         return peripheral.call( name, "getDiskLabel" ) | ||||
| --- Get the label of the floppy disk, record, or other media within the given | ||||
| -- disk drive. | ||||
| -- | ||||
| -- If there is a computer or turtle within the drive, this will set the label as | ||||
| -- read by `os.getComputerLabel`. | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @treturn string|nil The name of the current media, or `nil` if the drive is | ||||
| -- not present or empty. | ||||
| -- @see disk.setLabel | ||||
| function getLabel(name) | ||||
|     if isDrive(name) then | ||||
|         return peripheral.call(name, "getDiskLabel") | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function setLabel( name, label ) | ||||
|     if isDrive( name ) then | ||||
|         peripheral.call( name, "setDiskLabel", label ) | ||||
| --- Set the label of the floppy disk or other media | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @tparam string|nil label The new label of the disk | ||||
| function setLabel(name, label) | ||||
|     if isDrive(name) then | ||||
|         peripheral.call(name, "setDiskLabel", label) | ||||
|     end | ||||
| end | ||||
|  | ||||
| function hasData( name ) | ||||
|     if isDrive( name ) then | ||||
|         return peripheral.call( name, "hasData" ) | ||||
| --- Check whether the current disk provides a mount. | ||||
| -- | ||||
| -- This will return true for disks and computers, but not records. | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @treturn boolean If the disk is present and provides a mount. | ||||
| -- @see disk.getMountPath | ||||
| function hasData(name) | ||||
|     if isDrive(name) then | ||||
|         return peripheral.call(name, "hasData") | ||||
|     end | ||||
|     return false | ||||
| end | ||||
|  | ||||
| function getMountPath( name ) | ||||
|     if isDrive( name ) then | ||||
|         return peripheral.call( name, "getMountPath" ) | ||||
| --- Find the directory name on the local computer where the contents of the | ||||
| -- current floppy disk (or other mount) can be found. | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @treturn string|nil The mount's directory, or `nil` if the drive does not | ||||
| -- contain a floppy or computer. | ||||
| -- @see disk.hasData | ||||
| function getMountPath(name) | ||||
|     if isDrive(name) then | ||||
|         return peripheral.call(name, "getMountPath") | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function hasAudio( name ) | ||||
|     if isDrive( name ) then | ||||
|         return peripheral.call( name, "hasAudio" ) | ||||
| --- Whether the current disk is a [music disk][disk] as opposed to a floppy disk | ||||
| -- or other item. | ||||
| -- | ||||
| -- If this returns true, you will can @{disk.playAudio|play} the record. | ||||
| -- | ||||
| -- [disk]: https://minecraft.gamepedia.com/Music_Disc | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @treturn boolean If the disk is present and has audio saved on it. | ||||
| function hasAudio(name) | ||||
|     if isDrive(name) then | ||||
|         return peripheral.call(name, "hasAudio") | ||||
|     end | ||||
|     return false | ||||
| end | ||||
|  | ||||
| function getAudioTitle( name ) | ||||
|     if isDrive( name ) then | ||||
|         return peripheral.call( name, "getAudioTitle" ) | ||||
| --- Get the title of the audio track from the music record in the drive. | ||||
| -- | ||||
| -- This generally returns the same as @{disk.getLabel} for records. | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @treturn string|false|nil The track title, `false` if there is not a music | ||||
| -- record in the drive or `nil` if no drive is present. | ||||
| function getAudioTitle(name) | ||||
|     if isDrive(name) then | ||||
|         return peripheral.call(name, "getAudioTitle") | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function playAudio( name ) | ||||
|     if isDrive( name ) then | ||||
|         peripheral.call( name, "playAudio" ) | ||||
| --- Starts playing the music record in the drive. | ||||
| -- | ||||
| -- If any record is already playing on any disk drive, it stops before the | ||||
| -- target drive starts playing. The record stops when it reaches the end of the | ||||
| -- track, when it is removed from the drive, when @{disk.stopAudio} is called, or | ||||
| -- when another record is started. | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @usage disk.playAudio("bottom") | ||||
| function playAudio(name) | ||||
|     if isDrive(name) then | ||||
|         peripheral.call(name, "playAudio") | ||||
|     end | ||||
| end | ||||
|  | ||||
| function stopAudio( name ) | ||||
| --- Stops the music record in the drive from playing, if it was started with | ||||
| -- @{disk.playAudio}. | ||||
| -- | ||||
| -- @tparam string name The name o the disk drive. | ||||
| function stopAudio(name) | ||||
|     if not name then | ||||
|         for _, sName in ipairs( peripheral.getNames() ) do | ||||
|             stopAudio( sName ) | ||||
|         for _, sName in ipairs(peripheral.getNames()) do | ||||
|             stopAudio(sName) | ||||
|         end | ||||
|     else | ||||
|         if isDrive( name ) then | ||||
|             peripheral.call( name, "stopAudio" ) | ||||
|         if isDrive(name) then | ||||
|             peripheral.call(name, "stopAudio") | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| function eject( name ) | ||||
|     if isDrive( name ) then | ||||
|         peripheral.call( name, "ejectDisk" ) | ||||
| --- Ejects any item currently in the drive, spilling it into the world as a loose item. | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @usage disk.eject("bottom") | ||||
| function eject(name) | ||||
|     if isDrive(name) then | ||||
|         peripheral.call(name, "ejectDisk") | ||||
|     end | ||||
| end | ||||
|  | ||||
| function getID( name ) | ||||
|     if isDrive( name ) then | ||||
|         return peripheral.call( name, "getDiskID" ) | ||||
| --- Returns a number which uniquely identifies the disk in the drive. | ||||
| -- | ||||
| -- Note, unlike @{disk.getLabel}, this does not return anything for other media, | ||||
| -- such as computers or turtles. | ||||
| -- | ||||
| -- @tparam string name The name of the disk drive. | ||||
| -- @treturn string|nil The disk ID, or `nil` if the drive does not contain a floppy disk. | ||||
| function getID(name) | ||||
|     if isDrive(name) then | ||||
|         return peripheral.call(name, "getDiskID") | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -1,21 +1,46 @@ | ||||
| --- The GPS API provides a method for turtles and computers to retrieve their | ||||
| -- own locations. | ||||
| -- | ||||
| -- It broadcasts a PING message over @{rednet} and wait for responses. In order | ||||
| -- for this system to work, there must be at least 4 computers used as gps hosts | ||||
| -- which will respond and allow trilateration. Three of these hosts should be in | ||||
| -- a plane, and the fourth should be either above or below the other three. The | ||||
| -- three in a plane should not be in a line with each other. You can set up | ||||
| -- hosts using the gps program. | ||||
| -- | ||||
| -- **Note**: When entering in the coordinates for the host you need to put in | ||||
| -- the `x`, `y`, and `z` coordinates of the computer, not the modem, as all | ||||
| -- rednet distances are measured from the block the computer is in. | ||||
| -- | ||||
| -- Also note that you may choose which axes x, y, or z refers to - so long as | ||||
| -- your systems have the same definition as any GPS servers that're in range, it | ||||
| -- works just the same. For example, you might build a GPS cluster according to | ||||
| -- [this tutorial][1], using z to account for height, or you might use y to | ||||
| -- account for height in the way that Minecraft's debug screen displays. | ||||
| -- | ||||
| -- [1]: http://www.computercraft.info/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/ | ||||
| -- | ||||
| -- @module gps | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| --- The channel which GPS requests and responses are broadcast on. | ||||
| CHANNEL_GPS = 65534 | ||||
|  | ||||
| local function trilaterate( A, B, C ) | ||||
| local function trilaterate(A, B, C) | ||||
|     local a2b = B.vPosition - A.vPosition | ||||
|     local a2c = C.vPosition - A.vPosition | ||||
|  | ||||
|     if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then | ||||
|     if math.abs(a2b:normalize():dot(a2c:normalize())) > 0.999 then | ||||
|         return nil | ||||
|     end | ||||
|  | ||||
|     local d = a2b:length() | ||||
|     local ex = a2b:normalize( ) | ||||
|     local i = ex:dot( a2c ) | ||||
|     local i = ex:dot(a2c) | ||||
|     local ey = (a2c - ex * i):normalize() | ||||
|     local j = ey:dot( a2c ) | ||||
|     local ez = ex:cross( ey ) | ||||
|     local j = ey:dot(a2c) | ||||
|     local ez = ex:cross(ey) | ||||
|  | ||||
|     local r1 = A.nDistance | ||||
|     local r2 = B.nDistance | ||||
| @@ -28,35 +53,44 @@ local function trilaterate( A, B, C ) | ||||
|  | ||||
|     local zSquared = r1 * r1 - x * x - y * y | ||||
|     if zSquared > 0 then | ||||
|         local z = math.sqrt( zSquared ) | ||||
|         local z = math.sqrt(zSquared) | ||||
|         local result1 = result + ez * z | ||||
|         local result2 = result - ez * z | ||||
|  | ||||
|         local rounded1, rounded2 = result1:round( 0.01 ), result2:round( 0.01 ) | ||||
|         local rounded1, rounded2 = result1:round(0.01), result2:round(0.01) | ||||
|         if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then | ||||
|             return rounded1, rounded2 | ||||
|         else | ||||
|             return rounded1 | ||||
|         end | ||||
|     end | ||||
|     return result:round( 0.01 ) | ||||
|     return result:round(0.01) | ||||
|  | ||||
| end | ||||
|  | ||||
| local function narrow( p1, p2, fix ) | ||||
|     local dist1 = math.abs( (p1 - fix.vPosition):length() - fix.nDistance ) | ||||
|     local dist2 = math.abs( (p2 - fix.vPosition):length() - fix.nDistance ) | ||||
| local function narrow(p1, p2, fix) | ||||
|     local dist1 = math.abs((p1 - fix.vPosition):length() - fix.nDistance) | ||||
|     local dist2 = math.abs((p2 - fix.vPosition):length() - fix.nDistance) | ||||
|  | ||||
|     if math.abs(dist1 - dist2) < 0.01 then | ||||
|         return p1, p2 | ||||
|     elseif dist1 < dist2 then | ||||
|         return p1:round( 0.01 ) | ||||
|         return p1:round(0.01) | ||||
|     else | ||||
|         return p2:round( 0.01 ) | ||||
|         return p2:round(0.01) | ||||
|     end | ||||
| end | ||||
|  | ||||
| function locate( _nTimeout, _bDebug ) | ||||
| --- 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 | ||||
| -- @treturn[1] number This computer's `x` position. | ||||
| -- @treturn[1] number This computer's `y` position. | ||||
| -- @treturn[1] number This computer's `z` position. | ||||
| -- @treturn[2] nil If the position could not be established. | ||||
| function locate(_nTimeout, _bDebug) | ||||
|     expect(1, _nTimeout, "number", "nil") | ||||
|     expect(2, _bDebug, "boolean", "nil") | ||||
|     -- Let command computers use their magic fourth-wall-breaking special abilities | ||||
| @@ -66,8 +100,8 @@ function locate( _nTimeout, _bDebug ) | ||||
|  | ||||
|     -- Find a modem | ||||
|     local sModemSide = nil | ||||
|     for _, sSide in ipairs( rs.getSides() ) do | ||||
|         if peripheral.getType( sSide ) == "modem" and peripheral.call( sSide, "isWireless" ) then | ||||
|     for _, sSide in ipairs(rs.getSides()) do | ||||
|         if peripheral.getType(sSide) == "modem" and peripheral.call(sSide, "isWireless") then | ||||
|             sModemSide = sSide | ||||
|             break | ||||
|         end | ||||
| @@ -75,30 +109,30 @@ function locate( _nTimeout, _bDebug ) | ||||
|  | ||||
|     if sModemSide == nil then | ||||
|         if _bDebug then | ||||
|             print( "No wireless modem attached" ) | ||||
|             print("No wireless modem attached") | ||||
|         end | ||||
|         return nil | ||||
|     end | ||||
|  | ||||
|     if _bDebug then | ||||
|         print( "Finding position..." ) | ||||
|         print("Finding position...") | ||||
|     end | ||||
|  | ||||
|     -- Open GPS channel to listen for ping responses | ||||
|     local modem = peripheral.wrap( sModemSide ) | ||||
|     local modem = peripheral.wrap(sModemSide) | ||||
|     local bCloseChannel = false | ||||
|     if not modem.isOpen( CHANNEL_GPS ) then | ||||
|         modem.open( CHANNEL_GPS ) | ||||
|     if not modem.isOpen(CHANNEL_GPS) then | ||||
|         modem.open(CHANNEL_GPS) | ||||
|         bCloseChannel = true | ||||
|     end | ||||
|  | ||||
|     -- Send a ping to listening GPS hosts | ||||
|     modem.transmit( CHANNEL_GPS, CHANNEL_GPS, "PING" ) | ||||
|     modem.transmit(CHANNEL_GPS, CHANNEL_GPS, "PING") | ||||
|  | ||||
|     -- Wait for the responses | ||||
|     local tFixes = {} | ||||
|     local pos1, pos2 = nil, nil | ||||
|     local timeout = os.startTimer( _nTimeout or 2 ) | ||||
|     local timeout = os.startTimer(_nTimeout or 2) | ||||
|     while true do | ||||
|         local e, p1, p2, p3, p4, p5 = os.pullEvent() | ||||
|         if e == "modem_message" then | ||||
| @@ -107,19 +141,19 @@ function locate( _nTimeout, _bDebug ) | ||||
|             if sSide == sModemSide and sChannel == CHANNEL_GPS and sReplyChannel == CHANNEL_GPS and nDistance then | ||||
|                 -- Received the correct message from the correct modem: use it to determine position | ||||
|                 if type(tMessage) == "table" and #tMessage == 3 and tonumber(tMessage[1]) and tonumber(tMessage[2]) and tonumber(tMessage[3]) then | ||||
|                     local tFix = { vPosition = vector.new( tMessage[1], tMessage[2], tMessage[3] ), nDistance = nDistance } | ||||
|                     local tFix = { vPosition = vector.new(tMessage[1], tMessage[2], tMessage[3]), nDistance = nDistance } | ||||
|                     if _bDebug then | ||||
|                         print( tFix.nDistance .. " metres from " .. tostring( tFix.vPosition ) ) | ||||
|                         print(tFix.nDistance .. " metres from " .. tostring(tFix.vPosition)) | ||||
|                     end | ||||
|                     if tFix.nDistance == 0 then | ||||
|                         pos1, pos2 = tFix.vPosition, nil | ||||
|                     else | ||||
|                         table.insert( tFixes, tFix ) | ||||
|                         table.insert(tFixes, tFix) | ||||
|                         if #tFixes >= 3 then | ||||
|                             if not pos1 then | ||||
|                                 pos1, pos2 = trilaterate( tFixes[1], tFixes[2], tFixes[#tFixes] ) | ||||
|                                 pos1, pos2 = trilaterate(tFixes[1], tFixes[2], tFixes[#tFixes]) | ||||
|                             else | ||||
|                                 pos1, pos2 = narrow( pos1, pos2, tFixes[#tFixes] ) | ||||
|                                 pos1, pos2 = narrow(pos1, pos2, tFixes[#tFixes]) | ||||
|                             end | ||||
|                         end | ||||
|                     end | ||||
| @@ -141,24 +175,24 @@ function locate( _nTimeout, _bDebug ) | ||||
|  | ||||
|     -- Close the channel, if we opened one | ||||
|     if bCloseChannel then | ||||
|         modem.close( CHANNEL_GPS ) | ||||
|         modem.close(CHANNEL_GPS) | ||||
|     end | ||||
|  | ||||
|     -- Return the response | ||||
|     if pos1 and pos2 then | ||||
|         if _bDebug then | ||||
|             print( "Ambiguous position" ) | ||||
|             print( "Could be " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z .. " or " .. pos2.x .. "," .. pos2.y .. "," .. pos2.z ) | ||||
|             print("Ambiguous position") | ||||
|             print("Could be " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z .. " or " .. pos2.x .. "," .. pos2.y .. "," .. pos2.z) | ||||
|         end | ||||
|         return nil | ||||
|     elseif pos1 then | ||||
|         if _bDebug then | ||||
|             print( "Position is " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z ) | ||||
|             print("Position is " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z) | ||||
|         end | ||||
|         return pos1.x, pos1.y, pos1.z | ||||
|     else | ||||
|         if _bDebug then | ||||
|             print( "Could not determine position" ) | ||||
|             print("Could not determine position") | ||||
|         end | ||||
|         return nil | ||||
|     end | ||||
|   | ||||
| @@ -1,24 +1,46 @@ | ||||
| --- Provides an API to read help files. | ||||
| -- | ||||
| -- @module help | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| local sPath = "/rom/help" | ||||
|  | ||||
| --- Returns a colon-separated list of directories where help files are searched | ||||
| -- for. All directories are absolute. | ||||
| -- | ||||
| -- @treturn string The current help search path, separated by colons. | ||||
| -- @see help.setPath | ||||
| function path() | ||||
|     return sPath | ||||
| end | ||||
|  | ||||
| function setPath( _sPath ) | ||||
| --- Sets the colon-seperated list of directories where help files are searched | ||||
| -- for to `newPath` | ||||
| -- | ||||
| -- @tparam string newPath The new path to use. | ||||
| -- @usage help.setPath( "/disk/help/" ) | ||||
| -- @usage help.setPath( help.path() .. ":/myfolder/help/" ) | ||||
| -- @see help.path | ||||
| function setPath(_sPath) | ||||
|     expect(1, _sPath, "string") | ||||
|     sPath = _sPath | ||||
| end | ||||
|  | ||||
| function lookup( _sTopic ) | ||||
| --- Returns the location of the help file for the given topic. | ||||
| -- | ||||
| -- @tparam string topic The topic to find | ||||
| -- @treturn string|nil The path to the given topic's help file, or `nil` if it | ||||
| -- cannot be found. | ||||
| -- @usage print(help.lookup("disk")) | ||||
| function lookup(_sTopic) | ||||
|     expect(1, _sTopic, "string") | ||||
|     -- Look on the path variable | ||||
|     for sPath in string.gmatch(sPath, "[^:]+") do | ||||
|         sPath = fs.combine( sPath, _sTopic ) | ||||
|         if fs.exists( sPath ) and not fs.isDir( sPath ) then | ||||
|         sPath = fs.combine(sPath, _sTopic) | ||||
|         if fs.exists(sPath) and not fs.isDir(sPath) then | ||||
|             return sPath | ||||
|         elseif fs.exists( sPath .. ".txt" ) and not fs.isDir( sPath .. ".txt" ) then | ||||
|         elseif fs.exists(sPath .. ".txt") and not fs.isDir(sPath .. ".txt") then | ||||
|             return sPath .. ".txt" | ||||
|         end | ||||
|     end | ||||
| @@ -27,23 +49,26 @@ function lookup( _sTopic ) | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| --- Returns a list of topics that can be looked up and/or displayed. | ||||
| -- | ||||
| -- @treturn table A list of topics in alphabetical order. | ||||
| function topics() | ||||
|     -- Add index | ||||
|     local tItems = { | ||||
|         [ "index" ] = true, | ||||
|         ["index"] = true, | ||||
|     } | ||||
|  | ||||
|     -- Add topics from the path | ||||
|     for sPath in string.gmatch(sPath, "[^:]+") do | ||||
|         if fs.isDir( sPath ) then | ||||
|             local tList = fs.list( sPath ) | ||||
|             for _, sFile in pairs( tList ) do | ||||
|                 if string.sub( sFile, 1, 1 ) ~= "." then | ||||
|                     if not fs.isDir( fs.combine( sPath, sFile ) ) then | ||||
|         if fs.isDir(sPath) then | ||||
|             local tList = fs.list(sPath) | ||||
|             for _, sFile in pairs(tList) do | ||||
|                 if string.sub(sFile, 1, 1) ~= "." then | ||||
|                     if not fs.isDir(fs.combine(sPath, sFile)) then | ||||
|                         if #sFile > 4 and sFile:sub(-4) == ".txt" then | ||||
|                             sFile = sFile:sub(1, -5) | ||||
|                         end | ||||
|                         tItems[ sFile ] = true | ||||
|                         tItems[sFile] = true | ||||
|                     end | ||||
|                 end | ||||
|             end | ||||
| @@ -52,21 +77,26 @@ function topics() | ||||
|  | ||||
|     -- Sort and return | ||||
|     local tItemList = {} | ||||
|     for sItem in pairs( tItems ) do | ||||
|         table.insert( tItemList, sItem ) | ||||
|     for sItem in pairs(tItems) do | ||||
|         table.insert(tItemList, sItem) | ||||
|     end | ||||
|     table.sort( tItemList ) | ||||
|     table.sort(tItemList) | ||||
|     return tItemList | ||||
| end | ||||
|  | ||||
| function completeTopic( sText ) | ||||
| --- Returns a list of topic endings that match the prefix. Can be used with | ||||
| -- `read` to allow input of a help topic. | ||||
| -- | ||||
| -- @tparam string prefix The prefix to match | ||||
| -- @treturn table A list of matching topics. | ||||
| function completeTopic(sText) | ||||
|     expect(1, sText, "string") | ||||
|     local tTopics = topics() | ||||
|     local tResults = {} | ||||
|     for n = 1, #tTopics do | ||||
|         local sTopic = tTopics[n] | ||||
|         if #sTopic > #sText and string.sub( sTopic, 1, #sText ) == sText then | ||||
|             table.insert( tResults, string.sub( sTopic, #sText + 1 ) ) | ||||
|         if #sTopic > #sText and string.sub(sTopic, 1, #sText) == sText then | ||||
|             table.insert(tResults, string.sub(sTopic, #sText + 1)) | ||||
|         end | ||||
|     end | ||||
|     return tResults | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| -- Definition for the IO API | ||||
| --- Emulates Lua's standard [io library][io]. | ||||
| -- | ||||
| -- [io]: https://www.lua.org/manual/5.1/manual.html#5.7 | ||||
| -- | ||||
| -- @module io | ||||
|  | ||||
| local expect, typeOf = dofile("rom/modules/main/cc/expect.lua").expect, _G.type | ||||
| local expect, type_of = dofile("rom/modules/main/cc/expect.lua").expect, _G.type | ||||
|  | ||||
| --- If we return nil then close the file, as we've reached the end. | ||||
| -- We use this weird wrapper function as we wish to preserve the varargs | ||||
| @@ -9,6 +13,9 @@ local function checkResult(handle, ...) | ||||
|     return ... | ||||
| end | ||||
|  | ||||
| --- A file handle which can be read or written to. | ||||
| -- | ||||
| -- @type Handle | ||||
| local handleMetatable | ||||
| handleMetatable = { | ||||
|     __name = "FILE*", | ||||
| @@ -20,10 +27,17 @@ handleMetatable = { | ||||
|             return "file (" .. hash .. ")" | ||||
|         end | ||||
|     end, | ||||
|  | ||||
|     __index = { | ||||
|         --- Close this file handle, freeing any resources it uses. | ||||
|         -- | ||||
|         -- @treturn[1] true If this handle was successfully closed. | ||||
|         -- @treturn[2] nil If this file handle could not be closed. | ||||
|         -- @treturn[2] string The reason it could not be closed. | ||||
|         -- @throws If this handle was already closed. | ||||
|         close = function(self) | ||||
|             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||
|             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) | ||||
|             end | ||||
|             if self._closed then error("attempt to use a closed file", 2) end | ||||
|  | ||||
| @@ -36,18 +50,24 @@ handleMetatable = { | ||||
|                 return nil, "attempt to close standard stream" | ||||
|             end | ||||
|         end, | ||||
|  | ||||
|         --- Flush any buffered output, forcing it to be written to the file | ||||
|         -- | ||||
|         -- @throws If the handle has been closed | ||||
|         flush = function(self) | ||||
|             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||
|             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) | ||||
|             end | ||||
|             if self._closed then error("attempt to use a closed file", 2) end | ||||
|  | ||||
|             local handle = self._handle | ||||
|             if handle.flush then handle.flush() end | ||||
|             return true | ||||
|         end, | ||||
|  | ||||
|         lines = function(self, ...) | ||||
|             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||
|             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) | ||||
|             end | ||||
|             if self._closed then error("attempt to use a closed file", 2) end | ||||
|  | ||||
| @@ -57,9 +77,10 @@ handleMetatable = { | ||||
|             local args = table.pack(...) | ||||
|             return function() return checkResult(self, self:read(table.unpack(args, 1, args.n))) end | ||||
|         end, | ||||
|  | ||||
|         read = function(self, ...) | ||||
|             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||
|             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) | ||||
|             end | ||||
|             if self._closed then error("attempt to use a closed file", 2) end | ||||
|  | ||||
| @@ -71,9 +92,9 @@ handleMetatable = { | ||||
|             for i = 1, n do | ||||
|                 local arg = select(i, ...) | ||||
|                 local res | ||||
|                 if typeOf(arg) == "number" then | ||||
|                 if type_of(arg) == "number" then | ||||
|                     if handle.read then res = handle.read(arg) end | ||||
|                 elseif typeOf(arg) == "string" then | ||||
|                 elseif type_of(arg) == "string" then | ||||
|                     local format = arg:gsub("^%*", ""):sub(1, 1) | ||||
|  | ||||
|                     if format == "l" then | ||||
| @@ -88,7 +109,7 @@ handleMetatable = { | ||||
|                         error("bad argument #" .. i .. " (invalid format)", 2) | ||||
|                     end | ||||
|                 else | ||||
|                     error("bad argument #" .. i .. " (expected string, got " .. typeOf(arg) .. ")", 2) | ||||
|                     error("bad argument #" .. i .. " (expected string, got " .. type_of(arg) .. ")", 2) | ||||
|                 end | ||||
|  | ||||
|                 output[i] = res | ||||
| @@ -99,9 +120,10 @@ handleMetatable = { | ||||
|             if n == 0 and handle.readLine then return handle.readLine() end | ||||
|             return table.unpack(output, 1, n) | ||||
|         end, | ||||
|  | ||||
|         seek = function(self, whence, offset) | ||||
|             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||
|             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) | ||||
|             end | ||||
|             if self._closed then error("attempt to use a closed file", 2) end | ||||
|  | ||||
| @@ -111,10 +133,18 @@ handleMetatable = { | ||||
|             -- It's a tail call, so error positions are preserved | ||||
|             return handle.seek(whence, offset) | ||||
|         end, | ||||
|  | ||||
|         setvbuf = function(self, mode, size) end, | ||||
|  | ||||
|         --- Write one or more values to the file | ||||
|         -- | ||||
|         -- @tparam string|number ... The values to write. | ||||
|         -- @treturn[1] Handle The current file, allowing chained calls. | ||||
|         -- @treturn[2] nil If the file could not be written to. | ||||
|         -- @treturn[2] string The error message which occurred while writing. | ||||
|         write = function(self, ...) | ||||
|             if typeOf(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. typeOf(self) .. ")", 2) | ||||
|             if type_of(self) ~= "table" or getmetatable(self) ~= handleMetatable then | ||||
|                 error("bad argument #1 (FILE expected, got " .. type_of(self) .. ")", 2) | ||||
|             end | ||||
|             if self._closed then error("attempt to use a closed file", 2) end | ||||
|  | ||||
| @@ -156,41 +186,88 @@ local defaultError = setmetatable({ | ||||
| local currentInput = defaultInput | ||||
| local currentOutput = defaultOutput | ||||
|  | ||||
| --- A file handle representing the "standard input". Reading from this | ||||
| -- file will prompt the user for input. | ||||
| stdin = defaultInput | ||||
|  | ||||
| --- A file handle representing the "standard output". Writing to this | ||||
| -- file will display the written text to the screen. | ||||
| stdout = defaultOutput | ||||
|  | ||||
| --- A file handle representing the "standard error" stream. | ||||
| -- | ||||
| -- One may use this to display error messages, writing to it will display | ||||
| -- them on the terminal. | ||||
| stderr = defaultError | ||||
|  | ||||
| function close(_file) | ||||
|     if _file == nil then return currentOutput:close() end | ||||
| --- Closes the provided file handle. | ||||
| -- | ||||
| -- @tparam[opt] Handle file The file handle to close, defaults to the | ||||
| -- current output file. | ||||
| -- | ||||
| -- @see Handle:close | ||||
| -- @see io.output | ||||
| function close(file) | ||||
|     if file == nil then return currentOutput:close() end | ||||
|  | ||||
|     if typeOf(_file) ~= "table" or getmetatable(_file) ~= handleMetatable then | ||||
|         error("bad argument #1 (FILE expected, got " .. typeOf(_file) .. ")", 2) | ||||
|     if type_of(file) ~= "table" or getmetatable(file) ~= handleMetatable then | ||||
|         error("bad argument #1 (FILE expected, got " .. type_of(file) .. ")", 2) | ||||
|     end | ||||
|     return _file:close() | ||||
|     return file:close() | ||||
| end | ||||
|  | ||||
| --- Flushes the current output file. | ||||
| -- | ||||
| -- @see Handle:flush | ||||
| -- @see io.output | ||||
| function flush() | ||||
|     return currentOutput:flush() | ||||
| end | ||||
|  | ||||
| function input(_arg) | ||||
|     if typeOf(_arg) == "string" then | ||||
|         local res, err = open(_arg, "rb") | ||||
| --- Get or set the current input file. | ||||
| -- | ||||
| -- @tparam[opt] Handle|string file The new input file, either as a file path or pre-existing handle. | ||||
| -- @treturn Handle The current input file. | ||||
| -- @throws If the provided filename cannot be opened for reading. | ||||
| function input(file) | ||||
|     if type_of(file) == "string" then | ||||
|         local res, err = open(file, "rb") | ||||
|         if not res then error(err, 2) end | ||||
|         currentInput = res | ||||
|     elseif typeOf(_arg) == "table" and getmetatable(_arg) == handleMetatable then | ||||
|         currentInput = _arg | ||||
|     elseif _arg ~= nil then | ||||
|         error("bad argument #1 (FILE expected, got " .. typeOf(_arg) .. ")", 2) | ||||
|     elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then | ||||
|         currentInput = file | ||||
|     elseif file ~= nil then | ||||
|         error("bad fileument #1 (FILE expected, got " .. type_of(file) .. ")", 2) | ||||
|     end | ||||
|  | ||||
|     return currentInput | ||||
| end | ||||
|  | ||||
| function lines(_sFileName) | ||||
|     expect(1, _sFileName, "string", "nil") | ||||
|     if _sFileName then | ||||
|         local ok, err = open(_sFileName, "rb") | ||||
| --- Opens the given file name in read mode and returns an iterator that, | ||||
| -- each time it is called, returns a new line from the file. | ||||
| -- | ||||
| -- This can be used in a for loop to iterate over all lines of a file: | ||||
| -- | ||||
| -- ```lua | ||||
| -- for line in io.lines(filename) do print(line) end | ||||
| -- ``` | ||||
| -- | ||||
| -- Once the end of the file has been reached, @{nil} will be | ||||
| -- returned. The file is automatically closed. | ||||
| -- | ||||
| -- If no file name is given, the @{io.input|current input} will be used | ||||
| -- instead. In this case, the handle is not used. | ||||
| -- | ||||
| -- @tparam[opt] string filename The name of the file to extract lines from | ||||
| -- @treturn function():string|nil The line iterator. | ||||
| -- @throws If the file cannot be opened for reading | ||||
| -- | ||||
| -- @see Handle:lines | ||||
| -- @see io.input | ||||
| function lines(filename) | ||||
|     expect(1, filename, "string", "nil") | ||||
|     if filename then | ||||
|         local ok, err = open(filename, "rb") | ||||
|         if not ok then error(err, 2) end | ||||
|  | ||||
|         -- We set this magic flag to mark this file as being opened by io.lines and so should be | ||||
| @@ -202,38 +279,72 @@ function lines(_sFileName) | ||||
|     end | ||||
| end | ||||
|  | ||||
| function open(_sPath, _sMode) | ||||
|     expect(1, _sPath, "string") | ||||
|     expect(2, _sMode, "string", "nil") | ||||
| --- Open a file with the given mode, either returning a new file handle | ||||
| -- or @{nil}, plus an error message. | ||||
| -- | ||||
| -- The `mode` string can be any of the following: | ||||
| --  - **"r"**: Read mode | ||||
| --  - **"w"**: Write mode | ||||
| --  - **"w"**: Append mode | ||||
| -- | ||||
| -- The mode may also have a `b` at the end, which opens the file in "binary | ||||
| -- mode". This allows you to read binary files, as well as seek within a file. | ||||
| -- | ||||
| -- @tparam string filename The name of the file to open. | ||||
| -- @tparam[opt] string mode The mode to open the file with. This defaults to `rb`. | ||||
| -- @treturn[1] Handle The opened file. | ||||
| -- @treturn[2] nil In case of an error. | ||||
| -- @treturn[2] string The reason the file could not be opened. | ||||
| function open(filename, mode) | ||||
|     expect(1, filename, "string") | ||||
|     expect(2, mode, "string", "nil") | ||||
|  | ||||
|     local sMode = _sMode and _sMode:gsub("%+", "") or "rb" | ||||
|     local file, err = fs.open(_sPath, sMode) | ||||
|     local sMode = mode and mode:gsub("%+", "") or "rb" | ||||
|     local file, err = fs.open(filename, sMode) | ||||
|     if not file then return nil, err end | ||||
|  | ||||
|     return setmetatable({ _handle = file }, handleMetatable) | ||||
| end | ||||
|  | ||||
| function output(_arg) | ||||
|     if typeOf(_arg) == "string" then | ||||
|         local res, err = open(_arg, "w") | ||||
| --- Get or set the current output file. | ||||
| -- | ||||
| -- @tparam[opt] Handle|string file The new output file, either as a file path or pre-existing handle. | ||||
| -- @treturn Handle The current output file. | ||||
| -- @throws If the provided filename cannot be opened for writing. | ||||
| function output(file) | ||||
|     if type_of(file) == "string" then | ||||
|         local res, err = open(file, "w") | ||||
|         if not res then error(err, 2) end | ||||
|         currentOutput = res | ||||
|     elseif typeOf(_arg) == "table" and getmetatable(_arg) == handleMetatable then | ||||
|         currentOutput = _arg | ||||
|     elseif _arg ~= nil then | ||||
|         error("bad argument #1 (FILE expected, got " .. typeOf(_arg) .. ")", 2) | ||||
|     elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then | ||||
|         currentOutput = file | ||||
|     elseif file ~= nil then | ||||
|         error("bad argument #1 (FILE expected, got " .. type_of(file) .. ")", 2) | ||||
|     end | ||||
|  | ||||
|     return currentOutput | ||||
| end | ||||
|  | ||||
| --- Read from the currently opened input file. | ||||
| -- | ||||
| -- This is equivalent to `io.input():read(...)`. See @{Handle:read|the | ||||
| -- documentation} there for full details. | ||||
| -- | ||||
| -- @tparam string ... The formats to read, defaulting to a whole line. | ||||
| -- @treturn (string|nil)... The data read, or @{nil} if nothing can be read. | ||||
| function read(...) | ||||
|     return currentInput:read(...) | ||||
| end | ||||
|  | ||||
| function type(handle) | ||||
|     if typeOf(handle) == "table" and getmetatable(handle) == handleMetatable then | ||||
|         if handle._closed then | ||||
| --- Checks whether `handle` is a given file handle, and determine if it is open | ||||
| -- or not. | ||||
| -- | ||||
| -- @param obj The value to check | ||||
| -- @treturn string|nil `"file"` if this is an open file, `"closed file"` if it | ||||
| -- is a closed file handle, or `nil` if not a file handle. | ||||
| function type(obj) | ||||
|     if type_of(obj) == "table" and getmetatable(obj) == handleMetatable then | ||||
|         if obj._closed then | ||||
|             return "closed file" | ||||
|         else | ||||
|             return "file" | ||||
| @@ -242,6 +353,12 @@ function type(handle) | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| --- Write to the currently opened output file. | ||||
| -- | ||||
| -- This is equivalent to `io.output():write(...)`. See @{Handle:write|the | ||||
| -- documentation} there for full details. | ||||
| -- | ||||
| -- @tparam string ... The strings to write | ||||
| function write(...) | ||||
|     return currentOutput:write(...) | ||||
| end | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| --- Key codes for ComputerCraft | ||||
| --- The Keys API provides a table of numerical codes corresponding to keyboard | ||||
| -- keys, suitable for decoding key events. | ||||
| -- | ||||
| -- This is derived from the GLFW list of key codes, and mostly created via | ||||
| -- a couple of regexes. | ||||
| -- These values are not guaranteed to remain the same between versions. It is | ||||
| -- recommended that you use the constants provided by this file, rather than | ||||
| -- the underlying numerical values. | ||||
| -- | ||||
| -- Note that this is technically incompatible with previous versions of CC, as | ||||
| -- they relied on Minecraft's character mappings. However, if CC emulators have | ||||
| -- taught me anything, it's that emulating LWJGL's weird key handling is nigh-on | ||||
| -- impossible. | ||||
| -- @module keys | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| @@ -137,10 +136,15 @@ for nKey, sKey in pairs( tKeys ) do | ||||
| end | ||||
|  | ||||
| -- Alias some keys for ease-of-use and backwards compatibility | ||||
| keys["return"] = keys.enter | ||||
| keys.scollLock = keys.scrollLock | ||||
| keys.cimcumflex = keys.circumflex | ||||
| keys["return"] = keys.enter --- @local | ||||
| keys.scollLock = keys.scrollLock --- @local | ||||
| keys.cimcumflex = keys.circumflex --- @local | ||||
|  | ||||
| --- Translates a numerical key code to a human-readable name. The human-readable | ||||
| -- name is one of the constants in the keys API. | ||||
| -- | ||||
| -- @tparam number code The key code to look up. | ||||
| -- @treturn string|nil The name of the key, or `nil` if not a valid key code. | ||||
| function getName( _nKey ) | ||||
|     expect(1, _nKey, "number") | ||||
|     return tKeys[ _nKey ] | ||||
|   | ||||
| @@ -1,75 +1,117 @@ | ||||
| --- An API for advanced systems which can draw pixels and lines, load and draw | ||||
| -- image files. You can use the `colors` API for easier color manipulation. | ||||
| -- | ||||
| -- @module paintutils | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| local function drawPixelInternal( xPos, yPos ) | ||||
|     term.setCursorPos( xPos, yPos ) | ||||
| local function drawPixelInternal(xPos, yPos) | ||||
|     term.setCursorPos(xPos, yPos) | ||||
|     term.write(" ") | ||||
| end | ||||
|  | ||||
| local tColourLookup = {} | ||||
| for n = 1, 16 do | ||||
|     tColourLookup[ string.byte( "0123456789abcdef", n, n ) ] = 2 ^ (n - 1) | ||||
|     tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) | ||||
| end | ||||
|  | ||||
| local function parseLine( tImageArg, sLine ) | ||||
| local function parseLine(tImageArg, sLine) | ||||
|     local tLine = {} | ||||
|     for x = 1, sLine:len() do | ||||
|         tLine[x] = tColourLookup[ string.byte(sLine, x, x) ] or 0 | ||||
|         tLine[x] = tColourLookup[string.byte(sLine, x, x)] or 0 | ||||
|     end | ||||
|     table.insert( tImageArg, tLine ) | ||||
|     table.insert(tImageArg, tLine) | ||||
| end | ||||
|  | ||||
| function parseImage( sRawData ) | ||||
|     expect(1, sRawData, "string") | ||||
| --- Parses an image from a multi-line string | ||||
| -- | ||||
| -- @tparam string image The string containing the raw-image data. | ||||
| -- @treturn table The parsed image data, suitable for use with | ||||
| -- @{paintutils.drawImage}. | ||||
| function parseImage(image) | ||||
|     expect(1, image, "string") | ||||
|     local tImage = {} | ||||
|     for sLine in ( sRawData .. "\n" ):gmatch( "(.-)\n" ) do -- read each line like original file handling did | ||||
|         parseLine( tImage, sLine ) | ||||
|     for sLine in (image .. "\n"):gmatch("(.-)\n") do | ||||
|         parseLine(tImage, sLine) | ||||
|     end | ||||
|     return tImage | ||||
| end | ||||
|  | ||||
| function loadImage( sPath ) | ||||
|     expect(1, sPath, "string") | ||||
| --- Loads an image from a file. | ||||
| -- | ||||
| -- You can create a file suitable for being loaded using the `paint` program. | ||||
| -- | ||||
| -- @tparam string path The file to load. | ||||
| -- | ||||
| -- @treturn table|nil The parsed image data, suitable for use with | ||||
| -- @{paintutils.drawImage}, or `nil` if the file does not exist. | ||||
| function loadImage(path) | ||||
|     expect(1, path, "string") | ||||
|  | ||||
|     if fs.exists( sPath ) then | ||||
|         local file = io.open( sPath, "r" ) | ||||
|     if fs.exists(path) then | ||||
|         local file = io.open(path, "r") | ||||
|         local sContent = file:read("*a") | ||||
|         file:close() | ||||
|         return parseImage( sContent ) -- delegate image parse to parseImage | ||||
|         return parseImage(sContent) | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function drawPixel( xPos, yPos, nColour ) | ||||
| --- Draws a single pixel to the current term at the specified position. | ||||
| -- | ||||
| -- Be warned, this may change the position of the cursor and the current | ||||
| -- background colour. You should not expect either to be preserved. | ||||
| -- | ||||
| -- @tparam number xPos The x position to draw at, where 1 is the far left. | ||||
| -- @tparam number yPos The y position to draw at, where 1 is the very top. | ||||
| -- @tparam[opt] number colour The @{colors|color} of this pixel. This will be | ||||
| -- the current background colour if not specified. | ||||
| function drawPixel(xPos, yPos, colour) | ||||
|     expect(1, xPos, "number") | ||||
|     expect(2, yPos, "number") | ||||
|     expect(3, nColour, "number", "nil") | ||||
|     if nColour then | ||||
|         term.setBackgroundColor( nColour ) | ||||
|     expect(3, colour, "number", "nil") | ||||
|  | ||||
|     if type(xPos) ~= "number" then error("bad argument #1 (expected number, got " .. type(xPos) .. ")", 2) end | ||||
|     if type(yPos) ~= "number" then error("bad argument #2 (expected number, got " .. type(yPos) .. ")", 2) end | ||||
|     if colour ~= nil and type(colour) ~= "number" then error("bad argument #3 (expected number, got " .. type(colour) .. ")", 2) end | ||||
|     if colour then | ||||
|         term.setBackgroundColor(colour) | ||||
|     end | ||||
|     return drawPixelInternal( xPos, yPos ) | ||||
|     return drawPixelInternal(xPos, yPos) | ||||
| end | ||||
|  | ||||
| function drawLine( startX, startY, endX, endY, nColour ) | ||||
| --- Draws a straight line from the start to end position. | ||||
| -- | ||||
| -- Be warned, this may change the position of the cursor and the current | ||||
| -- background colour. You should not expect either to be preserved. | ||||
| -- | ||||
| -- @tparam number startX The starting x position of the line. | ||||
| -- @tparam number startY The starting y position of the line. | ||||
| -- @tparam number endX The end x position of the line. | ||||
| -- @tparam number endY The end y position of the line. | ||||
| -- @tparam[opt] number colour The @{colors|color} of this pixel. This will be | ||||
| -- the current background colour if not specified. | ||||
| function drawLine(startX, startY, endX, endY, colour) | ||||
|     expect(1, startX, "number") | ||||
|     expect(2, startY, "number") | ||||
|     expect(3, endX, "number") | ||||
|     expect(4, endY, "number") | ||||
|     expect(5, nColour, "number", "nil") | ||||
|     expect(5, colour, "number", "nil") | ||||
|  | ||||
|     startX = math.floor(startX) | ||||
|     startY = math.floor(startY) | ||||
|     endX = math.floor(endX) | ||||
|     endY = math.floor(endY) | ||||
|  | ||||
|     if nColour then | ||||
|         term.setBackgroundColor( nColour ) | ||||
|     if colour then | ||||
|         term.setBackgroundColor(colour) | ||||
|     end | ||||
|     if startX == endX and startY == endY then | ||||
|         drawPixelInternal( startX, startY ) | ||||
|         drawPixelInternal(startX, startY) | ||||
|         return | ||||
|     end | ||||
|  | ||||
|     local minX = math.min( startX, endX ) | ||||
|     local minX = math.min(startX, endX) | ||||
|     local maxX, minY, maxY | ||||
|     if minX == startX then | ||||
|         minY = startY | ||||
| @@ -90,7 +132,7 @@ function drawLine( startX, startY, endX, endY, nColour ) | ||||
|         local y = minY | ||||
|         local dy = yDiff / xDiff | ||||
|         for x = minX, maxX do | ||||
|             drawPixelInternal( x, math.floor( y + 0.5 ) ) | ||||
|             drawPixelInternal(x, math.floor(y + 0.5)) | ||||
|             y = y + dy | ||||
|         end | ||||
|     else | ||||
| @@ -98,19 +140,31 @@ function drawLine( startX, startY, endX, endY, nColour ) | ||||
|         local dx = xDiff / yDiff | ||||
|         if maxY >= minY then | ||||
|             for y = minY, maxY do | ||||
|                 drawPixelInternal( math.floor( x + 0.5 ), y ) | ||||
|                 drawPixelInternal(math.floor(x + 0.5), y) | ||||
|                 x = x + dx | ||||
|             end | ||||
|         else | ||||
|             for y = minY, maxY, -1 do | ||||
|                 drawPixelInternal( math.floor( x + 0.5 ), y ) | ||||
|                 drawPixelInternal(math.floor(x + 0.5), y) | ||||
|                 x = x - dx | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| function drawBox( startX, startY, endX, endY, nColour ) | ||||
| --- Draws the outline of a box on the current term from the specified start | ||||
| -- position to the specified end position. | ||||
| -- | ||||
| -- Be warned, this may change the position of the cursor and the current | ||||
| -- background colour. You should not expect either to be preserved. | ||||
| -- | ||||
| -- @tparam number startX The starting x position of the line. | ||||
| -- @tparam number startY The starting y position of the line. | ||||
| -- @tparam number endX The end x position of the line. | ||||
| -- @tparam number endY The end y position of the line. | ||||
| -- @tparam[opt] number colour The @{colors|color} of this pixel. This will be | ||||
| -- the current background colour if not specified. | ||||
| function drawBox(startX, startY, endX, endY, nColour) | ||||
|     expect(1, startX, "number") | ||||
|     expect(2, startY, "number") | ||||
|     expect(3, endX, "number") | ||||
| @@ -123,14 +177,14 @@ function drawBox( startX, startY, endX, endY, nColour ) | ||||
|     endY = math.floor(endY) | ||||
|  | ||||
|     if nColour then | ||||
|         term.setBackgroundColor( nColour ) | ||||
|         term.setBackgroundColor(nColour) | ||||
|     end | ||||
|     if startX == endX and startY == endY then | ||||
|         drawPixelInternal( startX, startY ) | ||||
|         drawPixelInternal(startX, startY) | ||||
|         return | ||||
|     end | ||||
|  | ||||
|     local minX = math.min( startX, endX ) | ||||
|     local minX = math.min(startX, endX) | ||||
|     local maxX, minY, maxY | ||||
|     if minX == startX then | ||||
|         minY = startY | ||||
| @@ -143,19 +197,30 @@ function drawBox( startX, startY, endX, endY, nColour ) | ||||
|     end | ||||
|  | ||||
|     for x = minX, maxX do | ||||
|         drawPixelInternal( x, minY ) | ||||
|         drawPixelInternal( x, maxY ) | ||||
|         drawPixelInternal(x, minY) | ||||
|         drawPixelInternal(x, maxY) | ||||
|     end | ||||
|  | ||||
|     if maxY - minY >= 2 then | ||||
|         for y = minY + 1, maxY - 1 do | ||||
|             drawPixelInternal( minX, y ) | ||||
|             drawPixelInternal( maxX, y ) | ||||
|             drawPixelInternal(minX, y) | ||||
|             drawPixelInternal(maxX, y) | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| function drawFilledBox( startX, startY, endX, endY, nColour ) | ||||
| --- Draws a filled box on the current term from the specified start position to | ||||
| -- the specified end position. | ||||
| -- | ||||
| -- Be warned, this may change the position of the cursor and the current | ||||
| -- background colour. You should not expect either to be preserved. | ||||
| -- | ||||
| -- @tparam number startX The starting x position of the line. | ||||
| -- @tparam number startY The starting y position of the line. | ||||
| -- @tparam number endX The end x position of the line. | ||||
| -- @tparam number endY The end y position of the line. | ||||
| -- @tparam[opt] number colour The @{colors|color} of this pixel. This will be | ||||
| -- the current background colour if not specified. | ||||
| function drawFilledBox(startX, startY, endX, endY, nColour) | ||||
|     expect(1, startX, "number") | ||||
|     expect(2, startY, "number") | ||||
|     expect(3, endX, "number") | ||||
| @@ -168,14 +233,14 @@ function drawFilledBox( startX, startY, endX, endY, nColour ) | ||||
|     endY = math.floor(endY) | ||||
|  | ||||
|     if nColour then | ||||
|         term.setBackgroundColor( nColour ) | ||||
|         term.setBackgroundColor(nColour) | ||||
|     end | ||||
|     if startX == endX and startY == endY then | ||||
|         drawPixelInternal( startX, startY ) | ||||
|         drawPixelInternal(startX, startY) | ||||
|         return | ||||
|     end | ||||
|  | ||||
|     local minX = math.min( startX, endX ) | ||||
|     local minX = math.min(startX, endX) | ||||
|     local maxX, minY, maxY | ||||
|     if minX == startX then | ||||
|         minY = startY | ||||
| @@ -189,21 +254,26 @@ function drawFilledBox( startX, startY, endX, endY, nColour ) | ||||
|  | ||||
|     for x = minX, maxX do | ||||
|         for y = minY, maxY do | ||||
|             drawPixelInternal( x, y ) | ||||
|             drawPixelInternal(x, y) | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| function drawImage( tImage, xPos, yPos ) | ||||
|     expect(1, tImage, "table") | ||||
| --- Draw an image loaded by @{paintutils.parseImage} or @{paintutils.loadImage}. | ||||
| -- | ||||
| -- @tparam table image The parsed image data. | ||||
| -- @tparam number xPos The x position to start drawing at. | ||||
| -- @tparam number xPos The y position to start drawing at. | ||||
| function drawImage(image, xPos, yPos) | ||||
|     expect(1, image, "table") | ||||
|     expect(2, xPos, "number") | ||||
|     expect(3, yPos, "number") | ||||
|     for y = 1, #tImage do | ||||
|         local tLine = tImage[y] | ||||
|     for y = 1, #image do | ||||
|         local tLine = image[y] | ||||
|         for x = 1, #tLine do | ||||
|             if tLine[x] > 0 then | ||||
|                 term.setBackgroundColor( tLine[x] ) | ||||
|                 drawPixelInternal( x + xPos - 1, y + yPos - 1 ) | ||||
|                 term.setBackgroundColor(tLine[x]) | ||||
|                 drawPixelInternal(x + xPos - 1, y + yPos - 1) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|   | ||||
| @@ -1,11 +1,26 @@ | ||||
| --- Provides a simple implementation of multitasking. | ||||
| -- | ||||
| -- Functions are not actually executed simultaniously, but rather this API will | ||||
| -- automatically switch between them whenever they yield (eg whenever they call | ||||
| -- @{coroutine.yield}, or functions that call that - eg `os.pullEvent` - or | ||||
| -- functions that call that, etc - basically, anything that causes the function | ||||
| -- to "pause"). | ||||
| -- | ||||
| -- Each function executed in "parallel" gets its own copy of the event queue, | ||||
| -- and so "event consuming" functions (again, mostly anything that causes the | ||||
| -- script to pause - eg `sleep`, `rednet.receive`, most of the `turtle` API, | ||||
| -- etc) can safely be used in one without affecting the event queue accessed by | ||||
| -- the other. | ||||
| -- | ||||
| -- @module parallel | ||||
|  | ||||
| local function create( ... ) | ||||
| local function create(...) | ||||
|     local tFns = table.pack(...) | ||||
|     local tCos = {} | ||||
|     for i = 1, tFns.n, 1 do | ||||
|         local fn = tFns[i] | ||||
|         if type( fn ) ~= "function" then | ||||
|             error( "bad argument #" .. i .. " (expected function, got " .. type( fn ) .. ")", 3 ) | ||||
|         if type(fn) ~= "function" then | ||||
|             error("bad argument #" .. i .. " (expected function, got " .. type(fn) .. ")", 3) | ||||
|         end | ||||
|  | ||||
|         tCos[i] = coroutine.create(fn) | ||||
| @@ -14,7 +29,7 @@ local function create( ... ) | ||||
|     return tCos | ||||
| end | ||||
|  | ||||
| local function runUntilLimit( _routines, _limit ) | ||||
| local function runUntilLimit(_routines, _limit) | ||||
|     local count = #_routines | ||||
|     local living = count | ||||
|  | ||||
| @@ -25,13 +40,13 @@ local function runUntilLimit( _routines, _limit ) | ||||
|             local r = _routines[n] | ||||
|             if r then | ||||
|                 if tFilters[r] == nil or tFilters[r] == eventData[1] or eventData[1] == "terminate" then | ||||
|                     local ok, param = coroutine.resume( r, table.unpack( eventData, 1, eventData.n ) ) | ||||
|                     local ok, param = coroutine.resume(r, table.unpack(eventData, 1, eventData.n)) | ||||
|                     if not ok then | ||||
|                         error( param, 0 ) | ||||
|                         error(param, 0) | ||||
|                     else | ||||
|                         tFilters[r] = param | ||||
|                     end | ||||
|                     if coroutine.status( r ) == "dead" then | ||||
|                     if coroutine.status(r) == "dead" then | ||||
|                         _routines[n] = nil | ||||
|                         living = living - 1 | ||||
|                         if living <= _limit then | ||||
| @@ -43,7 +58,7 @@ local function runUntilLimit( _routines, _limit ) | ||||
|         end | ||||
|         for n = 1, count do | ||||
|             local r = _routines[n] | ||||
|             if r and coroutine.status( r ) == "dead" then | ||||
|             if r and coroutine.status(r) == "dead" then | ||||
|                 _routines[n] = nil | ||||
|                 living = living - 1 | ||||
|                 if living <= _limit then | ||||
| @@ -51,16 +66,26 @@ local function runUntilLimit( _routines, _limit ) | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|         eventData = table.pack( os.pullEventRaw() ) | ||||
|         eventData = table.pack(os.pullEventRaw()) | ||||
|     end | ||||
| end | ||||
|  | ||||
| function waitForAny( ... ) | ||||
|     local routines = create( ... ) | ||||
|     return runUntilLimit( routines, #routines - 1 ) | ||||
| --- Switches between execution of the functions, until any of them | ||||
| -- finishes. If any of the functions errors, the message is propagated upwards | ||||
| -- from the @{parallel.waitForAny} call. | ||||
| -- | ||||
| -- @tparam function ... The functions this task will run | ||||
| function waitForAny(...) | ||||
|     local routines = create(...) | ||||
|     return runUntilLimit(routines, #routines - 1) | ||||
| end | ||||
|  | ||||
| function waitForAll( ... ) | ||||
|     local routines = create( ... ) | ||||
|     runUntilLimit( routines, 0 ) | ||||
| --- Switches between execution of the functions, until all of them are | ||||
| -- finished. If any of the functions errors, the message is propagated upwards | ||||
| -- from the @{parallel.waitForAll} call. | ||||
| -- | ||||
| -- @tparam function ... The functions this task will run | ||||
| function waitForAll(...) | ||||
|     local routines = create(...) | ||||
|     return runUntilLimit(routines, 0) | ||||
| end | ||||
|   | ||||
| @@ -1,110 +1,184 @@ | ||||
| --- The Peripheral API is for interacting with peripherals connected to the | ||||
| -- computer, such as the Disk Drive, the Advanced Monitor and Monitor. | ||||
| -- | ||||
| -- Each peripheral block has a name, either referring to the side the peripheral | ||||
| -- can be found on, or a name on an adjacent wired network. | ||||
| -- | ||||
| -- If the peripheral is next to the computer, its side is either `front`, | ||||
| -- `back`, `left`, `right`, `top` or `bottom`. If the peripheral is attached by | ||||
| -- a cable, its side will follow the format `type_id`, for example `printer_0`. | ||||
| -- | ||||
| -- Peripheral functions are called *methods*, a term borrowed from Java. | ||||
| -- | ||||
| -- @module peripheral | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| local native = peripheral | ||||
| local sides = rs.getSides() | ||||
|  | ||||
| --- Provides a list of all peripherals available. | ||||
| -- | ||||
| -- If a device is located directly next to the system, then its name will be | ||||
| -- listed as the side it is attached to. If a device is attached via a Wired | ||||
| -- Modem, then it'll be reported according to its name on the wired network. | ||||
| -- | ||||
| -- @treturn table A list of the names of all attached peripherals. | ||||
| function getNames() | ||||
|     local tResults = {} | ||||
|     for _, sSide in ipairs( rs.getSides() ) do | ||||
|         if native.isPresent( sSide ) then | ||||
|             table.insert( tResults, sSide ) | ||||
|             if native.getType( sSide ) == "modem" and not native.call( sSide, "isWireless" ) then | ||||
|                 local tRemote = native.call( sSide, "getNamesRemote" ) | ||||
|                 for _, sName in ipairs( tRemote ) do | ||||
|                     table.insert( tResults, sName ) | ||||
|     local results = {} | ||||
|     for n = 1, #sides do | ||||
|         local side = sides[n] | ||||
|         if native.isPresent(side) then | ||||
|             table.insert(results, side) | ||||
|             if native.getType(side) == "modem" and not native.call(side, "isWireless") then | ||||
|                 local remote = native.call(side, "getNamesRemote") | ||||
|                 for _, name in ipairs(remote) do | ||||
|                     table.insert(results, name) | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|     return tResults | ||||
|     return results | ||||
| end | ||||
|  | ||||
| function isPresent( _sSide ) | ||||
|     expect(1, _sSide, "string") | ||||
|     if native.isPresent( _sSide ) then | ||||
| --- Determines if a peripheral is present with the given name. | ||||
| -- | ||||
| -- @tparam string name The side or network name that you want to check. | ||||
| -- @treturn boolean If a peripheral is present with the given name. | ||||
| -- @usage peripheral.isPresent("top") | ||||
| -- @usage peripheral.isPresent("monitor_0") | ||||
| function isPresent(name) | ||||
|     expect(1, name, "string") | ||||
|     if native.isPresent(name) then | ||||
|         return true | ||||
|     end | ||||
|     for _, sSide in ipairs( rs.getSides() ) do | ||||
|         if native.getType( sSide ) == "modem" and not native.call( sSide, "isWireless" ) then | ||||
|             if native.call( sSide, "isPresentRemote", _sSide )  then | ||||
|                 return true | ||||
|             end | ||||
|  | ||||
|     for n = 1, #sides do | ||||
|         local side = sides[n] | ||||
|         if native.getType(side) == "modem" and not native.call(side, "isWireless") and | ||||
|             native.call(side, "isPresentRemote", name) | ||||
|         then | ||||
|             return true | ||||
|         end | ||||
|     end | ||||
|     return false | ||||
| end | ||||
|  | ||||
| function getType( _sSide ) | ||||
|     expect(1, _sSide, "string") | ||||
|     if native.isPresent( _sSide ) then | ||||
|         return native.getType( _sSide ) | ||||
| --- Get the type of the peripheral with the given name. | ||||
| -- | ||||
| -- @tparam string name The name of the peripheral to find. | ||||
| -- @treturn string|nil The peripheral's type, or `nil` if it is not present. | ||||
| function getType(name) | ||||
|     expect(1, name, "string") | ||||
|     if native.isPresent(name) then | ||||
|         return native.getType(name) | ||||
|     end | ||||
|     for _, sSide in ipairs( rs.getSides() ) do | ||||
|         if native.getType( sSide ) == "modem" and not native.call( sSide, "isWireless" ) then | ||||
|             if native.call( sSide, "isPresentRemote", _sSide )  then | ||||
|                 return native.call( sSide, "getTypeRemote", _sSide ) | ||||
|             end | ||||
|     for n = 1, #sides do | ||||
|         local side = sides[n] | ||||
|         if native.getType(side) == "modem" and not native.call(side, "isWireless") and | ||||
|             native.call(side, "isPresentRemote", name) | ||||
|         then | ||||
|             return native.call(side, "getTypeRemote", name) | ||||
|         end | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function getMethods( _sSide ) | ||||
|     expect(1, _sSide, "string") | ||||
|     if native.isPresent( _sSide ) then | ||||
|         return native.getMethods( _sSide ) | ||||
| --- Get all available methods for the peripheral with the given name. | ||||
| -- | ||||
| -- @tparam string name The name of the peripheral to find. | ||||
| -- @treturn table|nil A list of methods provided by this peripheral, or `nil` if | ||||
| -- it is not present. | ||||
| function getMethods(name) | ||||
|     expect(1, name, "string") | ||||
|     if native.isPresent(name) then | ||||
|         return native.getMethods(name) | ||||
|     end | ||||
|     for _, sSide in ipairs( rs.getSides() ) do | ||||
|         if native.getType( sSide ) == "modem" and not native.call( sSide, "isWireless" ) then | ||||
|             if native.call( sSide, "isPresentRemote", _sSide )  then | ||||
|                 return native.call( sSide, "getMethodsRemote", _sSide ) | ||||
|             end | ||||
|     for n = 1, #sides do | ||||
|         local side = sides[n] | ||||
|         if native.getType(side) == "modem" and not native.call(side, "isWireless") and | ||||
|             native.call(side, "isPresentRemote", name) | ||||
|         then | ||||
|             return native.call(side, "getMethodsRemote", name) | ||||
|         end | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function call( _sSide, _sMethod, ... ) | ||||
|     expect(1, _sSide, "string") | ||||
|     expect(2, _sMethod, "string") | ||||
|     if native.isPresent( _sSide ) then | ||||
|         return native.call( _sSide, _sMethod, ... ) | ||||
| --- Call a method on the peripheral with the given name. | ||||
| -- | ||||
| -- @tparam string name The name of the peripheral to invoke the method on. | ||||
| -- @tparam string method The name of the method | ||||
| -- @param ... Additional arguments to pass to the method | ||||
| -- @return The return values of the peripheral method. | ||||
| -- | ||||
| -- @usage Open the modem on the top of this computer. | ||||
| -- | ||||
| --     peripheral.call("top", "open", 1) | ||||
| function call(name, method, ...) | ||||
|     expect(1, name, "string") | ||||
|     expect(2, method, "string") | ||||
|     if native.isPresent(name) then | ||||
|         return native.call(name, method, ...) | ||||
|     end | ||||
|     for _, sSide in ipairs( rs.getSides() ) do | ||||
|         if native.getType( sSide ) == "modem" and not native.call( sSide, "isWireless" ) then | ||||
|             if native.call( sSide, "isPresentRemote", _sSide )  then | ||||
|                 return native.call( sSide, "callRemote", _sSide, _sMethod, ... ) | ||||
|             end | ||||
|  | ||||
|     for n = 1, #sides do | ||||
|         local side = sides[n] | ||||
|         if native.getType(side) == "modem" and not native.call(side, "isWireless") and | ||||
|             native.call(side, "isPresentRemote", name) | ||||
|         then | ||||
|             return native.call(side, "callRemote", name, method, ...) | ||||
|         end | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function wrap( _sSide ) | ||||
|     expect(1, _sSide, "string") | ||||
|     if peripheral.isPresent( _sSide ) then | ||||
|         local tMethods = peripheral.getMethods( _sSide ) | ||||
|         local tResult = {} | ||||
|         for _, sMethod in ipairs( tMethods ) do | ||||
|             tResult[sMethod] = function( ... ) | ||||
|                 return peripheral.call( _sSide, sMethod, ... ) | ||||
|             end | ||||
|         end | ||||
|         return tResult | ||||
| --- Get a table containing functions pointing to the peripheral's methods, which | ||||
| -- can then be called as if using @{peripheral.call}. | ||||
| -- | ||||
| -- @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) | ||||
| function wrap(name) | ||||
|     expect(1, name, "string") | ||||
|  | ||||
|     local methods = peripheral.getMethods(name) | ||||
|     if not methods then | ||||
|         return nil | ||||
|     end | ||||
|     return nil | ||||
|  | ||||
|     local result = {} | ||||
|     for _, method in ipairs(methods) do | ||||
|         result[method] = function(...) | ||||
|             return peripheral.call(name, method, ...) | ||||
|         end | ||||
|     end | ||||
|     return result | ||||
| end | ||||
|  | ||||
| function find( sType, fnFilter ) | ||||
|     expect(1, sType, "string") | ||||
|     expect(2, fnFilter, "function", "nil") | ||||
|     local tResults = {} | ||||
|     for _, sName in ipairs( peripheral.getNames() ) do | ||||
|         if peripheral.getType( sName ) == sType then | ||||
|             local wrapped = peripheral.wrap( sName ) | ||||
|             if fnFilter == nil or fnFilter( sName, wrapped ) then | ||||
|                 table.insert( tResults, wrapped ) | ||||
| --- 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 local monitors = { peripheral.find("monitor") } | ||||
| -- @usage peripheral.find("modem", rednet.open) | ||||
| function find(ty, filter) | ||||
|     expect(1, ty, "string") | ||||
|     expect(2, filter, "function", "nil") | ||||
|  | ||||
|     local results = {} | ||||
|     for _, name in ipairs(peripheral.getNames()) do | ||||
|         if peripheral.getType(name) == ty then | ||||
|             local wrapped = peripheral.wrap(name) | ||||
|             if filter == nil or filter(name, wrapped) then | ||||
|                 table.insert(results, wrapped) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|     return table.unpack( tResults ) | ||||
|     return table.unpack(results) | ||||
| end | ||||
|   | ||||
| @@ -1,51 +1,92 @@ | ||||
| --- The Rednet API allows systems to communicate between each other without | ||||
| -- using redstone. It serves as a wrapper for the modem API, offering ease of | ||||
| -- functionality (particularly in regards to repeating signals) with some | ||||
| -- expense of fine control. | ||||
| -- | ||||
| -- In order to send and receive data, a modem (either wired, wireless, or ender) | ||||
| -- is required. The data reaches any possible destinations immediately after | ||||
| -- sending it, but is range limited. | ||||
| -- | ||||
| -- Rednet also allows you to use a "protocol" - simple string names indicating | ||||
| -- what messages are about. Receiving systems may filter messages according to | ||||
| -- their protocols, thereby automatically ignoring incoming messages which don't | ||||
| -- specify an identical string. It's also possible to @{rednet.lookup|lookup} | ||||
| -- which systems in the area use certain protocols, hence making it easier to | ||||
| -- determine where given messages should be sent in the first place. | ||||
| -- | ||||
| -- @module rednet | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| --- The channel used by the Rednet API to @{broadcast} messages. | ||||
| CHANNEL_BROADCAST = 65535 | ||||
|  | ||||
| --- The channel used by the Rednet API to repeat messages. | ||||
| CHANNEL_REPEAT = 65533 | ||||
|  | ||||
| local tReceivedMessages = {} | ||||
| local tReceivedMessageTimeouts = {} | ||||
| local tHostnames = {} | ||||
|  | ||||
| function open( sModem ) | ||||
|     expect(1, sModem, "string") | ||||
|     if peripheral.getType( sModem ) ~= "modem" then | ||||
|         error( "No such modem: " .. sModem, 2 ) | ||||
| --- Opens a modem with the given @{peripheral} name, allowing it to send and | ||||
| --- receive messages over rednet. | ||||
| -- | ||||
| -- This will open the modem on two channels: one which has the same | ||||
| -- @{os.getComputerID|ID} as the computer, and another on | ||||
| -- @{CHANNEL_BROADCAST|the broadcast channel}. | ||||
| -- | ||||
| -- @tparam string modem The name of the modem to open. | ||||
| -- @throws If there is no such modem with the given name | ||||
| function open(modem) | ||||
|     expect(1, modem, "string") | ||||
|     if peripheral.getType(modem) ~= "modem" then | ||||
|         error("No such modem: " .. modem, 2) | ||||
|     end | ||||
|     peripheral.call( sModem, "open", os.getComputerID() ) | ||||
|     peripheral.call( sModem, "open", CHANNEL_BROADCAST ) | ||||
|     peripheral.call(modem, "open", os.getComputerID()) | ||||
|     peripheral.call(modem, "open", CHANNEL_BROADCAST) | ||||
| end | ||||
|  | ||||
| function close( sModem ) | ||||
|     expect(1, sModem, "string", "nil") | ||||
|     if sModem then | ||||
| --- Close a modem with the given @{peripheral} name, meaning it can no longer | ||||
| -- send and receive rednet messages. | ||||
| -- | ||||
| -- @tparam[opt] string modem The side the modem exists on. If not given, all | ||||
| -- open modems will be closed. | ||||
| -- @throws If there is no such modem with the given name | ||||
| function close(modem) | ||||
|     expect(1, modem, "string", "nil") | ||||
|     if modem then | ||||
|         -- Close a specific modem | ||||
|         if peripheral.getType( sModem ) ~= "modem" then | ||||
|             error( "No such modem: " .. sModem, 2 ) | ||||
|         if peripheral.getType(modem) ~= "modem" then | ||||
|             error("No such modem: " .. modem, 2) | ||||
|         end | ||||
|         peripheral.call( sModem, "close", os.getComputerID() ) | ||||
|         peripheral.call( sModem, "close", CHANNEL_BROADCAST ) | ||||
|         peripheral.call(modem, "close", os.getComputerID()) | ||||
|         peripheral.call(modem, "close", CHANNEL_BROADCAST) | ||||
|     else | ||||
|         -- Close all modems | ||||
|         for _, sModem in ipairs( peripheral.getNames() ) do | ||||
|             if isOpen( sModem ) then | ||||
|                 close( sModem ) | ||||
|         for _, modem in ipairs(peripheral.getNames()) do | ||||
|             if isOpen(modem) then | ||||
|                 close(modem) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| function isOpen( sModem ) | ||||
|     expect(1, sModem, "string", "nil") | ||||
|     if sModem then | ||||
| --- Determine if rednet is currently open. | ||||
| -- | ||||
| -- @tparam[opt] string modem Which modem to check. If not given, all connected | ||||
| -- modems will be checked. | ||||
| -- @treturn boolean If the given modem is open. | ||||
| function isOpen(modem) | ||||
|     expect(1, modem, "string", "nil") | ||||
|     if modem then | ||||
|         -- Check if a specific modem is open | ||||
|         if peripheral.getType( sModem ) == "modem" then | ||||
|             return peripheral.call( sModem, "isOpen", os.getComputerID() ) and peripheral.call( sModem, "isOpen", CHANNEL_BROADCAST ) | ||||
|         if peripheral.getType(modem) == "modem" then | ||||
|             return peripheral.call(modem, "isOpen", os.getComputerID()) and peripheral.call(modem, "isOpen", CHANNEL_BROADCAST) | ||||
|         end | ||||
|     else | ||||
|         -- Check if any modem is open | ||||
|         for _, sModem in ipairs( peripheral.getNames() ) do | ||||
|             if isOpen( sModem ) then | ||||
|         for _, modem in ipairs(peripheral.getNames()) do | ||||
|             if isOpen(modem) then | ||||
|                 return true | ||||
|             end | ||||
|         end | ||||
| @@ -53,15 +94,32 @@ function isOpen( sModem ) | ||||
|     return false | ||||
| end | ||||
|  | ||||
| function send( nRecipient, message, sProtocol ) | ||||
| --- Allows a computer or turtle with an attached modem to send a message | ||||
| -- intended for a system with a specific ID. At least one such modem must first | ||||
| -- be @{rednet.open|opened} before sending is possible. | ||||
| -- | ||||
| -- Assuming the target was in range and also had a correctly opened modem, it | ||||
| -- may then use @{rednet.receive} to collect the message. | ||||
| -- | ||||
| -- @tparam number nRecipient The ID of the receiving computer. | ||||
| -- @param message The message to send. This should not contain coroutines or | ||||
| -- functions, as they will be converted to @{nil}. | ||||
| -- @tparam[opt] string sProtocol The "protocol" to send this message under. When | ||||
| -- using @{rednet.receive} one can filter to only receive messages sent under a | ||||
| -- particular protocol. | ||||
| -- @treturn boolean If this message was successfully sent (i.e. if rednet is | ||||
| -- currently @{rednet.open|open}). Note, this does not guarantee the message was | ||||
| -- actually _received_. | ||||
| -- @see rednet.receive | ||||
| function send(nRecipient, message, sProtocol) | ||||
|     expect(1, nRecipient, "number") | ||||
|     expect(3, sProtocol, "string", "nil") | ||||
|     -- Generate a (probably) unique message ID | ||||
|     -- We could do other things to guarantee uniqueness, but we really don't need to | ||||
|     -- Store it to ensure we don't get our own messages back | ||||
|     local nMessageID = math.random( 1, 2147483647 ) | ||||
|     tReceivedMessages[ nMessageID ] = true | ||||
|     tReceivedMessageTimeouts[ os.startTimer( 30 ) ] = nMessageID | ||||
|     local nMessageID = math.random(1, 2147483647) | ||||
|     tReceivedMessages[nMessageID] = true | ||||
|     tReceivedMessageTimeouts[os.startTimer(30)] = nMessageID | ||||
|  | ||||
|     -- Create the message | ||||
|     local nReplyChannel = os.getComputerID() | ||||
| @@ -75,14 +133,14 @@ function send( nRecipient, message, sProtocol ) | ||||
|     local sent = false | ||||
|     if nRecipient == os.getComputerID() then | ||||
|         -- Loopback to ourselves | ||||
|         os.queueEvent( "rednet_message", nReplyChannel, message, sProtocol ) | ||||
|         os.queueEvent("rednet_message", nReplyChannel, message, sProtocol) | ||||
|         sent = true | ||||
|     else | ||||
|         -- Send on all open modems, to the target and to repeaters | ||||
|         for _, sModem in ipairs( peripheral.getNames() ) do | ||||
|             if isOpen( sModem ) then | ||||
|                 peripheral.call( sModem, "transmit", nRecipient, nReplyChannel, tMessage ) | ||||
|                 peripheral.call( sModem, "transmit", CHANNEL_REPEAT, nReplyChannel, tMessage ) | ||||
|         for _, sModem in ipairs(peripheral.getNames()) do | ||||
|             if isOpen(sModem) then | ||||
|                 peripheral.call(sModem, "transmit", nRecipient, nReplyChannel, tMessage) | ||||
|                 peripheral.call(sModem, "transmit", CHANNEL_REPEAT, nReplyChannel, tMessage) | ||||
|                 sent = true | ||||
|             end | ||||
|         end | ||||
| @@ -91,12 +149,35 @@ function send( nRecipient, message, sProtocol ) | ||||
|     return sent | ||||
| end | ||||
|  | ||||
| function broadcast( message, sProtocol ) | ||||
| --- Broadcasts a string message over the predefined @{CHANNEL_BROADCAST} | ||||
| -- channel. The message will be received by every device listening to rednet. | ||||
| -- | ||||
| -- @param message The message to send. This should not contain coroutines or | ||||
| -- functions, as they will be converted to @{nil}. | ||||
| -- @tparam[opt] string sProtocol The "protocol" to send this message under. When | ||||
| -- using @{rednet.receive} one can filter to only receive messages sent under a | ||||
| -- particular protocol. | ||||
| -- @see rednet.receive | ||||
| function broadcast(message, sProtocol) | ||||
|     expect(2, sProtocol, "string", "nil") | ||||
|     send( CHANNEL_BROADCAST, message, sProtocol ) | ||||
|     send(CHANNEL_BROADCAST, message, sProtocol) | ||||
| end | ||||
|  | ||||
| function receive( sProtocolFilter, nTimeout ) | ||||
| --- Wait for a rednet message to be received, or until `nTimeout` seconds have | ||||
| -- elapsed. | ||||
| -- | ||||
| -- @tparam[opt] string sProtocolFilter The protocol the received message must be | ||||
| -- sent with. If specified, any messages not sent under this protocol will be | ||||
| -- discarded. | ||||
| -- @tparam[opt] number nTimeout The number of seconds to wait if no message is | ||||
| -- received. | ||||
| -- @treturn[1] number The computer which sent this message | ||||
| -- @return[1] The received message | ||||
| -- @treturn[1] string|nil The protocol this message was sent under. | ||||
| -- @treturn[2] nil If the timeout elapsed and no message was received. | ||||
| -- @see rednet.broadcast | ||||
| -- @see rednet.send | ||||
| function receive(sProtocolFilter, nTimeout) | ||||
|     -- The parameters used to be ( nTimeout ), detect this case for backwards compatibility | ||||
|     if type(sProtocolFilter) == "number" and nTimeout == nil then | ||||
|         sProtocolFilter, nTimeout = nil, sProtocolFilter | ||||
| @@ -108,7 +189,7 @@ function receive( sProtocolFilter, nTimeout ) | ||||
|     local timer = nil | ||||
|     local sFilter = nil | ||||
|     if nTimeout then | ||||
|         timer = os.startTimer( nTimeout ) | ||||
|         timer = os.startTimer(nTimeout) | ||||
|         sFilter = nil | ||||
|     else | ||||
|         sFilter = "rednet_message" | ||||
| @@ -116,7 +197,7 @@ function receive( sProtocolFilter, nTimeout ) | ||||
|  | ||||
|     -- Wait for events | ||||
|     while true do | ||||
|         local sEvent, p1, p2, p3 = os.pullEvent( sFilter ) | ||||
|         local sEvent, p1, p2, p3 = os.pullEvent(sFilter) | ||||
|         if sEvent == "rednet_message" then | ||||
|             -- Return the first matching rednet_message | ||||
|             local nSenderID, message, sProtocol = p1, p2, p3 | ||||
| @@ -132,26 +213,62 @@ function receive( sProtocolFilter, nTimeout ) | ||||
|     end | ||||
| end | ||||
|  | ||||
| function host( sProtocol, sHostname ) | ||||
| --- Register the system as "hosting" the desired protocol under the specified | ||||
| -- name. If a rednet @{rednet.lookup|lookup} is performed for that protocol (and | ||||
| -- maybe name) on the same network, the registered system will automatically | ||||
| -- respond via a background process, hence providing the system performing the | ||||
| -- lookup with its ID number. | ||||
| -- | ||||
| -- Multiple computers may not register themselves on the same network as having | ||||
| -- the same names against the same protocols, and the title `localhost` is | ||||
| -- specifically reserved. They may, however, share names as long as their hosted | ||||
| -- protocols are different, or if they only join a given network after | ||||
| -- "registering" themselves before doing so (eg while offline or part of a | ||||
| -- different network). | ||||
| -- | ||||
| -- @tparam string sProtocol The protocol this computer provides. | ||||
| -- @tparam string sHostname The name this protocol exposes for the given protocol. | ||||
| -- @throws If trying to register a hostname which is reserved, or currently in use. | ||||
| -- @see rednet.unhost | ||||
| -- @see rednet.lookup | ||||
| function host(sProtocol, sHostname) | ||||
|     expect(1, sProtocol, "string") | ||||
|     expect(2, sHostname, "string") | ||||
|     if sHostname == "localhost" then | ||||
|         error( "Reserved hostname", 2 ) | ||||
|         error("Reserved hostname", 2) | ||||
|     end | ||||
|     if tHostnames[ sProtocol ] ~= sHostname then | ||||
|         if lookup( sProtocol, sHostname ) ~= nil then | ||||
|             error( "Hostname in use", 2 ) | ||||
|     if tHostnames[sProtocol] ~= sHostname then | ||||
|         if lookup(sProtocol, sHostname) ~= nil then | ||||
|             error("Hostname in use", 2) | ||||
|         end | ||||
|         tHostnames[ sProtocol ] = sHostname | ||||
|         tHostnames[sProtocol] = sHostname | ||||
|     end | ||||
| end | ||||
|  | ||||
| function unhost( sProtocol ) | ||||
| --- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer | ||||
| --- respond to @{rednet.lookup} requests. | ||||
| -- | ||||
| -- @tparam string sProtocol The protocol to unregister your self from. | ||||
| function unhost(sProtocol) | ||||
|     expect(1, sProtocol, "string") | ||||
|     tHostnames[ sProtocol ] = nil | ||||
|     tHostnames[sProtocol] = nil | ||||
| end | ||||
|  | ||||
| function lookup( sProtocol, sHostname ) | ||||
| --- Search the local rednet network for systems @{rednet.host|hosting} the | ||||
| -- desired protocol and returns any computer IDs that respond as "registered" | ||||
| -- against it. | ||||
| -- | ||||
| -- If a hostname is specified, only one ID will be returned (assuming an exact | ||||
| -- match is found). | ||||
| -- | ||||
| -- @tparam string sProtocol The protocol to search for. | ||||
| -- @tparam[opt] string sHostname The hostname to search for. | ||||
| -- | ||||
| -- @treturn[1] { number }|nil A list of computer IDs hosting the given | ||||
| -- protocol, or @{nil} if none exist. | ||||
| -- @treturn[2] number|nil The computer ID with the provided hostname and protocol, | ||||
| -- or @{nil} if none exists. | ||||
| function lookup(sProtocol, sHostname) | ||||
|     expect(1, sProtocol, "string") | ||||
|     expect(2, sHostname, "string", "nil") | ||||
|  | ||||
| @@ -162,30 +279,30 @@ function lookup( sProtocol, sHostname ) | ||||
|     end | ||||
|  | ||||
|     -- Check localhost first | ||||
|     if tHostnames[ sProtocol ] then | ||||
|     if tHostnames[sProtocol] then | ||||
|         if sHostname == nil then | ||||
|             table.insert( tResults, os.getComputerID() ) | ||||
|         elseif sHostname == "localhost" or sHostname == tHostnames[ sProtocol ] then | ||||
|             table.insert(tResults, os.getComputerID()) | ||||
|         elseif sHostname == "localhost" or sHostname == tHostnames[sProtocol] then | ||||
|             return os.getComputerID() | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     if not isOpen() then | ||||
|         if tResults then | ||||
|             return table.unpack( tResults ) | ||||
|             return table.unpack(tResults) | ||||
|         end | ||||
|         return nil | ||||
|     end | ||||
|  | ||||
|     -- Broadcast a lookup packet | ||||
|     broadcast( { | ||||
|     broadcast({ | ||||
|         sType = "lookup", | ||||
|         sProtocol = sProtocol, | ||||
|         sHostname = sHostname, | ||||
|     }, "dns" ) | ||||
|     }, "dns") | ||||
|  | ||||
|     -- Start a timer | ||||
|     local timer = os.startTimer( 2 ) | ||||
|     local timer = os.startTimer(2) | ||||
|  | ||||
|     -- Wait for events | ||||
|     while true do | ||||
| @@ -196,7 +313,7 @@ function lookup( sProtocol, sHostname ) | ||||
|             if sMessageProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup response" then | ||||
|                 if tMessage.sProtocol == sProtocol then | ||||
|                     if sHostname == nil then | ||||
|                         table.insert( tResults, nSenderID ) | ||||
|                         table.insert(tResults, nSenderID) | ||||
|                     elseif tMessage.sHostname == sHostname then | ||||
|                         return nSenderID | ||||
|                     end | ||||
| @@ -210,15 +327,17 @@ function lookup( sProtocol, sHostname ) | ||||
|         end | ||||
|     end | ||||
|     if tResults then | ||||
|         return table.unpack( tResults ) | ||||
|         return table.unpack(tResults) | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| local bRunning = false | ||||
|  | ||||
| --- @local | ||||
| function run() | ||||
|     if bRunning then | ||||
|         error( "rednet is already running", 2 ) | ||||
|         error("rednet is already running", 2) | ||||
|     end | ||||
|     bRunning = true | ||||
|  | ||||
| @@ -227,12 +346,12 @@ function run() | ||||
|         if sEvent == "modem_message" then | ||||
|             -- Got a modem message, process it and add it to the rednet event queue | ||||
|             local sModem, nChannel, nReplyChannel, tMessage = p1, p2, p3, p4 | ||||
|             if isOpen( sModem ) and ( nChannel == os.getComputerID() or nChannel == CHANNEL_BROADCAST ) then | ||||
|                 if type( tMessage ) == "table" and tMessage.nMessageID then | ||||
|                     if not tReceivedMessages[ tMessage.nMessageID ] then | ||||
|                         tReceivedMessages[ tMessage.nMessageID ] = true | ||||
|                         tReceivedMessageTimeouts[ os.startTimer( 30 ) ] = tMessage.nMessageID | ||||
|                         os.queueEvent( "rednet_message", nReplyChannel, tMessage.message, tMessage.sProtocol ) | ||||
|             if isOpen(sModem) and (nChannel == os.getComputerID() or nChannel == CHANNEL_BROADCAST) then | ||||
|                 if type(tMessage) == "table" and tMessage.nMessageID then | ||||
|                     if not tReceivedMessages[tMessage.nMessageID] then | ||||
|                         tReceivedMessages[tMessage.nMessageID] = true | ||||
|                         tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID | ||||
|                         os.queueEvent("rednet_message", nReplyChannel, tMessage.message, tMessage.sProtocol) | ||||
|                     end | ||||
|                 end | ||||
|             end | ||||
| @@ -241,23 +360,23 @@ function run() | ||||
|             -- Got a rednet message (queued from above), respond to dns lookup | ||||
|             local nSenderID, tMessage, sProtocol = p1, p2, p3 | ||||
|             if sProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup" then | ||||
|                 local sHostname = tHostnames[ tMessage.sProtocol ] | ||||
|                 local sHostname = tHostnames[tMessage.sProtocol] | ||||
|                 if sHostname ~= nil and (tMessage.sHostname == nil or tMessage.sHostname == sHostname) then | ||||
|                     rednet.send( nSenderID, { | ||||
|                     rednet.send(nSenderID, { | ||||
|                         sType = "lookup response", | ||||
|                         sHostname = sHostname, | ||||
|                         sProtocol = tMessage.sProtocol, | ||||
|                     }, "dns" ) | ||||
|                     }, "dns") | ||||
|                 end | ||||
|             end | ||||
|  | ||||
|         elseif sEvent == "timer" then | ||||
|             -- Got a timer event, use it to clear the event queue | ||||
|             local nTimer = p1 | ||||
|             local nMessage = tReceivedMessageTimeouts[ nTimer ] | ||||
|             local nMessage = tReceivedMessageTimeouts[nTimer] | ||||
|             if nMessage then | ||||
|                 tReceivedMessageTimeouts[ nTimer ] = nil | ||||
|                 tReceivedMessages[ nMessage ] = nil | ||||
|                 tReceivedMessageTimeouts[nTimer] = nil | ||||
|                 tReceivedMessages[nMessage] = nil | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|   | ||||
| @@ -1,62 +1,197 @@ | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
| --- The settings API allows to store values and save them to a file for | ||||
| -- persistent configurations for CraftOS and your programs. | ||||
| -- | ||||
| -- By default, the settings API will load its configuration from the | ||||
| -- `/.settings` file. One can then use @{settings.save} to update the file. | ||||
| -- | ||||
| -- @module settings | ||||
|  | ||||
| local tSettings = {} | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua") | ||||
| local type, expect, field = type, expect.expect, expect.field | ||||
|  | ||||
| function set( sName, value ) | ||||
|     expect(1, sName, "string") | ||||
| local details, values = {}, {} | ||||
|  | ||||
| local function reserialize(value) | ||||
|     if type(value) ~= "table" then return value end | ||||
|     return textutils.unserialize(textutils.serialize(value)) | ||||
| end | ||||
|  | ||||
| local function copy(value) | ||||
|     if type(value) ~= "table" then return value end | ||||
|     local result = {} | ||||
|     for k, v in pairs(value) do result[k] = copy(v) end | ||||
|     return result | ||||
| end | ||||
|  | ||||
| local valid_types = { "number", "string", "boolean", "table" } | ||||
| for _, v in ipairs(valid_types) do valid_types[v] = true end | ||||
|  | ||||
| --- Define a new setting, optional specifying various properties about it. | ||||
| -- | ||||
| -- While settings do not have to be added before being used, doing so allows | ||||
| -- you to provide defaults and additional metadata. | ||||
| -- | ||||
| -- @tparam string name The name of this option | ||||
| -- @tparam[opt] { description? = string, default? = value, type? = string } options | ||||
| -- Options for this setting. This table accepts the following fields: | ||||
| -- | ||||
| --  - `description`: A description which may be printed when running the `set` program. | ||||
| --  - `default`: A default value, which is returned by @{settings.get} if the | ||||
| --    setting has not been changed. | ||||
| --  - `type`: Require values to be of this type. @{set|Setting} the value to another type | ||||
| --    will error. | ||||
| function define(name, options) | ||||
|     expect(1, name, "string") | ||||
|     expect(2, options, "table", nil) | ||||
|  | ||||
|     if options then | ||||
|         options = { | ||||
|             description = field(options, "description", "string", "nil"), | ||||
|             default = reserialize(field(options, "default", "number", "string", "boolean", "table", "nil")), | ||||
|             type = field(options, "type", "string", "nil"), | ||||
|         } | ||||
|  | ||||
|         if options.type and not valid_types[options.type] then | ||||
|             error(("Unknown type %q. Expected one of %s."):format(options.type, table.concat(valid_types, ", ")), 2) | ||||
|         end | ||||
|     else | ||||
|         options = {} | ||||
|     end | ||||
|  | ||||
|     details[name] = options | ||||
| end | ||||
|  | ||||
| --- Remove a @{define|definition} of a setting. | ||||
| -- | ||||
| -- If a setting has been changed, this does not remove its value. Use @{settings.unset} | ||||
| -- for that. | ||||
| -- | ||||
| -- @tparam string name The name of this option | ||||
| function undefine(name) | ||||
|     expect(1, name, "string") | ||||
|     details[name] = nil | ||||
| end | ||||
|  | ||||
| local function set_value(name, value) | ||||
|     local new = reserialize(value) | ||||
|     local old = values[name] | ||||
|     if old == nil then | ||||
|         local opt = details[name] | ||||
|         old = opt and opt.default | ||||
|     end | ||||
|  | ||||
|     values[name] = new | ||||
|     if old ~= new then | ||||
|         -- This should be safe, as os.queueEvent copies values anyway. | ||||
|         os.queueEvent("setting_changed", name, new, old) | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Set the value of a setting. | ||||
| -- | ||||
| -- @tparam string name The name of the setting to set | ||||
| -- @param value The setting's value. This cannot be `nil`, and must be | ||||
| -- serialisable by @{textutils.serialize}. | ||||
| -- @throws If this value cannot be serialised | ||||
| -- @see settings.unset | ||||
| function set(name, value) | ||||
|     expect(1, name, "string") | ||||
|     expect(2, value, "number", "string", "boolean", "table") | ||||
|  | ||||
|     if type(value) == "table" then | ||||
|         -- Ensure value is serializeable | ||||
|         value = textutils.unserialize( textutils.serialize(value) ) | ||||
|     end | ||||
|     tSettings[ sName ] = value | ||||
|     local opt = details[name] | ||||
|     if opt and opt.type then expect(2, value, opt.type) end | ||||
|  | ||||
|     set_value(name, value) | ||||
| end | ||||
|  | ||||
| local copy | ||||
| function copy( value ) | ||||
|     if type(value) == "table" then | ||||
|         local result = {} | ||||
|         for k, v in pairs(value) do | ||||
|             result[k] = copy(v) | ||||
|         end | ||||
|         return result | ||||
|     else | ||||
|         return value | ||||
|     end | ||||
| end | ||||
|  | ||||
| function get( sName, default ) | ||||
|     expect(1, sName, "string") | ||||
|     local result = tSettings[ sName ] | ||||
| --- Get the value of a setting. | ||||
| -- | ||||
| -- @tparam string name The name of the setting to get. | ||||
| -- @param[opt] default The value to use should there be pre-existing value for | ||||
| -- this setting. If not given, it will use the setting's default value if given, | ||||
| -- or `nil` otherwise. | ||||
| -- @return The setting's, or the default if the setting has not been changed. | ||||
| function get(name, default) | ||||
|     expect(1, name, "string") | ||||
|     local result = values[name] | ||||
|     if result ~= nil then | ||||
|         return copy(result) | ||||
|     else | ||||
|     elseif default ~= nil then | ||||
|         return default | ||||
|     else | ||||
|         local opt = details[name] | ||||
|         return opt and copy(opt.default) | ||||
|     end | ||||
| end | ||||
|  | ||||
| function unset( sName ) | ||||
|     expect(1, sName, "string") | ||||
|     tSettings[ sName ] = nil | ||||
| --- Get details about a specific setting. | ||||
| -- | ||||
| -- @tparam string name The name of the setting to get. | ||||
| -- @treturn { description? = string, default? = value, type? = string, value? = value } | ||||
| -- Information about this setting. This includes all information from @{settings.define}, | ||||
| -- as well as this setting's value. | ||||
| function getDetails(name) | ||||
|     expect(1, name, "string") | ||||
|     local deets = copy(details[name]) or {} | ||||
|     deets.value = values[name] | ||||
|     deets.changed = deets.value ~= nil | ||||
|     if deets.value == nil then deets.value = deets.default end | ||||
|     return deets | ||||
| end | ||||
|  | ||||
| --- Remove the value of a setting, setting it to the default. | ||||
| -- | ||||
| -- @{settings.get} will return the default value until the setting's value is | ||||
| -- @{settings.set|set}, or the computer is rebooted. | ||||
| -- | ||||
| -- @tparam string name The name of the setting to unset. | ||||
| -- @see settings.set | ||||
| -- @see settings.clear | ||||
| function unset(name) | ||||
|     expect(1, name, "string") | ||||
|     set_value(name, nil) | ||||
| end | ||||
|  | ||||
| --- Resets the value of all settings. Equivalent to calling @{settings.unset} | ||||
| --- on every setting. | ||||
| -- | ||||
| -- @see settings.unset | ||||
| function clear() | ||||
|     tSettings = {} | ||||
|     for name in pairs(values) do | ||||
|         set_value(name, nil) | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Get the names of all currently defined settings. | ||||
| -- | ||||
| -- @treturn { string } An alphabetically sorted list of all currently-defined | ||||
| -- settings. | ||||
| function getNames() | ||||
|     local result = {} | ||||
|     for k in pairs( tSettings ) do | ||||
|         result[ #result + 1 ] = k | ||||
|     local result, n = {}, 1 | ||||
|     for k in pairs(details) do | ||||
|         result[n], n = k, n + 1 | ||||
|     end | ||||
|     for k in pairs(values) do | ||||
|         if not details[k] then result[n], n = k, n + 1 end | ||||
|     end | ||||
|     table.sort(result) | ||||
|     return result | ||||
| end | ||||
|  | ||||
| function load( sPath ) | ||||
|     expect(1, sPath, "string") | ||||
|     local file = fs.open( sPath, "r" ) | ||||
| --- Load settings from the given file. | ||||
| -- | ||||
| -- Existing settings will be merged with any pre-existing ones. Conflicting | ||||
| -- entries will be overwritten, but any others will be preserved. | ||||
| -- | ||||
| -- @tparam[opt] string sPath The file to load from, defaulting to `.settings`. | ||||
| -- @treturn boolean Whether settings were successfully read from this | ||||
| -- file. Reasons for failure may include the file not existing or being | ||||
| -- corrupted. | ||||
| -- | ||||
| -- @see settings.save | ||||
| function load(sPath) | ||||
|     expect(1, sPath, "string", "nil") | ||||
|     local file = fs.open(sPath or ".settings", "r") | ||||
|     if not file then | ||||
|         return false | ||||
|     end | ||||
| @@ -64,29 +199,41 @@ function load( sPath ) | ||||
|     local sText = file.readAll() | ||||
|     file.close() | ||||
|  | ||||
|     local tFile = textutils.unserialize( sText ) | ||||
|     local tFile = textutils.unserialize(sText) | ||||
|     if type(tFile) ~= "table" then | ||||
|         return false | ||||
|     end | ||||
|  | ||||
|     for k, v in pairs(tFile) do | ||||
|         if type(k) == "string" and | ||||
|            (type(v) == "string" or type(v) == "number" or type(v) == "boolean" or type(v) == "table") then | ||||
|             set( k, v ) | ||||
|         local ty_v = type(k) | ||||
|         if type(k) == "string" and (ty_v == "string" or ty_v == "number" or ty_v == "boolean" or ty_v == "table") then | ||||
|             local opt = details[name] | ||||
|             if not opt or not opt.type or ty_v == opt.type then | ||||
|                 set_value(k, v) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     return true | ||||
| end | ||||
|  | ||||
| function save( sPath ) | ||||
|     expect(1, sPath, "string") | ||||
|     local file = fs.open( sPath, "w" ) | ||||
| --- Save settings to the given file. | ||||
| -- | ||||
| -- This will entirely overwrite the pre-existing file. Settings defined in the | ||||
| -- file, but not currently loaded will be removed. | ||||
| -- | ||||
| -- @tparam[opt] string sPath The path to save settings to, defaulting to `.settings`. | ||||
| -- @treturn boolean If the settings were successfully saved. | ||||
| -- | ||||
| -- @see settings.load | ||||
| function save(sPath) | ||||
|     expect(1, sPath, "string", "nil") | ||||
|     local file = fs.open(sPath or ".settings", "w") | ||||
|     if not file then | ||||
|         return false | ||||
|     end | ||||
|  | ||||
|     file.write( textutils.serialize( tSettings ) ) | ||||
|     file.write(textutils.serialize(values)) | ||||
|     file.close() | ||||
|  | ||||
|     return true | ||||
|   | ||||
| @@ -1,26 +1,49 @@ | ||||
| --- The Terminal API provides functions for writing text to the terminal and | ||||
| -- monitors, and drawing ASCII graphics. | ||||
| -- | ||||
| -- @module term | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| local native = term.native and term.native() or term | ||||
| local redirectTarget = native | ||||
|  | ||||
| local function wrap( _sFunction ) | ||||
|     return function( ... ) | ||||
|         return redirectTarget[ _sFunction ]( ... ) | ||||
| local function wrap(_sFunction) | ||||
|     return function(...) | ||||
|         return redirectTarget[_sFunction](...) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local term = {} | ||||
| local term = _ENV | ||||
|  | ||||
| term.redirect = function( target ) | ||||
| --- Redirects terminal output to a monitor, a @{window}, or any other custom | ||||
| -- terminal object. Once the redirect is performed, any calls to a "term" | ||||
| -- function - or to a function that makes use of a term function, as @{print} - | ||||
| -- will instead operate with the new terminal object. | ||||
| -- | ||||
| -- A "terminal object" is simply a table that contains functions with the same | ||||
| -- names - and general features - as those found in the term table. For example, | ||||
| -- a wrapped monitor is suitable. | ||||
| -- | ||||
| -- The redirect can be undone by pointing back to the previous terminal object | ||||
| -- (which this function returns whenever you switch). | ||||
| -- | ||||
| -- @tparam Redirect target The terminal redirect the @{term} API will draw to. | ||||
| -- @treturn Redirect The previous redirect object, as returned by | ||||
| -- @{term.current}. | ||||
| -- @usage | ||||
| -- Redirect to a monitor on the right of the computer. | ||||
| --     term.redirect(peripheral.wrap("right")) | ||||
| term.redirect = function(target) | ||||
|     expect(1, target, "table") | ||||
|     if target == term or target == _G.term then | ||||
|         error( "term is not a recommended redirect target, try term.current() instead", 2 ) | ||||
|         error("term is not a recommended redirect target, try term.current() instead", 2) | ||||
|     end | ||||
|     for k, v in pairs( native ) do | ||||
|         if type( k ) == "string" and type( v ) == "function" then | ||||
|             if type( target[k] ) ~= "function" then | ||||
|     for k, v in pairs(native) do | ||||
|         if type(k) == "string" and type(v) == "function" then | ||||
|             if type(target[k]) ~= "function" then | ||||
|                 target[k] = function() | ||||
|                     error( "Redirect object is missing method " .. k .. ".", 2 ) | ||||
|                     error("Redirect object is missing method " .. k .. ".", 2) | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
| @@ -30,31 +53,36 @@ term.redirect = function( target ) | ||||
|     return oldRedirectTarget | ||||
| end | ||||
|  | ||||
| --- Returns the current terminal object of the computer. | ||||
| -- | ||||
| -- @treturn Redirect The current terminal redirect | ||||
| -- @usage | ||||
| -- Create a new @{window} which draws to the current redirect target | ||||
| --     window.create(term.current(), 1, 1, 10, 10) | ||||
| term.current = function() | ||||
|     return redirectTarget | ||||
| end | ||||
|  | ||||
| --- Get the native terminal object of the current computer. | ||||
| -- | ||||
| -- It is recommended you do not use this function unless you absolutely have | ||||
| -- to. In a multitasked environment, @{term.native} will _not_ be the current | ||||
| -- terminal object, and so drawing may interfere with other programs. | ||||
| -- | ||||
| -- @treturn Redirect The native terminal redirect. | ||||
| term.native = function() | ||||
|     -- NOTE: please don't use this function unless you have to. | ||||
|     -- If you're running in a redirected or multitasked enviorment, term.native() will NOT be | ||||
|     -- the current terminal when your program starts up. It is far better to use term.current() | ||||
|     return native | ||||
| end | ||||
|  | ||||
| -- Some methods shouldn't go through redirects, so we move them to the main | ||||
| -- term API. | ||||
| for _, method in ipairs { "nativePaletteColor", "nativePaletteColour"} do | ||||
| for _, method in ipairs { "nativePaletteColor", "nativePaletteColour" } do | ||||
|     term[method] = native[method] | ||||
|     native[method] = nil | ||||
| end | ||||
|  | ||||
| for k, v in pairs( native ) do | ||||
|     if type( k ) == "string" and type( v ) == "function" and term[k] == nil then | ||||
|         term[k] = wrap( k ) | ||||
| for k, v in pairs(native) do | ||||
|     if type(k) == "string" and type(v) == "function" and rawget(term, k) == nil then | ||||
|         term[k] = wrap(k) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local env = _ENV | ||||
| for k, v in pairs( term ) do | ||||
|     env[k] = v | ||||
| end | ||||
|   | ||||
| @@ -1,32 +1,65 @@ | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
| --- The `textutils` API provides helpful utilities for formatting and | ||||
| -- manipulating strings. | ||||
| -- | ||||
| -- @module textutils | ||||
|  | ||||
| function slowWrite( sText, nRate ) | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua") | ||||
| local expect, field = expect.expect, expect.field | ||||
|  | ||||
| --- Slowly writes string text at current cursor position, | ||||
| -- character-by-character. | ||||
| -- | ||||
| -- Like @{write}, this does not insert a newline at the end. | ||||
| -- | ||||
| -- @tparam string sText The the text to write to the screen | ||||
| -- @tparam[opt] number nRate The number of characters to write each second, | ||||
| -- Defaults to 20. | ||||
| -- @usage textutils.slowWrite("Hello, world!") | ||||
| -- @usage textutils.slowWrite("Hello, world!", 5) | ||||
| function slowWrite(sText, nRate) | ||||
|     expect(2, nRate, "number", "nil") | ||||
|     nRate = nRate or 20 | ||||
|     if nRate < 0 then | ||||
|         error( "Rate must be positive", 2 ) | ||||
|         error("Rate must be positive", 2) | ||||
|     end | ||||
|     local nSleep = 1 / nRate | ||||
|  | ||||
|     sText = tostring( sText ) | ||||
|     sText = tostring(sText) | ||||
|     local x, y = term.getCursorPos() | ||||
|     local len = #sText | ||||
|  | ||||
|     for n = 1, len do | ||||
|         term.setCursorPos( x, y ) | ||||
|         sleep( nSleep ) | ||||
|         local nLines = write( string.sub( sText, 1, n ) ) | ||||
|         term.setCursorPos(x, y) | ||||
|         sleep(nSleep) | ||||
|         local nLines = write(string.sub(sText, 1, n)) | ||||
|         local _, newY = term.getCursorPos() | ||||
|         y = newY - nLines | ||||
|     end | ||||
| end | ||||
|  | ||||
| function slowPrint( sText, nRate ) | ||||
|     slowWrite( sText, nRate ) | ||||
| --- Slowly prints string text at current cursor position, | ||||
| -- character-by-character. | ||||
| -- | ||||
| -- Like @{print}, this inserts a newline after printing. | ||||
| -- | ||||
| -- @tparam string sText The the text to write to the screen | ||||
| -- @tparam[opt] number nRate The number of characters to write each second, | ||||
| -- Defaults to 20. | ||||
| -- @usage textutils.slowPrint("Hello, world!") | ||||
| -- @usage textutils.slowPrint("Hello, world!", 5) | ||||
| function slowPrint(sText, nRate) | ||||
|     slowWrite(sText, nRate) | ||||
|     print() | ||||
| end | ||||
|  | ||||
| function formatTime( nTime, bTwentyFourHour ) | ||||
| --- Takes input time and formats it in a more readable format such as `6:30 PM`. | ||||
| -- | ||||
| -- @tparam number nTime The time to format, as provided by @{os.time}. | ||||
| -- @tparam[opt] boolean bTwentyFourHour Whether to format this as a 24-hour | ||||
| -- clock (`18:30`) rather than a 12-hour one (`6:30 AM`) | ||||
| -- @treturn string The formatted time | ||||
| -- @usage textutils.formatTime(os.time()) | ||||
| function formatTime(nTime, bTwentyFourHour) | ||||
|     expect(1, nTime, "number") | ||||
|     expect(2, bTwentyFourHour, "boolean", "nil") | ||||
|     local sTOD = nil | ||||
| @@ -44,26 +77,26 @@ function formatTime( nTime, bTwentyFourHour ) | ||||
|     local nHour = math.floor(nTime) | ||||
|     local nMinute = math.floor((nTime - nHour) * 60) | ||||
|     if sTOD then | ||||
|         return string.format( "%d:%02d %s", nHour, nMinute, sTOD ) | ||||
|         return string.format("%d:%02d %s", nHour, nMinute, sTOD) | ||||
|     else | ||||
|         return string.format( "%d:%02d", nHour, nMinute ) | ||||
|         return string.format("%d:%02d", nHour, nMinute) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function makePagedScroll( _term, _nFreeLines ) | ||||
| local function makePagedScroll(_term, _nFreeLines) | ||||
|     local nativeScroll = _term.scroll | ||||
|     local nFreeLines = _nFreeLines or 0 | ||||
|     return function( _n ) | ||||
|     return function(_n) | ||||
|         for _ = 1, _n do | ||||
|             nativeScroll( 1 ) | ||||
|             nativeScroll(1) | ||||
|  | ||||
|             if nFreeLines <= 0 then | ||||
|                 local _, h = _term.getSize() | ||||
|                 _term.setCursorPos( 1, h ) | ||||
|                 _term.write( "Press any key to continue" ) | ||||
|                 os.pullEvent( "key" ) | ||||
|                 _term.setCursorPos(1, h) | ||||
|                 _term.write("Press any key to continue") | ||||
|                 os.pullEvent("key") | ||||
|                 _term.clearLine() | ||||
|                 _term.setCursorPos( 1, h ) | ||||
|                 _term.setCursorPos(1, h) | ||||
|             else | ||||
|                 nFreeLines = nFreeLines - 1 | ||||
|             end | ||||
| @@ -71,38 +104,55 @@ local function makePagedScroll( _term, _nFreeLines ) | ||||
|     end | ||||
| end | ||||
|  | ||||
| function pagedPrint( _sText, _nFreeLines ) | ||||
| --- Prints a given string to the display. | ||||
| -- | ||||
| -- If the action can be completed without scrolling, it acts much the same as | ||||
| -- @{print}; otherwise, it will throw up a "Press any key to continue" prompt at | ||||
| -- the bottom of the display. Each press will cause it to scroll down and write | ||||
| -- a single line more before prompting again, if need be. | ||||
| -- | ||||
| -- @tparam string _sText The text to print to the screen. | ||||
| -- @tparam[opt] number _nFreeLines The number of lines which will be | ||||
| -- automatically scrolled before the first prompt appears (meaning _nFreeLines + | ||||
| -- 1 lines will be printed). This can be set to the terminal's height - 2 to | ||||
| -- always try to fill the screen. Defaults to 0, meaning only one line is | ||||
| -- displayed before prompting. | ||||
| -- @treturn number The number of lines printed. | ||||
| -- @usage | ||||
| -- local width, height = term.getSize() | ||||
| -- textutils.pagedPrint(("This is a rather verbose dose of repetition.\n"):rep(30), height - 2) | ||||
| function pagedPrint(_sText, _nFreeLines) | ||||
|     expect(2, _nFreeLines, "number", "nil") | ||||
|     -- Setup a redirector | ||||
|     local oldTerm = term.current() | ||||
|     local newTerm = {} | ||||
|     for k, v in pairs( oldTerm ) do | ||||
|     for k, v in pairs(oldTerm) do | ||||
|         newTerm[k] = v | ||||
|     end | ||||
|     newTerm.scroll = makePagedScroll( oldTerm, _nFreeLines ) | ||||
|     term.redirect( newTerm ) | ||||
|     newTerm.scroll = makePagedScroll(oldTerm, _nFreeLines) | ||||
|     term.redirect(newTerm) | ||||
|  | ||||
|     -- Print the text | ||||
|     local result | ||||
|     local ok, err = pcall( function() | ||||
|     local ok, err = pcall(function() | ||||
|         if _sText ~= nil then | ||||
|             result = print( _sText ) | ||||
|             result = print(_sText) | ||||
|         else | ||||
|             result = print() | ||||
|         end | ||||
|     end ) | ||||
|     end) | ||||
|  | ||||
|     -- Removed the redirector | ||||
|     term.redirect( oldTerm ) | ||||
|     term.redirect(oldTerm) | ||||
|  | ||||
|     -- Propogate errors | ||||
|     if not ok then | ||||
|         error( err, 0 ) | ||||
|         error(err, 0) | ||||
|     end | ||||
|     return result | ||||
| end | ||||
|  | ||||
| local function tabulateCommon( bPaged, ... ) | ||||
| local function tabulateCommon(bPaged, ...) | ||||
|     local tAll = table.pack(...) | ||||
|     for i = 1, tAll.n do | ||||
|         expect(i, tAll[i], "number", "table") | ||||
| @@ -110,17 +160,17 @@ local function tabulateCommon( bPaged, ... ) | ||||
|  | ||||
|     local w, h = term.getSize() | ||||
|     local nMaxLen = w / 8 | ||||
|     for n, t in ipairs( tAll ) do | ||||
|     for n, t in ipairs(tAll) do | ||||
|         if type(t) == "table" then | ||||
|             for nu, sItem in pairs(t) do | ||||
|                 if type( sItem ) ~= "string" then | ||||
|                     error( "bad argument #" .. n .. "." .. nu .. " (expected string, got " .. type( sItem ) .. ")", 3 ) | ||||
|                 if type(sItem) ~= "string" then | ||||
|                     error("bad argument #" .. n .. "." .. nu .. " (expected string, got " .. type(sItem) .. ")", 3) | ||||
|                 end | ||||
|                 nMaxLen = math.max( #sItem + 1, nMaxLen ) | ||||
|                 nMaxLen = math.max(#sItem + 1, nMaxLen) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|     local nCols = math.floor( w / nMaxLen ) | ||||
|     local nCols = math.floor(w / nMaxLen) | ||||
|     local nLines = 0 | ||||
|     local function newLine() | ||||
|         if bPaged and nLines >= h - 3 then | ||||
| @@ -131,9 +181,9 @@ local function tabulateCommon( bPaged, ... ) | ||||
|         nLines = nLines + 1 | ||||
|     end | ||||
|  | ||||
|     local function drawCols( _t ) | ||||
|     local function drawCols(_t) | ||||
|         local nCol = 1 | ||||
|         for _, s in ipairs( _t ) do | ||||
|         for _, s in ipairs(_t) do | ||||
|             if nCol > nCols then | ||||
|                 nCol = 1 | ||||
|                 newLine() | ||||
| @@ -141,61 +191,81 @@ local function tabulateCommon( bPaged, ... ) | ||||
|  | ||||
|             local cx, cy = term.getCursorPos() | ||||
|             cx = 1 + (nCol - 1) * nMaxLen | ||||
|             term.setCursorPos( cx, cy ) | ||||
|             term.write( s ) | ||||
|             term.setCursorPos(cx, cy) | ||||
|             term.write(s) | ||||
|  | ||||
|             nCol = nCol + 1 | ||||
|         end | ||||
|         print() | ||||
|     end | ||||
|     for _, t in ipairs( tAll ) do | ||||
|     for _, t in ipairs(tAll) do | ||||
|         if type(t) == "table" then | ||||
|             if #t > 0 then | ||||
|                 drawCols( t ) | ||||
|                 drawCols(t) | ||||
|             end | ||||
|         elseif type(t) == "number" then | ||||
|             term.setTextColor( t ) | ||||
|             term.setTextColor(t) | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| function tabulate( ... ) | ||||
|     return tabulateCommon( false, ... ) | ||||
| --- Prints tables in a structured form. | ||||
| -- | ||||
| -- This accepts multiple arguments, either a table or a number. When | ||||
| -- encountering a table, this will be treated as a table row, with each column | ||||
| -- width being auto-adjusted. | ||||
| -- | ||||
| -- When encountering a number, this sets the text color of the subsequent rows to it. | ||||
| -- | ||||
| -- @tparam {string...}|number ... The rows and text colors to display. | ||||
| -- @usage textutils.tabulate(colors.orange, { "1", "2", "3" }, colors.lightBlue, { "A", "B", "C" }) | ||||
| function tabulate(...) | ||||
|     return tabulateCommon(false, ...) | ||||
| end | ||||
|  | ||||
| function pagedTabulate( ... ) | ||||
|     return tabulateCommon( true, ... ) | ||||
| --- Prints tables in a structured form, stopping and prompting for input should | ||||
| -- the result not fit on the terminal. | ||||
| -- | ||||
| -- This functions identically to @{textutils.tabulate}, but will prompt for user | ||||
| -- input should the whole output not fit on the display. | ||||
| -- | ||||
| -- @tparam {string...}|number ... The rows and text colors to display. | ||||
| -- @usage textutils.tabulate(colors.orange, { "1", "2", "3" }, colors.lightBlue, { "A", "B", "C" }) | ||||
| -- @see textutils.tabulate | ||||
| -- @see textutils.pagedPrint | ||||
| function pagedTabulate(...) | ||||
|     return tabulateCommon(true, ...) | ||||
| end | ||||
|  | ||||
| local g_tLuaKeywords = { | ||||
|     [ "and" ] = true, | ||||
|     [ "break" ] = true, | ||||
|     [ "do" ] = true, | ||||
|     [ "else" ] = true, | ||||
|     [ "elseif" ] = true, | ||||
|     [ "end" ] = true, | ||||
|     [ "false" ] = true, | ||||
|     [ "for" ] = true, | ||||
|     [ "function" ] = true, | ||||
|     [ "if" ] = true, | ||||
|     [ "in" ] = true, | ||||
|     [ "local" ] = true, | ||||
|     [ "nil" ] = true, | ||||
|     [ "not" ] = true, | ||||
|     [ "or" ] = true, | ||||
|     [ "repeat" ] = true, | ||||
|     [ "return" ] = true, | ||||
|     [ "then" ] = true, | ||||
|     [ "true" ] = true, | ||||
|     [ "until" ] = true, | ||||
|     [ "while" ] = true, | ||||
|     ["and"] = true, | ||||
|     ["break"] = true, | ||||
|     ["do"] = true, | ||||
|     ["else"] = true, | ||||
|     ["elseif"] = true, | ||||
|     ["end"] = true, | ||||
|     ["false"] = true, | ||||
|     ["for"] = true, | ||||
|     ["function"] = true, | ||||
|     ["if"] = true, | ||||
|     ["in"] = true, | ||||
|     ["local"] = true, | ||||
|     ["nil"] = true, | ||||
|     ["not"] = true, | ||||
|     ["or"] = true, | ||||
|     ["repeat"] = true, | ||||
|     ["return"] = true, | ||||
|     ["then"] = true, | ||||
|     ["true"] = true, | ||||
|     ["until"] = true, | ||||
|     ["while"] = true, | ||||
| } | ||||
|  | ||||
| local function serializeImpl( t, tTracking, sIndent ) | ||||
| local function serializeImpl(t, tTracking, sIndent) | ||||
|     local sType = type(t) | ||||
|     if sType == "table" then | ||||
|         if tTracking[t] ~= nil then | ||||
|             error( "Cannot serialize table with recursive entries", 0 ) | ||||
|             error("Cannot serialize table with recursive entries", 0) | ||||
|         end | ||||
|         tTracking[t] = true | ||||
|  | ||||
| @@ -209,15 +279,15 @@ local function serializeImpl( t, tTracking, sIndent ) | ||||
|             local tSeen = {} | ||||
|             for k, v in ipairs(t) do | ||||
|                 tSeen[k] = true | ||||
|                 sResult = sResult .. sSubIndent .. serializeImpl( v, tTracking, sSubIndent ) .. ",\n" | ||||
|                 sResult = sResult .. sSubIndent .. serializeImpl(v, tTracking, sSubIndent) .. ",\n" | ||||
|             end | ||||
|             for k, v in pairs(t) do | ||||
|                 if not tSeen[k] then | ||||
|                     local sEntry | ||||
|                     if type(k) == "string" and not g_tLuaKeywords[k] and string.match( k, "^[%a_][%a%d_]*$" ) then | ||||
|                         sEntry = k .. " = " .. serializeImpl( v, tTracking, sSubIndent ) .. ",\n" | ||||
|                     if type(k) == "string" and not g_tLuaKeywords[k] and string.match(k, "^[%a_][%a%d_]*$") then | ||||
|                         sEntry = k .. " = " .. serializeImpl(v, tTracking, sSubIndent) .. ",\n" | ||||
|                     else | ||||
|                         sEntry = "[ " .. serializeImpl( k, tTracking, sSubIndent ) .. " ] = " .. serializeImpl( v, tTracking, sSubIndent ) .. ",\n" | ||||
|                         sEntry = "[ " .. serializeImpl(k, tTracking, sSubIndent) .. " ] = " .. serializeImpl(v, tTracking, sSubIndent) .. ",\n" | ||||
|                     end | ||||
|                     sResult = sResult .. sSubIndent .. sEntry | ||||
|                 end | ||||
| @@ -227,31 +297,52 @@ local function serializeImpl( t, tTracking, sIndent ) | ||||
|         end | ||||
|  | ||||
|     elseif sType == "string" then | ||||
|         return string.format( "%q", t ) | ||||
|         return string.format("%q", t) | ||||
|  | ||||
|     elseif sType == "number" or sType == "boolean" or sType == "nil" then | ||||
|         return tostring(t) | ||||
|  | ||||
|     else | ||||
|         error( "Cannot serialize type " .. sType, 0 ) | ||||
|         error("Cannot serialize type " .. sType, 0) | ||||
|  | ||||
|     end | ||||
| end | ||||
|  | ||||
| empty_json_array = setmetatable({}, { | ||||
|     __newindex = function() | ||||
|         error("attempt to mutate textutils.empty_json_array", 2) | ||||
|     end, | ||||
| }) | ||||
| local function mk_tbl(str, name) | ||||
|     local msg = "attempt to mutate textutils." .. name | ||||
|     return setmetatable({}, { | ||||
|         __newindex = function() error(msg, 2) end, | ||||
|         __tostring = function() return str end, | ||||
|     }) | ||||
| end | ||||
|  | ||||
| local function serializeJSONImpl( t, tTracking, bNBTStyle ) | ||||
| --- A table representing an empty JSON array, in order to distinguish it from an | ||||
| -- empty JSON object. | ||||
| -- | ||||
| -- The contents of this table should not be modified. | ||||
| -- | ||||
| -- @usage textutils.serialiseJSON(textutils.empty_json_array) | ||||
| -- @see textutils.serialiseJSON | ||||
| -- @see textutils.unserialiseJSON | ||||
| empty_json_array = mk_tbl("[]", "empty_json_array") | ||||
|  | ||||
| --- A table representing the JSON null value. | ||||
| -- | ||||
| -- The contents of this table should not be modified. | ||||
| -- | ||||
| -- @usage textutils.serialiseJSON(textutils.json_null) | ||||
| -- @see textutils.serialiseJSON | ||||
| -- @see textutils.unserialiseJSON | ||||
| json_null = mk_tbl("null", "json_null") | ||||
|  | ||||
| local function serializeJSONImpl(t, tTracking, bNBTStyle) | ||||
|     local sType = type(t) | ||||
|     if t == empty_json_array then | ||||
|         return "[]" | ||||
|     if t == empty_json_array then return "[]" | ||||
|     elseif t == json_null then return "null" | ||||
|  | ||||
|     elseif sType == "table" then | ||||
|         if tTracking[t] ~= nil then | ||||
|             error( "Cannot serialize table with recursive entries", 0 ) | ||||
|             error("Cannot serialize table with recursive entries", 0) | ||||
|         end | ||||
|         tTracking[t] = true | ||||
|  | ||||
| @@ -268,9 +359,9 @@ local function serializeJSONImpl( t, tTracking, bNBTStyle ) | ||||
|                 if type(k) == "string" then | ||||
|                     local sEntry | ||||
|                     if bNBTStyle then | ||||
|                         sEntry = tostring(k) .. ":" .. serializeJSONImpl( v, tTracking, bNBTStyle ) | ||||
|                         sEntry = tostring(k) .. ":" .. serializeJSONImpl(v, tTracking, bNBTStyle) | ||||
|                     else | ||||
|                         sEntry = string.format( "%q", k ) .. ":" .. serializeJSONImpl( v, tTracking, bNBTStyle ) | ||||
|                         sEntry = string.format("%q", k) .. ":" .. serializeJSONImpl(v, tTracking, bNBTStyle) | ||||
|                     end | ||||
|                     if nObjectSize == 0 then | ||||
|                         sObjectResult = sObjectResult .. sEntry | ||||
| @@ -281,7 +372,7 @@ local function serializeJSONImpl( t, tTracking, bNBTStyle ) | ||||
|                 end | ||||
|             end | ||||
|             for _, v in ipairs(t) do | ||||
|                 local sEntry = serializeJSONImpl( v, tTracking, bNBTStyle ) | ||||
|                 local sEntry = serializeJSONImpl(v, tTracking, bNBTStyle) | ||||
|                 if nArraySize == 0 then | ||||
|                     sArrayResult = sArrayResult .. sEntry | ||||
|                 else | ||||
| @@ -299,27 +390,247 @@ local function serializeJSONImpl( t, tTracking, bNBTStyle ) | ||||
|         end | ||||
|  | ||||
|     elseif sType == "string" then | ||||
|         return string.format( "%q", t ) | ||||
|         return string.format("%q", t) | ||||
|  | ||||
|     elseif sType == "number" or sType == "boolean" then | ||||
|         return tostring(t) | ||||
|  | ||||
|     else | ||||
|         error( "Cannot serialize type " .. sType, 0 ) | ||||
|         error("Cannot serialize type " .. sType, 0) | ||||
|  | ||||
|     end | ||||
| end | ||||
|  | ||||
| function serialize( t ) | ||||
|     local tTracking = {} | ||||
|     return serializeImpl( t, tTracking, "" ) | ||||
| local unserialise_json | ||||
| do | ||||
|     local sub, find, match, concat, tonumber = string.sub, string.find, string.match, table.concat, tonumber | ||||
|  | ||||
|     --- Skip any whitespace | ||||
|     local function skip(str, pos) | ||||
|         local _, last = find(str, "^[ \n\r\v]+", pos) | ||||
|         if last then return last + 1 else return pos end | ||||
|     end | ||||
|  | ||||
|     local escapes = { | ||||
|         ["b"] = '\b', ["f"] = '\f', ["n"] = '\n', ["r"] = '\r', ["t"] = '\t', | ||||
|         ["\""] = "\"", ["/"] = "/", ["\\"] = "\\", | ||||
|     } | ||||
|  | ||||
|     local mt = {} | ||||
|  | ||||
|     local function error_at(pos, msg, ...) | ||||
|         if select('#', ...) > 0 then msg = msg:format(...) end | ||||
|         error(setmetatable({ pos = pos, msg = msg }, mt)) | ||||
|     end | ||||
|  | ||||
|     local function expected(pos, actual, exp) | ||||
|         if actual == "" then actual = "end of input" else actual = ("%q"):format(actual) end | ||||
|         error_at(pos, "Unexpected %s, expected %s.", actual, exp) | ||||
|     end | ||||
|  | ||||
|     local function parse_string(str, pos) | ||||
|         local buf, n = {}, 1 | ||||
|  | ||||
|         while true do | ||||
|             local c = sub(str, pos, pos) | ||||
|             if c == "" then error_at(pos, "Unexpected end of input, expected '\"'.") end | ||||
|             if c == '"' then break end | ||||
|  | ||||
|             if c == '\\' then | ||||
|                 -- Handle the various escapes | ||||
|                 c = sub(str, pos + 1, pos + 1) | ||||
|                 if c == "" then error_at(pos, "Unexpected end of input, expected escape sequence.") end | ||||
|  | ||||
|                 if c == "u" then | ||||
|                     local num_str = match(str, "^%x%x%x%x", pos + 2) | ||||
|                     if not num_str then error_at(pos, "Malformed unicode escape %q.", sub(str, pos + 2, pos + 5)) end | ||||
|                     buf[n], n, pos = utf8.char(tonumber(num_str, 16)), n + 1, pos + 6 | ||||
|                 else | ||||
|                     local unesc = escapes[c] | ||||
|                     if not unesc then error_at(pos + 1, "Unknown escape character %q.", unesc) end | ||||
|                     buf[n], n, pos = unesc, n + 1, pos + 2 | ||||
|                 end | ||||
|             elseif c >= '\x20' then | ||||
|                 buf[n], n, pos = c, n + 1, pos + 1 | ||||
|             else | ||||
|                 error_at(pos + 1, "Unescaped whitespace %q.", c) | ||||
|             end | ||||
|         end | ||||
|  | ||||
|         return concat(buf, "", 1, n - 1), pos + 1 | ||||
|     end | ||||
|  | ||||
|     local valid = { b = true, B = true, s = true, S = true, l = true, L = true, f = true, F = true, d = true, D = true } | ||||
|     local function parse_number(str, pos, opts) | ||||
|         local _, last, num_str = find(str, '^(-?%d+%.?%d*[eE]?[+-]?%d*)', pos) | ||||
|         local val = tonumber(num_str) | ||||
|         if not val then error_at(pos, "Malformed number %q.", num_str) end | ||||
|  | ||||
|         if opts.nbt_style and valid[sub(str, pos + 1, pos + 1)] then return val, last + 2 end | ||||
|  | ||||
|         return val, last + 1 | ||||
|     end | ||||
|  | ||||
|     local function parse_ident(str, pos) | ||||
|         local _, last, val = find(str, '^([%a][%w_]*)', pos) | ||||
|         return val, last + 1 | ||||
|     end | ||||
|  | ||||
|     local function decode_impl(str, pos, opts) | ||||
|         local c = sub(str, pos, pos) | ||||
|         if c == '"' then return parse_string(str, pos + 1) | ||||
|         elseif c == "-" or c >= "0" and c <= "9" then return parse_number(str, pos, opts) | ||||
|         elseif c == "t" then | ||||
|             if sub(str, pos + 1, pos + 3) == "rue" then return true, pos + 4 end | ||||
|         elseif c == 'f' then | ||||
|             if sub(str, pos + 1, pos + 4) == "alse" then return false, pos + 5 end | ||||
|         elseif c == 'n' then | ||||
|             if sub(str, pos + 1, pos + 3) == "ull" then | ||||
|                 if opts.parse_null then | ||||
|                     return json_null, pos + 4 | ||||
|                 else | ||||
|                     return nil, pos + 4 | ||||
|                 end | ||||
|             end | ||||
|         elseif c == "{" then | ||||
|             local obj = {} | ||||
|  | ||||
|             pos = skip(str, pos + 1) | ||||
|             c = sub(str, pos, pos) | ||||
|  | ||||
|             if c == "" then return error_at(pos, "Unexpected end of input, expected '}'.") end | ||||
|             if c == "}" then return obj, pos + 1 end | ||||
|  | ||||
|             while true do | ||||
|                 local key, value | ||||
|                 if c == "\"" then key, pos = parse_string(str, pos + 1) | ||||
|                 elseif opts.nbt_style then key, pos = parse_ident(str, pos) | ||||
|                 else return expected(pos, c, "object key") | ||||
|                 end | ||||
|  | ||||
|                 pos = skip(str, pos) | ||||
|  | ||||
|                 c = sub(str, pos, pos) | ||||
|                 if c ~= ":" then return expected(pos, c, "':'") end | ||||
|  | ||||
|                 value, pos = decode_impl(str, skip(str, pos + 1), opts) | ||||
|                 obj[key] = value | ||||
|  | ||||
|                 -- Consume the next delimiter | ||||
|                 pos = skip(str, pos) | ||||
|                 c = sub(str, pos, pos) | ||||
|                 if c == "}" then break | ||||
|                 elseif c == "," then pos = skip(str, pos + 1) | ||||
|                 else return expected(pos, c, "',' or '}'") | ||||
|                 end | ||||
|  | ||||
|                 c = sub(str, pos, pos) | ||||
|             end | ||||
|  | ||||
|             return obj, pos + 1 | ||||
|  | ||||
|         elseif c == "[" then | ||||
|             local arr, n = {}, 1 | ||||
|  | ||||
|             pos = skip(str, pos + 1) | ||||
|             c = sub(str, pos, pos) | ||||
|  | ||||
|             if c == "" then return expected(pos, c, "']'") end | ||||
|             if c == "]" then return empty_json_array, pos + 1 end | ||||
|  | ||||
|             while true do | ||||
|                 n, arr[n], pos = n + 1, decode_impl(str, pos, opts) | ||||
|  | ||||
|                 -- Consume the next delimiter | ||||
|                 pos = skip(str, pos) | ||||
|                 c = sub(str, pos, pos) | ||||
|                 if c == "]" then break | ||||
|                 elseif c == "," then pos = skip(str, pos + 1) | ||||
|                 else return expected(pos, c, "',' or ']'") | ||||
|                 end | ||||
|             end | ||||
|  | ||||
|             return arr, pos + 1 | ||||
|         elseif c == "" then error_at(pos, 'Unexpected end of input.') | ||||
|         end | ||||
|  | ||||
|         error_at(pos, "Unexpected character %q.", c) | ||||
|     end | ||||
|  | ||||
|     --- Converts a serialised JSON string back into a reassembled Lua object. | ||||
|     -- | ||||
|     -- This may be used with @{textutils.serializeJSON}, or when communicating | ||||
|     -- with command blocks or web APIs. | ||||
|     -- | ||||
|     -- @tparam string s The serialised string to deserialise. | ||||
|     -- @tparam[opt] { nbt_style? = boolean, parse_null? = boolean } options | ||||
|     -- Options which control how this JSON object is parsed. | ||||
|     -- | ||||
|     --  - `nbt_style`: When true, this will accept [stringified NBT][nbt] strings, | ||||
|     --    as produced by many commands. | ||||
|     --  - `parse_null`: When true, `null` will be parsed as @{json_null}, rather | ||||
|     --    than `nil`. | ||||
|     -- | ||||
|     --  [nbt]: https://minecraft.gamepedia.com/NBT_format | ||||
|     -- @return[1] The deserialised object | ||||
|     -- @treturn[2] nil If the object could not be deserialised. | ||||
|     -- @treturn string A message describing why the JSON string is invalid. | ||||
|     unserialise_json = function(s, options) | ||||
|         expect(1, s, "string") | ||||
|         expect(2, options, "table", "nil") | ||||
|  | ||||
|         if options then | ||||
|             field(options, "nbt_style", "boolean", "nil") | ||||
|             field(options, "nbt_style", "boolean", "nil") | ||||
|         else | ||||
|             options = {} | ||||
|         end | ||||
|  | ||||
|         local ok, res, pos = pcall(decode_impl, s, skip(s, 1), options) | ||||
|         if not ok then | ||||
|             if type(res) == "table" and getmetatable(res) == mt then | ||||
|                 return nil, ("Malformed JSON at position %d: %s"):format(res.pos, res.msg) | ||||
|             end | ||||
|  | ||||
|             error(res, 0) | ||||
|         end | ||||
|  | ||||
|         pos = skip(s, pos) | ||||
|         if pos <= #s then | ||||
|             return nil, ("Malformed JSON at position %d: Unexpected trailing character %q."):format(pos, sub(s, pos, pos)) | ||||
|         end | ||||
|         return res | ||||
|  | ||||
|     end | ||||
| end | ||||
|  | ||||
| function unserialize( s ) | ||||
| --- Convert a Lua object into a textual representation, suitable for | ||||
| -- saving in a file or pretty-printing. | ||||
| -- | ||||
| -- @param t The object to serialise | ||||
| -- @treturn string The serialised representation | ||||
| -- @throws If the object contains a value which cannot be | ||||
| -- serialised. This includes functions and tables which appear multiple | ||||
| -- times. | ||||
| function serialize(t) | ||||
|     local tTracking = {} | ||||
|     return serializeImpl(t, tTracking, "") | ||||
| end | ||||
|  | ||||
| serialise = serialize -- GB version | ||||
|  | ||||
| --- Converts a serialised string back into a reassembled Lua object. | ||||
| -- | ||||
| -- This is mainly used together with @{textutils.serialize}. | ||||
| -- | ||||
| -- @tparam string s The serialised string to deserialise. | ||||
| -- @return[1] The deserialised object | ||||
| -- @treturn[2] nil If the object could not be deserialised. | ||||
| function unserialize(s) | ||||
|     expect(1, s, "string") | ||||
|     local func = load( "return " .. s, "unserialize", "t", {} ) | ||||
|     local func = load("return " .. s, "unserialize", "t", {}) | ||||
|     if func then | ||||
|         local ok, result = pcall( func ) | ||||
|         local ok, result = pcall(func) | ||||
|         if ok then | ||||
|             return result | ||||
|         end | ||||
| @@ -327,14 +638,44 @@ function unserialize( s ) | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function serializeJSON( t, bNBTStyle ) | ||||
| unserialise = unserialize -- GB version | ||||
|  | ||||
| --- Returns a JSON representation of the given data. | ||||
| -- | ||||
| -- This function attempts to guess whether a table is a JSON array or | ||||
| -- object. However, empty tables are assumed to be empty objects - use | ||||
| -- @{textutils.empty_json_array} to mark an empty array. | ||||
| -- | ||||
| -- This is largely intended for interacting with various functions from the | ||||
| -- @{commands} API, though may also be used in making @{http} requests. | ||||
| -- | ||||
| -- @param t The value to serialise. Like @{textutils.serialise}, this should not | ||||
| -- contain recursive tables or functions. | ||||
| -- @tparam[opt] boolean bNBTStyle Whether to produce NBT-style JSON (non-quoted keys) | ||||
| -- instead of standard JSON. | ||||
| -- @treturn string The JSON representation of the input. | ||||
| -- @throws If the object contains a value which cannot be | ||||
| -- serialised. This includes functions and tables which appear multiple | ||||
| -- times. | ||||
| -- @usage textutils.serializeJSON({ values = { 1, "2", true } }) | ||||
| function serializeJSON(t, bNBTStyle) | ||||
|     expect(1, t, "table", "string", "number", "boolean") | ||||
|     expect(2, bNBTStyle, "boolean", "nil") | ||||
|     local tTracking = {} | ||||
|     return serializeJSONImpl( t, tTracking, bNBTStyle or false ) | ||||
|     return serializeJSONImpl(t, tTracking, bNBTStyle or false) | ||||
| end | ||||
|  | ||||
| function urlEncode( str ) | ||||
| serialiseJSON = serializeJSON -- GB version | ||||
|  | ||||
| unserializeJSON = unserialise_json | ||||
| unserialiseJSON = unserialise_json | ||||
|  | ||||
| --- Replaces certain characters in a string to make it safe for use in URLs or POST data. | ||||
| -- | ||||
| -- @tparam string str The string to encode | ||||
| -- @treturn string The encoded string. | ||||
| -- @usage print("https://example.com/?view=" .. textutils.urlEncode(read())) | ||||
| function urlEncode(str) | ||||
|     expect(1, str, "string") | ||||
|     if str then | ||||
|         str = string.gsub(str, "\n", "\r\n") | ||||
| @@ -346,40 +687,57 @@ function urlEncode( str ) | ||||
|             else | ||||
|                 -- Non-ASCII (encode as UTF-8) | ||||
|                 return | ||||
|                     string.format("%%%02X", 192 + bit32.band( bit32.arshift(n, 6), 31 ) ) .. | ||||
|                     string.format("%%%02X", 128 + bit32.band( n, 63 ) ) | ||||
|                     string.format("%%%02X", 192 + bit32.band(bit32.arshift(n, 6), 31)) .. | ||||
|                     string.format("%%%02X", 128 + bit32.band(n, 63)) | ||||
|             end | ||||
|         end ) | ||||
|         end) | ||||
|         str = string.gsub(str, " ", "+") | ||||
|     end | ||||
|     return str | ||||
| end | ||||
|  | ||||
| local tEmpty = {} | ||||
| function complete( sSearchText, tSearchTable ) | ||||
|  | ||||
| --- Provides a list of possible completions for a partial Lua expression. | ||||
| -- | ||||
| -- If the completed element is a table, suggestions will have `.` appended to | ||||
| -- them. Similarly, functions have `(` appended to them. | ||||
| -- | ||||
| -- @tparam string sSearchText The partial expression to complete, such as a | ||||
| -- variable name or table index. | ||||
| -- | ||||
| -- @tparam[opt] table tSearchTable The table to find variables in, defaulting to | ||||
| -- the global environment (@{_G}). The function also searches the "parent" | ||||
| -- environment via the `__index` metatable field. | ||||
| -- | ||||
| -- @treturn { string... } The (possibly empty) list of completions. | ||||
| -- @see shell.setCompletionFunction | ||||
| -- @see read | ||||
| -- @usage textutils.complete( "pa", getfenv() ) | ||||
| function complete(sSearchText, tSearchTable) | ||||
|     expect(1, sSearchText, "string") | ||||
|     expect(2, tSearchTable, "table", "nil") | ||||
|  | ||||
|     if g_tLuaKeywords[sSearchText] then return tEmpty end | ||||
|     local nStart = 1 | ||||
|     local nDot = string.find( sSearchText, ".", nStart, true ) | ||||
|     local nDot = string.find(sSearchText, ".", nStart, true) | ||||
|     local tTable = tSearchTable or _ENV | ||||
|     while nDot do | ||||
|         local sPart = string.sub( sSearchText, nStart, nDot - 1 ) | ||||
|         local value = tTable[ sPart ] | ||||
|         if type( value ) == "table" then | ||||
|         local sPart = string.sub(sSearchText, nStart, nDot - 1) | ||||
|         local value = tTable[sPart] | ||||
|         if type(value) == "table" then | ||||
|             tTable = value | ||||
|             nStart = nDot + 1 | ||||
|             nDot = string.find( sSearchText, ".", nStart, true ) | ||||
|             nDot = string.find(sSearchText, ".", nStart, true) | ||||
|         else | ||||
|             return tEmpty | ||||
|         end | ||||
|     end | ||||
|     local nColon = string.find( sSearchText, ":", nStart, true ) | ||||
|     local nColon = string.find(sSearchText, ":", nStart, true) | ||||
|     if nColon then | ||||
|         local sPart = string.sub( sSearchText, nStart, nColon - 1 ) | ||||
|         local value = tTable[ sPart ] | ||||
|         if type( value ) == "table" then | ||||
|         local sPart = string.sub(sSearchText, nStart, nColon - 1) | ||||
|         local value = tTable[sPart] | ||||
|         if type(value) == "table" then | ||||
|             tTable = value | ||||
|             nStart = nColon + 1 | ||||
|         else | ||||
| @@ -387,24 +745,24 @@ function complete( sSearchText, tSearchTable ) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local sPart = string.sub( sSearchText, nStart ) | ||||
|     local sPart = string.sub(sSearchText, nStart) | ||||
|     local nPartLength = #sPart | ||||
|  | ||||
|     local tResults = {} | ||||
|     local tSeen = {} | ||||
|     while tTable do | ||||
|         for k, v in pairs( tTable ) do | ||||
|         for k, v in pairs(tTable) do | ||||
|             if not tSeen[k] and type(k) == "string" then | ||||
|                 if string.find( k, sPart, 1, true ) == 1 then | ||||
|                     if not g_tLuaKeywords[k] and string.match( k, "^[%a_][%a%d_]*$" ) then | ||||
|                         local sResult = string.sub( k, nPartLength + 1 ) | ||||
|                 if string.find(k, sPart, 1, true) == 1 then | ||||
|                     if not g_tLuaKeywords[k] and string.match(k, "^[%a_][%a%d_]*$") then | ||||
|                         local sResult = string.sub(k, nPartLength + 1) | ||||
|                         if nColon then | ||||
|                             if type(v) == "function" then | ||||
|                                 table.insert( tResults, sResult .. "(" ) | ||||
|                                 table.insert(tResults, sResult .. "(") | ||||
|                             elseif type(v) == "table" then | ||||
|                                 local tMetatable = getmetatable( v ) | ||||
|                                 if tMetatable and ( type( tMetatable.__call ) == "function" or  type( tMetatable.__call ) == "table" ) then | ||||
|                                     table.insert( tResults, sResult .. "(" ) | ||||
|                                 local tMetatable = getmetatable(v) | ||||
|                                 if tMetatable and (type(tMetatable.__call) == "function" or  type(tMetatable.__call) == "table") then | ||||
|                                     table.insert(tResults, sResult .. "(") | ||||
|                                 end | ||||
|                             end | ||||
|                         else | ||||
| @@ -413,26 +771,21 @@ function complete( sSearchText, tSearchTable ) | ||||
|                             elseif type(v) == "table" and next(v) ~= nil then | ||||
|                                 sResult = sResult .. "." | ||||
|                             end | ||||
|                             table.insert( tResults, sResult ) | ||||
|                             table.insert(tResults, sResult) | ||||
|                         end | ||||
|                     end | ||||
|                 end | ||||
|             end | ||||
|             tSeen[k] = true | ||||
|         end | ||||
|         local tMetatable = getmetatable( tTable ) | ||||
|         if tMetatable and type( tMetatable.__index ) == "table" then | ||||
|         local tMetatable = getmetatable(tTable) | ||||
|         if tMetatable and type(tMetatable.__index) == "table" then | ||||
|             tTable = tMetatable.__index | ||||
|         else | ||||
|             tTable = nil | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     table.sort( tResults ) | ||||
|     table.sort(tResults) | ||||
|     return tResults | ||||
| end | ||||
|  | ||||
| -- GB versions | ||||
| serialise = serialize | ||||
| unserialise = unserialize | ||||
| serialiseJSON = serializeJSON | ||||
|   | ||||
| @@ -1,17 +1,20 @@ | ||||
| --- The turtle API allows you to control your turtle. | ||||
| -- | ||||
| -- @module turtle | ||||
|  | ||||
| if not turtle then | ||||
|     error( "Cannot load turtle API on computer", 2 ) | ||||
|     error("Cannot load turtle API on computer", 2) | ||||
| end | ||||
| native = turtle.native or turtle | ||||
| native = turtle.native or turtle --- @local | ||||
|  | ||||
| local function addCraftMethod( object ) | ||||
|     if peripheral.getType( "left" ) == "workbench" then | ||||
|         object.craft = function( ... ) | ||||
|             return peripheral.call( "left", "craft", ... ) | ||||
| local function addCraftMethod(object) | ||||
|     if peripheral.getType("left") == "workbench" then | ||||
|         object.craft = function(...) | ||||
|             return peripheral.call("left", "craft", ...) | ||||
|         end | ||||
|     elseif peripheral.getType( "right" ) == "workbench" then | ||||
|         object.craft = function( ... ) | ||||
|             return peripheral.call( "right", "craft", ... ) | ||||
|     elseif peripheral.getType("right") == "workbench" then | ||||
|         object.craft = function(...) | ||||
|             return peripheral.call("right", "craft", ...) | ||||
|         end | ||||
|     else | ||||
|         object.craft = nil | ||||
| @@ -20,15 +23,15 @@ end | ||||
|  | ||||
| -- Put commands into environment table | ||||
| local env = _ENV | ||||
| for k, v in pairs( native ) do | ||||
| for k, v in pairs(native) do | ||||
|     if k == "equipLeft" or k == "equipRight" then | ||||
|         env[k] = function( ... ) | ||||
|             local result, err = v( ... ) | ||||
|             addCraftMethod( turtle ) | ||||
|         env[k] = function(...) | ||||
|             local result, err = v(...) | ||||
|             addCraftMethod(turtle) | ||||
|             return result, err | ||||
|         end | ||||
|     else | ||||
|         env[k] = v | ||||
|     end | ||||
| end | ||||
| addCraftMethod( env ) | ||||
| addCraftMethod(env) | ||||
|   | ||||
| @@ -1,85 +1,178 @@ | ||||
| --- The vector API provides methods to create and manipulate vectors. | ||||
| -- | ||||
| -- An introduction to vectors can be found on [Wikipedia][wiki]. | ||||
| -- | ||||
| -- [wiki]: http://en.wikipedia.org/wiki/Euclidean_vector | ||||
| -- | ||||
| -- @module vector | ||||
|  | ||||
| --- A 3-dimensional vector, with `x`, `y`, and `z` values. | ||||
| -- | ||||
| -- This is suitable for representing both position and directional vectors. | ||||
| -- | ||||
| -- @type Vector | ||||
| local vector = { | ||||
| 	add = function( self, o ) | ||||
| 		return vector.new( | ||||
| 			self.x + o.x, | ||||
| 			self.y + o.y, | ||||
| 			self.z + o.z | ||||
| 		) | ||||
| 	end, | ||||
| 	sub = function( self, o ) | ||||
| 		return vector.new( | ||||
| 			self.x - o.x, | ||||
| 			self.y - o.y, | ||||
| 			self.z - o.z | ||||
| 		) | ||||
| 	end, | ||||
| 	mul = function( self, m ) | ||||
| 		return vector.new( | ||||
| 			self.x * m, | ||||
| 			self.y * m, | ||||
| 			self.z * m | ||||
| 		) | ||||
| 	end, | ||||
| 	div = function( self, m ) | ||||
| 		return vector.new( | ||||
| 			self.x / m, | ||||
| 			self.y / m, | ||||
| 			self.z / m | ||||
| 		) | ||||
| 	end, | ||||
| 	unm = function( self ) | ||||
| 		return vector.new( | ||||
| 			-self.x, | ||||
| 			-self.y, | ||||
| 			-self.z | ||||
| 		) | ||||
| 	end, | ||||
| 	dot = function( self, o ) | ||||
| 		return self.x * o.x + self.y * o.y + self.z * o.z | ||||
| 	end, | ||||
| 	cross = function( self, o ) | ||||
| 		return vector.new( | ||||
| 			self.y * o.z - self.z * o.y, | ||||
| 			self.z * o.x - self.x * o.z, | ||||
| 			self.x * o.y - self.y * o.x | ||||
| 		) | ||||
| 	end, | ||||
| 	length = function( self ) | ||||
| 		return math.sqrt( self.x * self.x + self.y * self.y + self.z * self.z ) | ||||
| 	end, | ||||
| 	normalize = function( self ) | ||||
| 		return self:mul( 1 / self:length() ) | ||||
| 	end, | ||||
| 	round = function( self, nTolerance ) | ||||
| 	    nTolerance = nTolerance or 1.0 | ||||
| 		return vector.new( | ||||
| 			math.floor( (self.x + nTolerance * 0.5) / nTolerance ) * nTolerance, | ||||
| 			math.floor( (self.y + nTolerance * 0.5) / nTolerance ) * nTolerance, | ||||
| 			math.floor( (self.z + nTolerance * 0.5) / nTolerance ) * nTolerance | ||||
| 		) | ||||
| 	end, | ||||
| 	tostring = function( self ) | ||||
| 		return self.x .. "," .. self.y .. "," .. self.z | ||||
| 	end, | ||||
|     --- Adds two vectors together. | ||||
|     -- | ||||
|     -- @tparam Vector self The first vector to add. | ||||
|     -- @tparam Vector o The second vector to add. | ||||
|     -- @treturn Vector The resulting vector | ||||
|     -- @usage v1:add(v2) | ||||
|     -- @usage v1 + v2 | ||||
|     add = function(self, o) | ||||
|         return vector.new( | ||||
|             self.x + o.x, | ||||
|             self.y + o.y, | ||||
|             self.z + o.z | ||||
|         ) | ||||
|     end, | ||||
|  | ||||
|     --- Subtracts one vector from another. | ||||
|     -- | ||||
|     -- @tparam Vector self The vector to subtract from. | ||||
|     -- @tparam Vector o The vector to subtract. | ||||
|     -- @treturn Vector The resulting vector | ||||
|     -- @usage v1:sub(v2) | ||||
|     -- @usage v1 - v2 | ||||
|     sub = function(self, o) | ||||
|         return vector.new( | ||||
|             self.x - o.x, | ||||
|             self.y - o.y, | ||||
|             self.z - o.z | ||||
|         ) | ||||
|     end, | ||||
|  | ||||
|     --- Multiplies a vector by a scalar value. | ||||
|     -- | ||||
|     -- @tparam Vector self The vector to multiply. | ||||
|     -- @tparam number m The scalar value to multiply with. | ||||
|     -- @treturn Vector A vector with value `(x * m, y * m, z * m)`. | ||||
|     -- @usage v:mul(3) | ||||
|     -- @usage v * 3 | ||||
|     mul = function(self, m) | ||||
|         return vector.new( | ||||
|             self.x * m, | ||||
|             self.y * m, | ||||
|             self.z * m | ||||
|         ) | ||||
|     end, | ||||
|  | ||||
|     --- Divides a vector by a scalar value. | ||||
|     -- | ||||
|     -- @tparam Vector self The vector to divide. | ||||
|     -- @tparam number m The scalar value to divide by. | ||||
|     -- @treturn Vector A vector with value `(x / m, y / m, z / m)`. | ||||
|     -- @usage v:div(3) | ||||
|     -- @usage v / 3 | ||||
|     div = function(self, m) | ||||
|         return vector.new( | ||||
|             self.x / m, | ||||
|             self.y / m, | ||||
|             self.z / m | ||||
|         ) | ||||
|     end, | ||||
|  | ||||
|     --- Negate a vector | ||||
|     -- | ||||
|     -- @tparam Vector self The vector to negate. | ||||
|     -- @treturn Vector The negated vector. | ||||
|     -- @usage -v | ||||
|     unm = function(self) | ||||
|         return vector.new( | ||||
|             -self.x, | ||||
|             -self.y, | ||||
|             -self.z | ||||
|         ) | ||||
|     end, | ||||
|  | ||||
|     --- Compute the dot product of two vectors | ||||
|     -- | ||||
|     -- @tparam Vector self The first vector to compute the dot product of. | ||||
|     -- @tparam Vector o The second vector to compute the dot product of. | ||||
|     -- @treturn Vector The dot product of `self` and `o`. | ||||
|     -- @usage v1:dot(v2) | ||||
|     dot = function(self, o) | ||||
|         return self.x * o.x + self.y * o.y + self.z * o.z | ||||
|     end, | ||||
|  | ||||
|     --- Compute the cross product of two vectors | ||||
|     -- | ||||
|     -- @tparam Vector self The first vector to compute the cross product of. | ||||
|     -- @tparam Vector o The second vector to compute the cross product of. | ||||
|     -- @treturn Vector The cross product of `self` and `o`. | ||||
|     -- @usage v1:cross(v2) | ||||
|     cross = function(self, o) | ||||
|         return vector.new( | ||||
|             self.y * o.z - self.z * o.y, | ||||
|             self.z * o.x - self.x * o.z, | ||||
|             self.x * o.y - self.y * o.x | ||||
|         ) | ||||
|     end, | ||||
|  | ||||
|     --- Get the length (also referred to as magnitude) of this vector. | ||||
|     -- @tparam Vector self This vector. | ||||
|     -- @treturn number The length of this vector. | ||||
|     length = function(self) | ||||
|         return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) | ||||
|     end, | ||||
|  | ||||
|     --- Divide this vector by its length, producing with the same direction, but | ||||
|     -- of length 1. | ||||
|     -- | ||||
|     -- @tparam Vector self The vector to normalise | ||||
|     -- @treturn Vector The normalised vector | ||||
|     -- @usage v:normalize() | ||||
|     normalize = function(self) | ||||
|         return self:mul(1 / self:length()) | ||||
|     end, | ||||
|  | ||||
|     --- Construct a vector with each dimension rounded to the nearest value. | ||||
|     -- | ||||
|     -- @tparam Vector self The vector to round | ||||
|     -- @tparam[opt] number tolerance The tolerance that we should round to, | ||||
|     -- defaulting to 1. For instance, a tolerance of 0.5 will round to the | ||||
|     -- nearest 0.5. | ||||
|     -- @treturn Vector The rounded vector. | ||||
|     round = function(self, tolerance) | ||||
|         tolerance = tolerance or 1.0 | ||||
|         return vector.new( | ||||
|             math.floor((self.x + tolerance * 0.5) / tolerance) * tolerance, | ||||
|             math.floor((self.y + tolerance * 0.5) / tolerance) * tolerance, | ||||
|             math.floor((self.z + tolerance * 0.5) / tolerance) * tolerance | ||||
|         ) | ||||
|     end, | ||||
|  | ||||
|     --- Convert this vector into a string, for pretty printing. | ||||
|     -- | ||||
|     -- @tparam Vector self This vector. | ||||
|     -- @treturn string This vector's string representation. | ||||
|     -- @usage v:tostring() | ||||
|     -- @usage tostring(v) | ||||
|     tostring = function(self) | ||||
|         return self.x .. "," .. self.y .. "," .. self.z | ||||
|     end, | ||||
| } | ||||
|  | ||||
| local vmetatable = { | ||||
| 	__index = vector, | ||||
| 	__add = vector.add, | ||||
| 	__sub = vector.sub, | ||||
| 	__mul = vector.mul, | ||||
| 	__div = vector.div, | ||||
| 	__unm = vector.unm, | ||||
| 	__tostring = vector.tostring, | ||||
|     __index = vector, | ||||
|     __add = vector.add, | ||||
|     __sub = vector.sub, | ||||
|     __mul = vector.mul, | ||||
|     __div = vector.div, | ||||
|     __unm = vector.unm, | ||||
|     __tostring = vector.tostring, | ||||
| } | ||||
|  | ||||
| function new( x, y, z ) | ||||
| 	local v = { | ||||
| 		x = tonumber(x) or 0, | ||||
| 		y = tonumber(y) or 0, | ||||
| 		z = tonumber(z) or 0, | ||||
| 	} | ||||
| 	setmetatable( v, vmetatable ) | ||||
| 	return v | ||||
| --- Construct a new @{Vector} with the given coordinates. | ||||
| -- | ||||
| -- @tparam number x The X coordinate or direction of the vector. | ||||
| -- @tparam number y The Y coordinate or direction of the vector. | ||||
| -- @tparam number z The Z coordinate or direction of the vector. | ||||
| -- @treturn Vector The constructed vector. | ||||
| function new(x, y, z) | ||||
|     return setmetatable({ | ||||
|         x = tonumber(x) or 0, | ||||
|         y = tonumber(y) or 0, | ||||
|         z = tonumber(z) or 0, | ||||
|     }, vmetatable) | ||||
| end | ||||
|   | ||||
| @@ -1,29 +1,76 @@ | ||||
| --- The Window API allows easy definition of spaces within the display that can | ||||
| -- be written/drawn to, then later redrawn/repositioned/etc as need be. The API | ||||
| -- itself contains only one function, @{window.create}, which returns the | ||||
| -- windows themselves. | ||||
| -- | ||||
| -- Windows are considered terminal objects - as such, they have access to nearly | ||||
| -- all the commands in the term API (plus a few extras of their own, listed | ||||
| -- within said API) and are valid targets to redirect to. | ||||
| -- | ||||
| -- Each window has a "parent" terminal object, which can be the computer's own | ||||
| -- display, a monitor, another window or even other, user-defined terminal | ||||
| -- objects. Whenever a window is rendered to, the actual screen-writing is | ||||
| -- performed via that parent (or, if that has one too, then that parent, and so | ||||
| -- forth). Bear in mind that the cursor of a window's parent will hence be moved | ||||
| -- around etc when writing a given child window. | ||||
| -- | ||||
| -- Windows retain a memory of everything rendered "through" them (hence acting | ||||
| -- as display buffers), and if the parent's display is wiped, the window's | ||||
| -- content can be easily redrawn later. A window may also be flagged as | ||||
| -- invisible, preventing any changes to it from being rendered until it's | ||||
| -- flagged as visible once more. | ||||
| -- | ||||
| -- A parent terminal object may have multiple children assigned to it, and | ||||
| -- windows may overlap. For example, the Multishell system functions by | ||||
| -- assigning each tab a window covering the screen, each using the starting | ||||
| -- terminal display as its parent, and only one of which is visible at a time. | ||||
| -- | ||||
| -- @module window | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| local tHex = { | ||||
|     [ colors.white ] = "0", | ||||
|     [ colors.orange ] = "1", | ||||
|     [ colors.magenta ] = "2", | ||||
|     [ colors.lightBlue ] = "3", | ||||
|     [ colors.yellow ] = "4", | ||||
|     [ colors.lime ] = "5", | ||||
|     [ colors.pink ] = "6", | ||||
|     [ colors.gray ] = "7", | ||||
|     [ colors.lightGray ] = "8", | ||||
|     [ colors.cyan ] = "9", | ||||
|     [ colors.purple ] = "a", | ||||
|     [ colors.blue ] = "b", | ||||
|     [ colors.brown ] = "c", | ||||
|     [ colors.green ] = "d", | ||||
|     [ colors.red ] = "e", | ||||
|     [ colors.black ] = "f", | ||||
|     [colors.white] = "0", | ||||
|     [colors.orange] = "1", | ||||
|     [colors.magenta] = "2", | ||||
|     [colors.lightBlue] = "3", | ||||
|     [colors.yellow] = "4", | ||||
|     [colors.lime] = "5", | ||||
|     [colors.pink] = "6", | ||||
|     [colors.gray] = "7", | ||||
|     [colors.lightGray] = "8", | ||||
|     [colors.cyan] = "9", | ||||
|     [colors.purple] = "a", | ||||
|     [colors.blue] = "b", | ||||
|     [colors.brown] = "c", | ||||
|     [colors.green] = "d", | ||||
|     [colors.red] = "e", | ||||
|     [colors.black] = "f", | ||||
| } | ||||
|  | ||||
| local type = type | ||||
| local string_rep = string.rep | ||||
| local string_sub = string.sub | ||||
|  | ||||
| function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
| --- Returns a terminal object that is a space within the specified parent | ||||
| -- terminal object. This can then be used (or even redirected to) in the same | ||||
| -- manner as eg a wrapped monitor. Refer to @{term|the term API} for a list of | ||||
| -- functions available to it. | ||||
| -- | ||||
| -- @{term} itself may not be passed as the parent, though @{term.native} is | ||||
| -- acceptable. Generally, @{term.current} or a wrapped monitor will be most | ||||
| -- suitable, though windows may even have other windows assigned as their | ||||
| -- parents. | ||||
| -- | ||||
| -- @tparam term.Redirect parent The parent terminal redirect to draw to. | ||||
| -- @tparam number nX The x coordinate this window is drawn at in the parent terminal | ||||
| -- @tparam number nY The y coordinate this window is drawn at in the parent terminal | ||||
| -- @tparam number nWidth The width of this window | ||||
| -- @tparam number nHeight The height of this window | ||||
| -- @tparam[opt] boolean bStartVisible Whether this window is visible by | ||||
| -- default. Defaults to `true`. | ||||
| -- @treturn Window The constructed window | ||||
| function create(parent, nX, nY, nWidth, nHeight, bStartVisible) | ||||
|     expect(1, parent, "table") | ||||
|     expect(2, nX, "number") | ||||
|     expect(3, nY, "number") | ||||
| @@ -32,21 +79,21 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|     expect(6, bStartVisible, "boolean", "nil") | ||||
|  | ||||
|     if parent == term then | ||||
|         error( "term is not a recommended window parent, try term.current() instead", 2 ) | ||||
|         error("term is not a recommended window parent, try term.current() instead", 2) | ||||
|     end | ||||
|  | ||||
|     local sEmptySpaceLine | ||||
|     local tEmptyColorLines = {} | ||||
|     local function createEmptyLines( nWidth ) | ||||
|         sEmptySpaceLine = string_rep( " ", nWidth ) | ||||
|     local function createEmptyLines(nWidth) | ||||
|         sEmptySpaceLine = string_rep(" ", nWidth) | ||||
|         for n = 0, 15 do | ||||
|             local nColor = 2 ^ n | ||||
|             local sHex = tHex[nColor] | ||||
|             tEmptyColorLines[nColor] = string_rep( sHex, nWidth ) | ||||
|             tEmptyColorLines[nColor] = string_rep(sHex, nWidth) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     createEmptyLines( nWidth ) | ||||
|     createEmptyLines(nWidth) | ||||
|  | ||||
|     -- Setup | ||||
|     local bVisible = bStartVisible ~= false | ||||
| @@ -59,8 +106,8 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|     local tPalette = {} | ||||
|     do | ||||
|         local sEmptyText = sEmptySpaceLine | ||||
|         local sEmptyTextColor = tEmptyColorLines[ nTextColor ] | ||||
|         local sEmptyBackgroundColor = tEmptyColorLines[ nBackgroundColor ] | ||||
|         local sEmptyTextColor = tEmptyColorLines[nTextColor] | ||||
|         local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] | ||||
|         for y = 1, nHeight do | ||||
|             tLines[y] = { | ||||
|                 text = sEmptyText, | ||||
| @@ -71,7 +118,7 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|  | ||||
|         for i = 0, 15 do | ||||
|             local c = 2 ^ i | ||||
|             tPalette[c] = { parent.getPaletteColour( c ) } | ||||
|             tPalette[c] = { parent.getPaletteColour(c) } | ||||
|         end | ||||
|     end | ||||
|  | ||||
| @@ -79,45 +126,45 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|     local function updateCursorPos() | ||||
|         if nCursorX >= 1 and nCursorY >= 1 and | ||||
|            nCursorX <= nWidth and nCursorY <= nHeight then | ||||
|             parent.setCursorPos( nX + nCursorX - 1, nY + nCursorY - 1 ) | ||||
|             parent.setCursorPos(nX + nCursorX - 1, nY + nCursorY - 1) | ||||
|         else | ||||
|             parent.setCursorPos( 0, 0 ) | ||||
|             parent.setCursorPos(0, 0) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local function updateCursorBlink() | ||||
|         parent.setCursorBlink( bCursorBlink ) | ||||
|         parent.setCursorBlink(bCursorBlink) | ||||
|     end | ||||
|  | ||||
|     local function updateCursorColor() | ||||
|         parent.setTextColor( nTextColor ) | ||||
|         parent.setTextColor(nTextColor) | ||||
|     end | ||||
|  | ||||
|     local function redrawLine( n ) | ||||
|         local tLine = tLines[ n ] | ||||
|         parent.setCursorPos( nX, nY + n - 1 ) | ||||
|         parent.blit( tLine.text, tLine.textColor, tLine.backgroundColor ) | ||||
|     local function redrawLine(n) | ||||
|         local tLine = tLines[n] | ||||
|         parent.setCursorPos(nX, nY + n - 1) | ||||
|         parent.blit(tLine.text, tLine.textColor, tLine.backgroundColor) | ||||
|     end | ||||
|  | ||||
|     local function redraw() | ||||
|         for n = 1, nHeight do | ||||
|             redrawLine( n ) | ||||
|             redrawLine(n) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local function updatePalette() | ||||
|         for k, v in pairs( tPalette ) do | ||||
|             parent.setPaletteColour( k, v[1], v[2], v[3] ) | ||||
|         for k, v in pairs(tPalette) do | ||||
|             parent.setPaletteColour(k, v[1], v[2], v[3]) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local function internalBlit( sText, sTextColor, sBackgroundColor ) | ||||
|     local function internalBlit(sText, sTextColor, sBackgroundColor) | ||||
|         local nStart = nCursorX | ||||
|         local nEnd = nStart + #sText - 1 | ||||
|         if nCursorY >= 1 and nCursorY <= nHeight then | ||||
|             if nStart <= nWidth and nEnd >= 1 then | ||||
|                 -- Modify line | ||||
|                 local tLine = tLines[ nCursorY ] | ||||
|                 local tLine = tLines[nCursorY] | ||||
|                 if nStart == 1 and nEnd == nWidth then | ||||
|                     tLine.text = sText | ||||
|                     tLine.textColor = sTextColor | ||||
| @@ -127,14 +174,14 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|                     if nStart < 1 then | ||||
|                         local nClipStart = 1 - nStart + 1 | ||||
|                         local nClipEnd = nWidth - nStart + 1 | ||||
|                         sClippedText = string_sub( sText, nClipStart, nClipEnd ) | ||||
|                         sClippedTextColor = string_sub( sTextColor, nClipStart, nClipEnd ) | ||||
|                         sClippedBackgroundColor = string_sub( sBackgroundColor, nClipStart, nClipEnd ) | ||||
|                         sClippedText = string_sub(sText, nClipStart, nClipEnd) | ||||
|                         sClippedTextColor = string_sub(sTextColor, nClipStart, nClipEnd) | ||||
|                         sClippedBackgroundColor = string_sub(sBackgroundColor, nClipStart, nClipEnd) | ||||
|                     elseif nEnd > nWidth then | ||||
|                         local nClipEnd = nWidth - nStart + 1 | ||||
|                         sClippedText = string_sub( sText, 1, nClipEnd ) | ||||
|                         sClippedTextColor = string_sub( sTextColor, 1, nClipEnd ) | ||||
|                         sClippedBackgroundColor = string_sub( sBackgroundColor, 1, nClipEnd ) | ||||
|                         sClippedText = string_sub(sText, 1, nClipEnd) | ||||
|                         sClippedTextColor = string_sub(sTextColor, 1, nClipEnd) | ||||
|                         sClippedBackgroundColor = string_sub(sBackgroundColor, 1, nClipEnd) | ||||
|                     else | ||||
|                         sClippedText = sText | ||||
|                         sClippedTextColor = sTextColor | ||||
| @@ -147,9 +194,9 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|                     local sNewText, sNewTextColor, sNewBackgroundColor | ||||
|                     if nStart > 1 then | ||||
|                         local nOldEnd = nStart - 1 | ||||
|                         sNewText = string_sub( sOldText, 1, nOldEnd ) .. sClippedText | ||||
|                         sNewTextColor = string_sub( sOldTextColor, 1, nOldEnd ) .. sClippedTextColor | ||||
|                         sNewBackgroundColor = string_sub( sOldBackgroundColor, 1, nOldEnd ) .. sClippedBackgroundColor | ||||
|                         sNewText = string_sub(sOldText, 1, nOldEnd) .. sClippedText | ||||
|                         sNewTextColor = string_sub(sOldTextColor, 1, nOldEnd) .. sClippedTextColor | ||||
|                         sNewBackgroundColor = string_sub(sOldBackgroundColor, 1, nOldEnd) .. sClippedBackgroundColor | ||||
|                     else | ||||
|                         sNewText = sClippedText | ||||
|                         sNewTextColor = sClippedTextColor | ||||
| @@ -157,9 +204,9 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|                     end | ||||
|                     if nEnd < nWidth then | ||||
|                         local nOldStart = nEnd + 1 | ||||
|                         sNewText = sNewText .. string_sub( sOldText, nOldStart, nWidth ) | ||||
|                         sNewTextColor = sNewTextColor .. string_sub( sOldTextColor, nOldStart, nWidth ) | ||||
|                         sNewBackgroundColor = sNewBackgroundColor .. string_sub( sOldBackgroundColor, nOldStart, nWidth ) | ||||
|                         sNewText = sNewText .. string_sub(sOldText, nOldStart, nWidth) | ||||
|                         sNewTextColor = sNewTextColor .. string_sub(sOldTextColor, nOldStart, nWidth) | ||||
|                         sNewBackgroundColor = sNewBackgroundColor .. string_sub(sOldBackgroundColor, nOldStart, nWidth) | ||||
|                     end | ||||
|  | ||||
|                     tLine.text = sNewText | ||||
| @@ -169,7 +216,7 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|  | ||||
|                 -- Redraw line | ||||
|                 if bVisible then | ||||
|                     redrawLine( nCursorY ) | ||||
|                     redrawLine(nCursorY) | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
| @@ -182,28 +229,32 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     -- Terminal implementation | ||||
|     --- The window object. Refer to the @{window|module's documentation} for | ||||
|     -- a full description. | ||||
|     -- | ||||
|     -- @type Window | ||||
|     -- @see term.Redirect | ||||
|     local window = {} | ||||
|  | ||||
|     function window.write( sText ) | ||||
|         sText = tostring( sText ) | ||||
|         internalBlit( sText, string_rep( tHex[ nTextColor ], #sText ), string_rep( tHex[ nBackgroundColor ], #sText ) ) | ||||
|     function window.write(sText) | ||||
|         sText = tostring(sText) | ||||
|         internalBlit(sText, string_rep(tHex[nTextColor], #sText), string_rep(tHex[nBackgroundColor], #sText)) | ||||
|     end | ||||
|  | ||||
|     function window.blit( sText, sTextColor, sBackgroundColor ) | ||||
|     function window.blit(sText, sTextColor, sBackgroundColor) | ||||
|         if type(sText) ~= "string" then expect(1, sText, "string") end | ||||
|         if type(sTextColor) ~= "string" then expect(2, sTextColor, "string") end | ||||
|         if type(sBackgroundColor) ~= "string" then expect(3, sBackgroundColor, "string") end | ||||
|         if #sTextColor ~= #sText or #sBackgroundColor ~= #sText then | ||||
|             error( "Arguments must be the same length", 2 ) | ||||
|             error("Arguments must be the same length", 2) | ||||
|         end | ||||
|         internalBlit( sText, sTextColor, sBackgroundColor ) | ||||
|         internalBlit(sText, sTextColor, sBackgroundColor) | ||||
|     end | ||||
|  | ||||
|     function window.clear() | ||||
|         local sEmptyText = sEmptySpaceLine | ||||
|         local sEmptyTextColor = tEmptyColorLines[ nTextColor ] | ||||
|         local sEmptyBackgroundColor = tEmptyColorLines[ nBackgroundColor ] | ||||
|         local sEmptyTextColor = tEmptyColorLines[nTextColor] | ||||
|         local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] | ||||
|         for y = 1, nHeight do | ||||
|             tLines[y] = { | ||||
|                 text = sEmptyText, | ||||
| @@ -221,15 +272,15 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|     function window.clearLine() | ||||
|         if nCursorY >= 1 and nCursorY <= nHeight then | ||||
|             local sEmptyText = sEmptySpaceLine | ||||
|             local sEmptyTextColor = tEmptyColorLines[ nTextColor ] | ||||
|             local sEmptyBackgroundColor = tEmptyColorLines[ nBackgroundColor ] | ||||
|             tLines[ nCursorY ] = { | ||||
|             local sEmptyTextColor = tEmptyColorLines[nTextColor] | ||||
|             local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] | ||||
|             tLines[nCursorY] = { | ||||
|                 text = sEmptyText, | ||||
|                 textColor = sEmptyTextColor, | ||||
|                 backgroundColor = sEmptyBackgroundColor, | ||||
|             } | ||||
|             if bVisible then | ||||
|                 redrawLine( nCursorY ) | ||||
|                 redrawLine(nCursorY) | ||||
|                 updateCursorColor() | ||||
|                 updateCursorPos() | ||||
|             end | ||||
| @@ -240,17 +291,17 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|         return nCursorX, nCursorY | ||||
|     end | ||||
|  | ||||
|     function window.setCursorPos( x, y ) | ||||
|     function window.setCursorPos(x, y) | ||||
|         if type(x) ~= "number" then expect(1, x, "number") end | ||||
|         if type(y) ~= "number" then expect(2, y, "number") end | ||||
|         nCursorX = math.floor( x ) | ||||
|         nCursorY = math.floor( y ) | ||||
|         nCursorX = math.floor(x) | ||||
|         nCursorY = math.floor(y) | ||||
|         if bVisible then | ||||
|             updateCursorPos() | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     function window.setCursorBlink( blink ) | ||||
|     function window.setCursorBlink(blink) | ||||
|         if type(blink) ~= "boolean" then expect(1, blink, "boolean") end | ||||
|         bCursorBlink = blink | ||||
|         if bVisible then | ||||
| @@ -274,10 +325,10 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|         return isColor() | ||||
|     end | ||||
|  | ||||
|     local function setTextColor( color ) | ||||
|     local function setTextColor(color) | ||||
|         if type(color) ~= "number" then expect(1, color, "number") end | ||||
|         if tHex[color] == nil then | ||||
|             error( "Invalid color (got " .. color .. ")" , 2 ) | ||||
|             error("Invalid color (got " .. color .. ")" , 2) | ||||
|         end | ||||
|  | ||||
|         nTextColor = color | ||||
| @@ -289,50 +340,50 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|     window.setTextColor = setTextColor | ||||
|     window.setTextColour = setTextColor | ||||
|  | ||||
|     function window.setPaletteColour( colour, r, g, b ) | ||||
|     function window.setPaletteColour(colour, r, g, b) | ||||
|         if type(colour) ~= "number" then expect(1, colour, "number") end | ||||
|  | ||||
|         if tHex[colour] == nil then | ||||
|             error( "Invalid color (got " .. colour .. ")" , 2 ) | ||||
|             error("Invalid color (got " .. colour .. ")" , 2) | ||||
|         end | ||||
|  | ||||
|         local tCol | ||||
|         if type(r) == "number" and g == nil and b == nil then | ||||
|             tCol = { colours.unpackRGB( r ) } | ||||
|             tPalette[ colour ] = tCol | ||||
|             tCol = { colours.unpackRGB(r) } | ||||
|             tPalette[colour] = tCol | ||||
|         else | ||||
|             if type(r) ~= "number" then expect(2, r, "number") end | ||||
|             if type(g) ~= "number" then expect(3, g, "number") end | ||||
|             if type(b) ~= "number" then expect(4, b, "number") end | ||||
|  | ||||
|             tCol = tPalette[ colour ] | ||||
|             tCol = tPalette[colour] | ||||
|             tCol[1] = r | ||||
|             tCol[2] = g | ||||
|             tCol[3] = b | ||||
|         end | ||||
|  | ||||
|         if bVisible then | ||||
|             return parent.setPaletteColour( colour, tCol[1], tCol[2], tCol[3] ) | ||||
|             return parent.setPaletteColour(colour, tCol[1], tCol[2], tCol[3]) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     window.setPaletteColor = window.setPaletteColour | ||||
|  | ||||
|     function window.getPaletteColour( colour ) | ||||
|     function window.getPaletteColour(colour) | ||||
|         if type(colour) ~= "number" then expect(1, colour, "number") end | ||||
|         if tHex[colour] == nil then | ||||
|             error( "Invalid color (got " .. colour .. ")" , 2 ) | ||||
|             error("Invalid color (got " .. colour .. ")" , 2) | ||||
|         end | ||||
|         local tCol = tPalette[ colour ] | ||||
|         local tCol = tPalette[colour] | ||||
|         return tCol[1], tCol[2], tCol[3] | ||||
|     end | ||||
|  | ||||
|     window.getPaletteColor = window.getPaletteColour | ||||
|  | ||||
|     local function setBackgroundColor( color ) | ||||
|     local function setBackgroundColor(color) | ||||
|         if type(color) ~= "number" then expect(1, color, "number") end | ||||
|         if tHex[color] == nil then | ||||
|             error( "Invalid color (got " .. color .. ")", 2 ) | ||||
|             error("Invalid color (got " .. color .. ")", 2) | ||||
|         end | ||||
|         nBackgroundColor = color | ||||
|     end | ||||
| @@ -344,13 +395,13 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|         return nWidth, nHeight | ||||
|     end | ||||
|  | ||||
|     function window.scroll( n ) | ||||
|     function window.scroll(n) | ||||
|         if type(n) ~= "number" then expect(1, n, "number") end | ||||
|         if n ~= 0 then | ||||
|             local tNewLines = {} | ||||
|             local sEmptyText = sEmptySpaceLine | ||||
|             local sEmptyTextColor = tEmptyColorLines[ nTextColor ] | ||||
|             local sEmptyBackgroundColor = tEmptyColorLines[ nBackgroundColor ] | ||||
|             local sEmptyTextColor = tEmptyColorLines[nTextColor] | ||||
|             local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] | ||||
|             for newY = 1, nHeight do | ||||
|                 local y = newY + n | ||||
|                 if y >= 1 and y <= nHeight then | ||||
| @@ -388,6 +439,13 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|         return nBackgroundColor | ||||
|     end | ||||
|  | ||||
|     --- Get the buffered contents of a line in this window. | ||||
|     --- | ||||
|     -- @tparam number y The y position of the line to get. | ||||
|     -- @treturn string The textual content of this line. | ||||
|     -- @treturn string The text colours of this line, suitable for use with @{term.blit}. | ||||
|     -- @treturn string The background colours of this line, suitable for use with @{term.blit}. | ||||
|     -- @throws If `y` is not between 1 and this window's height. | ||||
|     function window.getLine(y) | ||||
|         if type(y) ~= "number" then expect(1, y, "number") end | ||||
|  | ||||
| @@ -399,16 +457,26 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|     end | ||||
|  | ||||
|     -- Other functions | ||||
|     function window.setVisible( bVis ) | ||||
|         if type(bVis) ~= "boolean" then expect(1, bVis, "boolean") end | ||||
|         if bVisible ~= bVis then | ||||
|             bVisible = bVis | ||||
|  | ||||
|     --- Set whether this window is visible. Invisible windows will not be drawn | ||||
|     -- to the screen until they are made visible again. | ||||
|     -- | ||||
|     -- Making an invisible window visible will immediately draw it. | ||||
|     -- | ||||
|     -- @tparam boolean visible Whether this window is visible. | ||||
|     function window.setVisible(visible) | ||||
|         if type(visible) ~= "boolean" then expect(1, visible, "boolean") end | ||||
|         if bVisible ~= visible then | ||||
|             bVisible = visible | ||||
|             if bVisible then | ||||
|                 window.redraw() | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     --- Draw this window. This does nothing if the window is not visible. | ||||
|     -- | ||||
|     -- @see Window:setVisible | ||||
|     function window.redraw() | ||||
|         if bVisible then | ||||
|             redraw() | ||||
| @@ -419,6 +487,8 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     --- Set the current terminal's cursor to where this window's cursor is. This | ||||
|     -- does nothing if the window is not visible. | ||||
|     function window.restoreCursor() | ||||
|         if bVisible then | ||||
|             updateCursorBlink() | ||||
| @@ -427,31 +497,47 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     --- Get the position of the top left corner of this window. | ||||
|     -- | ||||
|     -- @treturn number The x position of this window. | ||||
|     -- @treturn number The y position of this window. | ||||
|     function window.getPosition() | ||||
|         return nX, nY | ||||
|     end | ||||
|  | ||||
|     function window.reposition( nNewX, nNewY, nNewWidth, nNewHeight, newParent ) | ||||
|         if type(nNewX) ~= "number" then expect(1, nNewX, "number") end | ||||
|         if type(nNewY) ~= "number" then expect(2, nNewY, "number") end | ||||
|         if nNewWidth ~= nil or nNewHeight ~= nil then | ||||
|             expect(3, nNewWidth, "number") | ||||
|             expect(4, nNewHeight, "number") | ||||
|     --- Reposition or resize the given window. | ||||
|     -- | ||||
|     -- This function also accepts arguments to change the size of this window. | ||||
|     -- It is recommended that you fire a `term_resize` event after changing a | ||||
|     -- window's, to allow programs to adjust their sizing. | ||||
|     -- | ||||
|     -- @tparam number new_x The new x position of this window. | ||||
|     -- @tparam number new_y The new y position of this window. | ||||
|     -- @tparam[opt] number new_width The new width of this window. | ||||
|     -- @tparam number new_height The new height of this window. | ||||
|     -- @tparam[opt] term.Redirect new_parent The new redirect object this | ||||
|     -- window should draw to. | ||||
|     function window.reposition(new_x, new_y, new_width, new_height, new_parent) | ||||
|         if type(new_x) ~= "number" then expect(1, new_x, "number") end | ||||
|         if type(new_y) ~= "number" then expect(2, new_y, "number") end | ||||
|         if new_width ~= nil or new_height ~= nil then | ||||
|             expect(3, new_width, "number") | ||||
|             expect(4, new_height, "number") | ||||
|         end | ||||
|         if newParent ~= nil and type(newParent) ~= "table" then expect(5, newParent, "table") end | ||||
|         if new_parent ~= nil and type(new_parent) ~= "table" then expect(5, new_parent, "table") end | ||||
|  | ||||
|         nX = nNewX | ||||
|         nY = nNewY | ||||
|         nX = new_x | ||||
|         nY = new_y | ||||
|  | ||||
|         if newParent then parent = newParent end | ||||
|         if new_parent then parent = new_parent end | ||||
|  | ||||
|         if nNewWidth and nNewHeight then | ||||
|         if new_width and new_height then | ||||
|             local tNewLines = {} | ||||
|             createEmptyLines( nNewWidth ) | ||||
|             createEmptyLines(new_width) | ||||
|             local sEmptyText = sEmptySpaceLine | ||||
|             local sEmptyTextColor = tEmptyColorLines[ nTextColor ] | ||||
|             local sEmptyBackgroundColor = tEmptyColorLines[ nBackgroundColor ] | ||||
|             for y = 1, nNewHeight do | ||||
|             local sEmptyTextColor = tEmptyColorLines[nTextColor] | ||||
|             local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] | ||||
|             for y = 1, new_height do | ||||
|                 if y > nHeight then | ||||
|                     tNewLines[y] = { | ||||
|                         text = sEmptyText, | ||||
| @@ -460,25 +546,25 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible ) | ||||
|                     } | ||||
|                 else | ||||
|                     local tOldLine = tLines[y] | ||||
|                     if nNewWidth == nWidth then | ||||
|                     if new_width == nWidth then | ||||
|                         tNewLines[y] = tOldLine | ||||
|                     elseif nNewWidth < nWidth then | ||||
|                     elseif new_width < nWidth then | ||||
|                         tNewLines[y] = { | ||||
|                             text = string_sub( tOldLine.text, 1, nNewWidth ), | ||||
|                             textColor = string_sub( tOldLine.textColor, 1, nNewWidth ), | ||||
|                             backgroundColor = string_sub( tOldLine.backgroundColor, 1, nNewWidth ), | ||||
|                             text = string_sub(tOldLine.text, 1, new_width), | ||||
|                             textColor = string_sub(tOldLine.textColor, 1, new_width), | ||||
|                             backgroundColor = string_sub(tOldLine.backgroundColor, 1, new_width), | ||||
|                         } | ||||
|                     else | ||||
|                         tNewLines[y] = { | ||||
|                             text = tOldLine.text .. string_sub( sEmptyText, nWidth + 1, nNewWidth ), | ||||
|                             textColor = tOldLine.textColor .. string_sub( sEmptyTextColor, nWidth + 1, nNewWidth ), | ||||
|                             backgroundColor = tOldLine.backgroundColor .. string_sub( sEmptyBackgroundColor, nWidth + 1, nNewWidth ), | ||||
|                             text = tOldLine.text .. string_sub(sEmptyText, nWidth + 1, new_width), | ||||
|                             textColor = tOldLine.textColor .. string_sub(sEmptyTextColor, nWidth + 1, new_width), | ||||
|                             backgroundColor = tOldLine.backgroundColor .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width), | ||||
|                         } | ||||
|                     end | ||||
|                 end | ||||
|             end | ||||
|             nWidth = nNewWidth | ||||
|             nHeight = nNewHeight | ||||
|             nWidth = new_width | ||||
|             nHeight = new_height | ||||
|             tLines = tNewLines | ||||
|         end | ||||
|         if bVisible then | ||||
|   | ||||
| @@ -5,28 +5,29 @@ | ||||
|  | ||||
| local native_select, native_type = select, type | ||||
|  | ||||
| --- Expect an argument to have a specific type. | ||||
| -- | ||||
| -- @tparam int index The 1-based argument index. | ||||
| -- @param value The argument's value. | ||||
| -- @tparam string ... The allowed types of the argument. | ||||
| -- @throws If the value is not one of the allowed types. | ||||
| local function expect(index, value, ...) | ||||
|     local t = native_type(value) | ||||
|     for i = 1, native_select("#", ...) do | ||||
|         if t == native_select(i, ...) then return true end | ||||
|     end | ||||
|  | ||||
| local function get_type_names(...) | ||||
|     local types = table.pack(...) | ||||
|     for i = types.n, 1, -1 do | ||||
|         if types[i] == "nil" then table.remove(types, i) end | ||||
|     end | ||||
|  | ||||
|     local type_names | ||||
|     if #types <= 1 then | ||||
|         type_names = tostring(...) | ||||
|         return tostring(...) | ||||
|     else | ||||
|         type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] | ||||
|         return table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] | ||||
|     end | ||||
| end | ||||
| --- Expect an argument to have a specific type. | ||||
| -- | ||||
| -- @tparam number index The 1-based argument index. | ||||
| -- @param value The argument's value. | ||||
| -- @tparam string ... The allowed types of the argument. | ||||
| -- @return The given `value`. | ||||
| -- @throws If the value is not one of the allowed types. | ||||
| local function expect(index, value, ...) | ||||
|     local t = native_type(value) | ||||
|     for i = 1, native_select("#", ...) do | ||||
|         if t == native_select(i, ...) then return value end | ||||
|     end | ||||
|  | ||||
|     -- If we can determine the function name with a high level of confidence, try to include it. | ||||
| @@ -36,11 +37,39 @@ local function expect(index, value, ...) | ||||
|         if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end | ||||
|     end | ||||
|  | ||||
|     local type_names = get_type_names(...) | ||||
|     if name then | ||||
|         error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 ) | ||||
|         error(("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3) | ||||
|     else | ||||
|         error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 ) | ||||
|         error(("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3) | ||||
|     end | ||||
| end | ||||
|  | ||||
| return { expect = expect } | ||||
| --- Expect an field to have a specific type. | ||||
| -- | ||||
| -- @tparam table tbl The table to index. | ||||
| -- @tparam string index The field name to check. | ||||
| -- @tparam string ... The allowed types of the argument. | ||||
| -- @return The contents of the given field. | ||||
| -- @throws If the field is not one of the allowed types. | ||||
| local function field(tbl, index, ...) | ||||
|     expect(1, tbl, "table") | ||||
|     expect(2, index, "string") | ||||
|  | ||||
|     local value = tbl[index] | ||||
|     local t = native_type(value) | ||||
|     for i = 1, native_select("#", ...) do | ||||
|         if t == native_select(i, ...) then return value end | ||||
|     end | ||||
|  | ||||
|     if value == nil then | ||||
|         error(("field '%s' missing from table"):format(index), 3) | ||||
|     else | ||||
|         error(("bad field '%s' (expected %s, got %s)"):format(index, get_type_names(...), t), 3) | ||||
|     end | ||||
| end | ||||
|  | ||||
| return { | ||||
|     expect = expect, | ||||
|     field = field, | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,419 @@ | ||||
| --- 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.write(pretty.dump({ 1, 2, 3 })) | ||||
| -- | ||||
| -- @usage Build a custom document and display it | ||||
| --     local pretty = require "cc.pretty" | ||||
| --     pretty.write(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world"))) | ||||
|  | ||||
| local expect = require "cc.expect".expect | ||||
| local type, getmetatable, setmetatable, colours, str_write = type, getmetatable, setmetatable, colours, write | ||||
|  | ||||
| --- @{table.insert} alternative, but with the length stored inline. | ||||
| local function append(out, value) | ||||
|     local n = out.n + 1 | ||||
|     out[n], out.n = value, n | ||||
| end | ||||
|  | ||||
| --- A document containing formatted text, with multiple possible layouts. | ||||
| -- | ||||
| -- Documents effectively represent a sequence of strings in alternative layouts, | ||||
| -- which we will try to print in the most compact form necessary. | ||||
| -- | ||||
| -- @type Doc | ||||
| local Doc = { } | ||||
|  | ||||
| --- An empty document. | ||||
| local empty = setmetatable({ tag = "nil" }, Doc) | ||||
|  | ||||
| --- A document with a single space in it. | ||||
| local space = setmetatable({ tag = "text", text = " " }, Doc) | ||||
|  | ||||
| --- A line break. When collapsed with @{group}, this will be replaced with @{empty}. | ||||
| local line = setmetatable({ tag = "line", flat = empty }, Doc) | ||||
|  | ||||
| --- A line break. When collapsed with @{group}, this will be replaced with @{space}. | ||||
| local space_line = setmetatable({ tag = "line", flat = space }, Doc) | ||||
|  | ||||
| local text_cache = { [""] = empty, [" "] = space, ["\n"] = space_line } | ||||
|  | ||||
| local function mk_text(text, colour) | ||||
|     return text_cache[text] or setmetatable({ tag = "text", text = text, colour = colour }, Doc) | ||||
| end | ||||
|  | ||||
| --- Create a new document from a string. | ||||
| -- | ||||
| -- If your string contains multiple lines, @{group} will flatten the string | ||||
| -- into a single line, with spaces between each line. | ||||
| -- | ||||
| -- @tparam      string text   The string to construct a new document with. | ||||
| -- @tparam[opt] number colour The colour this text should be printed with. If not given, we default to the current | ||||
| -- colour. | ||||
| -- @treturn Doc The document with the provided text. | ||||
| local function text(text, colour) | ||||
|     expect(1, text, "string") | ||||
|     expect(2, colour, "number", "nil") | ||||
|  | ||||
|     local cached = text_cache[text] | ||||
|     if cached then return cached end | ||||
|  | ||||
|     local new_line = text:find("\n", 1) | ||||
|     if not new_line then return mk_text(text, colour) end | ||||
|  | ||||
|     -- Split the string by "\n". With a micro-optimisation to skip empty strings. | ||||
|     local doc = setmetatable({ tag = "concat", n = 0 }, Doc) | ||||
|     if new_line ~= 1 then append(doc, mk_text(text:sub(1, new_line - 1), colour)) end | ||||
|  | ||||
|     new_line = new_line + 1 | ||||
|     while true do | ||||
|         local next_line = text:find("\n", new_line) | ||||
|         append(doc, space_line) | ||||
|         if not next_line then | ||||
|             if new_line <= #text then append(doc, mk_text(text:sub(new_line), colour)) end | ||||
|             return doc | ||||
|         else | ||||
|             if new_line <= next_line - 1 then | ||||
|                 append(doc, mk_text(text:sub(new_line, next_line - 1), colour)) | ||||
|             end | ||||
|             new_line = next_line + 1 | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Concatenate several documents together. This behaves very similar to string concatenation. | ||||
| -- | ||||
| -- @tparam Doc|string ... The documents to concatenate. | ||||
| -- @treturn Doc The concatenated documents. | ||||
| -- @usage pretty.concat(doc1, " - ", doc2) | ||||
| -- @usage doc1 .. " - " .. doc2 | ||||
| local function concat(...) | ||||
|     local args = table.pack(...) | ||||
|     for i = 1, args.n do | ||||
|         if type(args[i]) == "string" then args[i] = text(args[i]) end | ||||
|         if getmetatable(args[i]) ~= Doc then expect(i, args[i], "document") end | ||||
|     end | ||||
|  | ||||
|     if args.n == 0 then return empty end | ||||
|     if args.n == 1 then return args[1] end | ||||
|  | ||||
|     args.tag = "concat" | ||||
|     return setmetatable(args, Doc) | ||||
| end | ||||
|  | ||||
| Doc.__concat = concat --- @local | ||||
|  | ||||
| --- Indent later lines of the given document with the given number of spaces. | ||||
| -- | ||||
| -- For instance, nesting the document | ||||
| -- ```txt | ||||
| -- foo | ||||
| -- bar | ||||
| -- ``` | ||||
| -- by two spaces will produce | ||||
| -- ```txt | ||||
| -- foo | ||||
| --   bar | ||||
| -- ``` | ||||
| -- | ||||
| -- @tparam number depth The number of spaces with which the document should be indented. | ||||
| -- @tparam Doc    doc   The document to indent. | ||||
| -- @treturn Doc The nested document. | ||||
| -- @usage pretty.nest(2, pretty.text("foo\nbar")) | ||||
| local function nest(depth, doc) | ||||
|     expect(1, depth, "number") | ||||
|     if getmetatable(doc) ~= Doc then expect(2, doc, "document") end | ||||
|     if depth <= 0 then error("depth must be a positive number", 2) end | ||||
|  | ||||
|     return setmetatable({ tag = "nest", depth = depth, doc }, Doc) | ||||
| end | ||||
|  | ||||
| local function flatten(doc) | ||||
|     if doc.flat then return doc.flat end | ||||
|  | ||||
|     local kind = doc.tag | ||||
|     if kind == "nil" or kind == "text" then | ||||
|         return doc | ||||
|     elseif kind == "concat" then | ||||
|         local out = setmetatable({ tag = "concat", n = doc.n }, Doc) | ||||
|         for i = 1, doc.n do out[i] = flatten(doc[i]) end | ||||
|         doc.flat, out.flat = out, out -- cache the flattened node | ||||
|         return out | ||||
|     elseif kind == "nest" then | ||||
|         return flatten(doc[1]) | ||||
|     elseif kind == "group" then | ||||
|         return doc[1] | ||||
|     else | ||||
|         error("Unknown doc " .. kind) | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Builds a document which is displayed on a single line if there is enough | ||||
| -- room, or as normal if not. | ||||
| -- | ||||
| -- @tparam Doc doc The document to group. | ||||
| -- @treturn Doc The grouped document. | ||||
| local function group(doc) | ||||
|     if getmetatable(doc) ~= Doc then expect(1, doc, "document") end | ||||
|  | ||||
|     if doc.tag == "group" then return doc end -- Skip if already grouped. | ||||
|  | ||||
|     local flattened = flatten(doc) | ||||
|     if flattened == doc then return doc end -- Also skip if flattening does nothing. | ||||
|     return setmetatable({ tag = "group", flattened, doc }, Doc) | ||||
| end | ||||
|  | ||||
| local function get_remaining(doc, width) | ||||
|     local kind = doc.tag | ||||
|     if kind == "nil" or kind == "line" then | ||||
|         return width | ||||
|     elseif kind == "text" then | ||||
|         return width - #doc.text | ||||
|     elseif kind == "concat" then | ||||
|         for i = 1, doc.n do | ||||
|             width = get_remaining(doc[i], width) | ||||
|             if width < 0 then break end | ||||
|         end | ||||
|         return width | ||||
|     elseif kind == "group" or kind == "nest" then | ||||
|         return get_remaining(kind[1]) | ||||
|     else | ||||
|         error("Unknown doc " .. kind) | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Display a document on the terminal. | ||||
| -- | ||||
| -- @tparam      Doc     doc         The document to render | ||||
| -- @tparam[opt] number  ribbon_frac The maximum fraction of the width that we should write in. | ||||
| local function write(doc, ribbon_frac) | ||||
|     if getmetatable(doc) ~= Doc then expect(1, doc, "document") end | ||||
|     expect(2, ribbon_frac, "number", "nil") | ||||
|  | ||||
|     local term = term | ||||
|     local width, height = term.getSize() | ||||
|     local ribbon_width = (ribbon_frac or 0.6) * width | ||||
|     if ribbon_width < 0 then ribbon_width = 0 end | ||||
|     if ribbon_width > width then ribbon_width = width end | ||||
|  | ||||
|     local def_colour = term.getTextColour() | ||||
|     local current_colour = def_colour | ||||
|  | ||||
|     local function go(doc, indent, col) | ||||
|         local kind = doc.tag | ||||
|         if kind == "nil" then | ||||
|             return col | ||||
|         elseif kind == "text" then | ||||
|             local doc_colour = doc.colour or def_colour | ||||
|             if doc_colour ~= current_colour then | ||||
|                 term.setTextColour(doc_colour) | ||||
|                 current_colour = doc_colour | ||||
|             end | ||||
|  | ||||
|             str_write(doc.text) | ||||
|  | ||||
|             return col + #doc.text | ||||
|         elseif kind == "line" then | ||||
|             local _, y = term.getCursorPos() | ||||
|             if y < height then | ||||
|                 term.setCursorPos(indent + 1, y + 1) | ||||
|             else | ||||
|                 term.scroll(1) | ||||
|                 term.setCursorPos(indent + 1, height) | ||||
|             end | ||||
|  | ||||
|             return indent | ||||
|         elseif kind == "concat" then | ||||
|             for i = 1, doc.n do col = go(doc[i], indent, col) end | ||||
|             return col | ||||
|         elseif kind == "nest" then | ||||
|             return go(doc[1], indent + doc.depth, col) | ||||
|         elseif kind == "group" then | ||||
|             if get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then | ||||
|                 return go(doc[1], indent, col) | ||||
|             else | ||||
|                 return go(doc[2], indent, col) | ||||
|             end | ||||
|         else | ||||
|             error("Unknown doc " .. kind) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local col = math.max(term.getCursorPos() - 1, 0) | ||||
|     go(doc, 0, col) | ||||
|     if current_colour ~= def_colour then term.setTextColour(def_colour) end | ||||
| end | ||||
|  | ||||
| --- Display a document on the terminal with a trailing new line. | ||||
| -- | ||||
| -- @tparam      Doc     doc         The document to render. | ||||
| -- @tparam[opt] number  ribbon_frac The maximum fraction of the width that we should write in. | ||||
| local function print(doc, ribbon_frac) | ||||
|     if getmetatable(doc) ~= Doc then expect(1, doc, "document") end | ||||
|     expect(2, ribbon_frac, "number", "nil") | ||||
|     write(doc, ribbon_frac) | ||||
|     str_write("\n") | ||||
| end | ||||
|  | ||||
| --- Render a document, converting it into a string. | ||||
| -- | ||||
| -- @tparam      Doc     doc         The document to render. | ||||
| -- @tparam[opt] number  width       The maximum width of this document. Note that long strings will not be wrapped to | ||||
| -- fit this width - it is only used for finding the best layout. | ||||
| -- @tparam[opt] number  ribbon_frac The maximum fraction of the width that we should write in. | ||||
| -- @treturn string The rendered document as a string. | ||||
| local function render(doc, width, ribbon_frac) | ||||
|     if getmetatable(doc) ~= Doc then expect(1, doc, "document") end | ||||
|     expect(2, width, "number", "nil") | ||||
|     expect(3, ribbon_frac, "number", "nil") | ||||
|  | ||||
|     local ribbon_width | ||||
|     if width then | ||||
|         ribbon_width = (ribbon_frac or 0.6) * width | ||||
|         if ribbon_width < 0 then ribbon_width = 0 end | ||||
|         if ribbon_width > width then ribbon_width = width end | ||||
|     end | ||||
|  | ||||
|     local out = { n = 0 } | ||||
|     local function go(doc, indent, col) | ||||
|         local kind = doc.tag | ||||
|         if kind == "nil" then | ||||
|             return col | ||||
|         elseif kind == "text" then | ||||
|             append(out, doc.text) | ||||
|             return col + #doc.text | ||||
|         elseif kind == "line" then | ||||
|             append(out, "\n" .. (" "):rep(indent)) | ||||
|             return indent | ||||
|         elseif kind == "concat" then | ||||
|             for i = 1, doc.n do col = go(doc[i], indent, col) end | ||||
|             return col | ||||
|         elseif kind == "nest" then | ||||
|             return go(doc[1], indent + doc.depth, col) | ||||
|         elseif kind == "group" then | ||||
|             if not width or get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then | ||||
|                 return go(doc[1], indent, col) | ||||
|             else | ||||
|                 return go(doc[2], indent, col) | ||||
|             end | ||||
|         else | ||||
|             error("Unknown doc " .. kind) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     go(doc, 0, 0) | ||||
|     return table.concat(out, "", 1, out.n) | ||||
| end | ||||
|  | ||||
| Doc.__tostring = render --- @local | ||||
|  | ||||
| local keywords = { | ||||
|     ["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true, | ||||
|     ["elseif"] = true, ["end"] = true, ["false"] = true, ["for"] = true, | ||||
|     ["function"] = true, ["if"] = true, ["in"] = true, ["local"] = true, | ||||
|     ["nil"] = true, ["not"] = true, ["or"] = true, ["repeat"] = true, ["return"] = true, | ||||
|     ["then"] = true, ["true"] = true, ["until"] = true, ["while"] = true, | ||||
|   } | ||||
|  | ||||
| local comma = text(",") | ||||
| local braces = text("{}") | ||||
| local obrace, cbrace = text("{"), text("}") | ||||
| local obracket, cbracket = text("["), text("] = ") | ||||
|  | ||||
| local function key_compare(a, b) | ||||
|     local ta, tb = type(a), type(b) | ||||
|  | ||||
|     if ta == "string" then return tb ~= "string" or a < b | ||||
|     elseif tb == "string" then return false | ||||
|     end | ||||
|  | ||||
|     if ta == "number" then return tb ~= "number" or a < b end | ||||
|     return false | ||||
| end | ||||
|  | ||||
| local function pretty_impl(obj, tracking) | ||||
|     local obj_type = type(obj) | ||||
|     if obj_type == "string" then | ||||
|         local formatted = ("%q"):format(obj):gsub("\\\n", "\\n") | ||||
|         return text(formatted, colours.red) | ||||
|     elseif obj_type == "number" then | ||||
|         return text(tostring(obj), colours.magenta) | ||||
|     elseif obj_type ~= "table" or tracking[obj] then | ||||
|         return text(tostring(obj), colours.lightGrey) | ||||
|     elseif getmetatable(obj) ~= nil and getmetatable(obj).__tostring then | ||||
|         return text(tostring(obj)) | ||||
|     elseif next(obj) == nil then | ||||
|         return braces | ||||
|     else | ||||
|         tracking[obj] = true | ||||
|         local doc = setmetatable({ tag = "concat", n = 1, space_line }, Doc) | ||||
|  | ||||
|         local length, keys, keysn = #obj, {}, 1 | ||||
|         for k in pairs(obj) do keys[keysn], keysn = k, keysn + 1 end | ||||
|         table.sort(keys, key_compare) | ||||
|  | ||||
|         for i = 1, keysn - 1 do | ||||
|             if i > 1 then append(doc, comma) append(doc, space_line) end | ||||
|  | ||||
|             local k = keys[i] | ||||
|             local v = obj[k] | ||||
|             local ty = type(k) | ||||
|             if ty == "number" and k % 1 == 0 and k >= 1 and k <= length then | ||||
|                 append(doc, pretty_impl(v, tracking)) | ||||
|             elseif ty == "string" and not keywords[k] and k:match("^[%a_][%a%d_]*$") then | ||||
|                 append(doc, text(k .. " = ")) | ||||
|                 append(doc, pretty_impl(v, tracking)) | ||||
|             else | ||||
|                 append(doc, obracket) | ||||
|                 append(doc, pretty_impl(k, tracking)) | ||||
|                 append(doc, cbracket) | ||||
|                 append(doc, pretty_impl(v, tracking)) | ||||
|             end | ||||
|         end | ||||
|  | ||||
|         tracking[obj] = nil | ||||
|         return group(concat(obrace, nest(2, concat(table.unpack(doc, 1, n))), space_line, cbrace)) | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Pretty-print an arbitrary object, converting it into a document. | ||||
| -- | ||||
| -- This can then be rendered with @{write} or @{print}. | ||||
| -- | ||||
| -- @param obj The object to pretty-print. | ||||
| -- @treturn Doc The object formatted as a document. | ||||
| -- @usage Display a table on the screen | ||||
| --     local pretty = require "cc.pretty" | ||||
| --     pretty.print(pretty.pretty({ 1, 2, 3 })) | ||||
| local function pretty(obj) | ||||
|     return pretty_impl(obj, {}) | ||||
| end | ||||
|  | ||||
| return { | ||||
|     empty = empty, | ||||
|     space = space, | ||||
|     line = line, | ||||
|     space_line = space_line, | ||||
|     text = text, | ||||
|     concat = concat, | ||||
|     nest = nest, | ||||
|     group = group, | ||||
|  | ||||
|     write = write, | ||||
|     print = print, | ||||
|     render = render, | ||||
|  | ||||
|     pretty = pretty, | ||||
| } | ||||
| @@ -97,7 +97,7 @@ end | ||||
| --     complete.build( | ||||
| --       { complete.choice, { "get", "put" } }, | ||||
| --       complete.dir, | ||||
| --       } complete.file, many = true } | ||||
| --       { complete.file, many = true } | ||||
| --     ) | ||||
| local function build(...) | ||||
|     local arguments = table.pack(...) | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
|  | ||||
| if not shell.openTab then | ||||
|     printError( "Requires multishell" ) | ||||
|     printError("Requires multishell") | ||||
|     return | ||||
| end | ||||
|  | ||||
| local tArgs = { ... } | ||||
| if #tArgs > 0 then | ||||
|     shell.openTab( table.unpack( tArgs ) ) | ||||
|     shell.openTab(table.unpack(tArgs)) | ||||
| else | ||||
|     shell.openTab( "shell" ) | ||||
|     shell.openTab("shell") | ||||
| end | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
|  | ||||
| if not shell.openTab then | ||||
|     printError( "Requires multishell" ) | ||||
|     printError("Requires multishell") | ||||
|     return | ||||
| end | ||||
|  | ||||
| local tArgs = { ... } | ||||
| if #tArgs > 0 then | ||||
|     local nTask = shell.openTab( table.unpack( tArgs ) ) | ||||
|     local nTask = shell.openTab(table.unpack(tArgs)) | ||||
|     if nTask then | ||||
|         shell.switchTab( nTask ) | ||||
|         shell.switchTab(nTask) | ||||
|     end | ||||
| else | ||||
|     local nTask = shell.openTab( "shell" ) | ||||
|     local nTask = shell.openTab("shell") | ||||
|     if nTask then | ||||
|         shell.switchTab( nTask ) | ||||
|         shell.switchTab(nTask) | ||||
|     end | ||||
| end | ||||
|   | ||||
| @@ -12,84 +12,84 @@ local bWindowsResized = false | ||||
| local nScrollPos = 1 | ||||
| local bScrollRight = false | ||||
|  | ||||
| local function selectProcess( n ) | ||||
| local function selectProcess(n) | ||||
|     if nCurrentProcess ~= n then | ||||
|         if nCurrentProcess then | ||||
|             local tOldProcess = tProcesses[ nCurrentProcess ] | ||||
|             tOldProcess.window.setVisible( false ) | ||||
|             local tOldProcess = tProcesses[nCurrentProcess] | ||||
|             tOldProcess.window.setVisible(false) | ||||
|         end | ||||
|         nCurrentProcess = n | ||||
|         if nCurrentProcess then | ||||
|             local tNewProcess = tProcesses[ nCurrentProcess ] | ||||
|             tNewProcess.window.setVisible( true ) | ||||
|             local tNewProcess = tProcesses[nCurrentProcess] | ||||
|             tNewProcess.window.setVisible(true) | ||||
|             tNewProcess.bInteracted = true | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function setProcessTitle( n, sTitle ) | ||||
|     tProcesses[ n ].sTitle = sTitle | ||||
| local function setProcessTitle(n, sTitle) | ||||
|     tProcesses[n].sTitle = sTitle | ||||
| end | ||||
|  | ||||
| local function resumeProcess( nProcess, sEvent, ... ) | ||||
|     local tProcess = tProcesses[ nProcess ] | ||||
| local function resumeProcess(nProcess, sEvent, ...) | ||||
|     local tProcess = tProcesses[nProcess] | ||||
|     local sFilter = tProcess.sFilter | ||||
|     if sFilter == nil or sFilter == sEvent or sEvent == "terminate" then | ||||
|         local nPreviousProcess = nRunningProcess | ||||
|         nRunningProcess = nProcess | ||||
|         term.redirect( tProcess.terminal ) | ||||
|         local ok, result = coroutine.resume( tProcess.co, sEvent, ... ) | ||||
|         term.redirect(tProcess.terminal) | ||||
|         local ok, result = coroutine.resume(tProcess.co, sEvent, ...) | ||||
|         tProcess.terminal = term.current() | ||||
|         if ok then | ||||
|             tProcess.sFilter = result | ||||
|         else | ||||
|             printError( result ) | ||||
|             printError(result) | ||||
|         end | ||||
|         nRunningProcess = nPreviousProcess | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function launchProcess( bFocus, tProgramEnv, sProgramPath, ... ) | ||||
|     local tProgramArgs = table.pack( ... ) | ||||
| local function launchProcess(bFocus, tProgramEnv, sProgramPath, ...) | ||||
|     local tProgramArgs = table.pack(...) | ||||
|     local nProcess = #tProcesses + 1 | ||||
|     local tProcess = {} | ||||
|     tProcess.sTitle = fs.getName( sProgramPath ) | ||||
|     tProcess.sTitle = fs.getName(sProgramPath) | ||||
|     if bShowMenu then | ||||
|         tProcess.window = window.create( parentTerm, 1, 2, w, h - 1, false ) | ||||
|         tProcess.window = window.create(parentTerm, 1, 2, w, h - 1, false) | ||||
|     else | ||||
|         tProcess.window = window.create( parentTerm, 1, 1, w, h, false ) | ||||
|         tProcess.window = window.create(parentTerm, 1, 1, w, h, false) | ||||
|     end | ||||
|     tProcess.co = coroutine.create( function() | ||||
|         os.run( tProgramEnv, sProgramPath, table.unpack( tProgramArgs, 1, tProgramArgs.n ) ) | ||||
|     tProcess.co = coroutine.create(function() | ||||
|         os.run(tProgramEnv, sProgramPath, table.unpack(tProgramArgs, 1, tProgramArgs.n)) | ||||
|         if not tProcess.bInteracted then | ||||
|             term.setCursorBlink( false ) | ||||
|             print( "Press any key to continue" ) | ||||
|             os.pullEvent( "char" ) | ||||
|             term.setCursorBlink(false) | ||||
|             print("Press any key to continue") | ||||
|             os.pullEvent("char") | ||||
|         end | ||||
|     end ) | ||||
|     end) | ||||
|     tProcess.sFilter = nil | ||||
|     tProcess.terminal = tProcess.window | ||||
|     tProcess.bInteracted = false | ||||
|     tProcesses[ nProcess ] = tProcess | ||||
|     tProcesses[nProcess] = tProcess | ||||
|     if bFocus then | ||||
|         selectProcess( nProcess ) | ||||
|         selectProcess(nProcess) | ||||
|     end | ||||
|     resumeProcess( nProcess ) | ||||
|     resumeProcess(nProcess) | ||||
|     return nProcess | ||||
| end | ||||
|  | ||||
| local function cullProcess( nProcess ) | ||||
|     local tProcess = tProcesses[ nProcess ] | ||||
|     if coroutine.status( tProcess.co ) == "dead" then | ||||
| local function cullProcess(nProcess) | ||||
|     local tProcess = tProcesses[nProcess] | ||||
|     if coroutine.status(tProcess.co) == "dead" then | ||||
|         if nCurrentProcess == nProcess then | ||||
|             selectProcess( nil ) | ||||
|             selectProcess(nil) | ||||
|         end | ||||
|         table.remove( tProcesses, nProcess ) | ||||
|         table.remove(tProcesses, nProcess) | ||||
|         if nCurrentProcess == nil then | ||||
|             if nProcess > 1 then | ||||
|                 selectProcess( nProcess - 1 ) | ||||
|                 selectProcess(nProcess - 1) | ||||
|             elseif #tProcesses > 0 then | ||||
|                 selectProcess( 1 ) | ||||
|                 selectProcess(1) | ||||
|             end | ||||
|         end | ||||
|         if nScrollPos ~= 1 then | ||||
| @@ -103,7 +103,7 @@ end | ||||
| local function cullProcesses() | ||||
|     local culled = false | ||||
|     for n = #tProcesses, 1, -1 do | ||||
|         culled = culled or cullProcess( n ) | ||||
|         culled = culled or cullProcess(n) | ||||
|     end | ||||
|     return culled | ||||
| end | ||||
| @@ -121,40 +121,40 @@ end | ||||
| local function redrawMenu() | ||||
|     if bShowMenu then | ||||
|         -- Draw menu | ||||
|         parentTerm.setCursorPos( 1, 1 ) | ||||
|         parentTerm.setBackgroundColor( menuOtherBgColor ) | ||||
|         parentTerm.setCursorPos(1, 1) | ||||
|         parentTerm.setBackgroundColor(menuOtherBgColor) | ||||
|         parentTerm.clearLine() | ||||
|         local nCharCount = 0 | ||||
|         local nSize = parentTerm.getSize() | ||||
|         if nScrollPos ~= 1 then | ||||
|             parentTerm.setTextColor( menuOtherTextColor ) | ||||
|             parentTerm.setBackgroundColor( menuOtherBgColor ) | ||||
|             parentTerm.write( "<" ) | ||||
|             parentTerm.setTextColor(menuOtherTextColor) | ||||
|             parentTerm.setBackgroundColor(menuOtherBgColor) | ||||
|             parentTerm.write("<") | ||||
|             nCharCount = 1 | ||||
|         end | ||||
|         for n = nScrollPos, #tProcesses do | ||||
|             if n == nCurrentProcess then | ||||
|                 parentTerm.setTextColor( menuMainTextColor ) | ||||
|                 parentTerm.setBackgroundColor( menuMainBgColor ) | ||||
|                 parentTerm.setTextColor(menuMainTextColor) | ||||
|                 parentTerm.setBackgroundColor(menuMainBgColor) | ||||
|             else | ||||
|                 parentTerm.setTextColor( menuOtherTextColor ) | ||||
|                 parentTerm.setBackgroundColor( menuOtherBgColor ) | ||||
|                 parentTerm.setTextColor(menuOtherTextColor) | ||||
|                 parentTerm.setBackgroundColor(menuOtherBgColor) | ||||
|             end | ||||
|             parentTerm.write( " " .. tProcesses[n].sTitle .. " " ) | ||||
|             parentTerm.write(" " .. tProcesses[n].sTitle .. " ") | ||||
|             nCharCount = nCharCount + #tProcesses[n].sTitle + 2 | ||||
|         end | ||||
|         if nCharCount > nSize then | ||||
|             parentTerm.setTextColor( menuOtherTextColor ) | ||||
|             parentTerm.setBackgroundColor( menuOtherBgColor ) | ||||
|             parentTerm.setCursorPos( nSize, 1 ) | ||||
|             parentTerm.write( ">" ) | ||||
|             parentTerm.setTextColor(menuOtherTextColor) | ||||
|             parentTerm.setBackgroundColor(menuOtherBgColor) | ||||
|             parentTerm.setCursorPos(nSize, 1) | ||||
|             parentTerm.write(">") | ||||
|             bScrollRight = true | ||||
|         else | ||||
|             bScrollRight = false | ||||
|         end | ||||
|  | ||||
|         -- Put the cursor back where it should be | ||||
|         local tProcess = tProcesses[ nCurrentProcess ] | ||||
|         local tProcess = tProcesses[nCurrentProcess] | ||||
|         if tProcess then | ||||
|             tProcess.window.restoreCursor() | ||||
|         end | ||||
| @@ -174,15 +174,15 @@ local function resizeWindows() | ||||
|         local tProcess = tProcesses[n] | ||||
|         local x, y = tProcess.window.getCursorPos() | ||||
|         if y > windowHeight then | ||||
|             tProcess.window.scroll( y - windowHeight ) | ||||
|             tProcess.window.setCursorPos( x, windowHeight ) | ||||
|             tProcess.window.scroll(y - windowHeight) | ||||
|             tProcess.window.setCursorPos(x, windowHeight) | ||||
|         end | ||||
|         tProcess.window.reposition( 1, windowY, w, windowHeight ) | ||||
|         tProcess.window.reposition(1, windowY, w, windowHeight) | ||||
|     end | ||||
|     bWindowsResized = true | ||||
| end | ||||
|  | ||||
| local function setMenuVisible( bVis ) | ||||
| local function setMenuVisible(bVis) | ||||
|     if bShowMenu ~= bVis then | ||||
|         bShowMenu = bVis | ||||
|         resizeWindows() | ||||
| @@ -196,17 +196,17 @@ function multishell.getFocus() | ||||
|     return nCurrentProcess | ||||
| end | ||||
|  | ||||
| function multishell.setFocus( n ) | ||||
| function multishell.setFocus(n) | ||||
|     expect(1, n, "number") | ||||
|     if n >= 1 and n <= #tProcesses then | ||||
|         selectProcess( n ) | ||||
|         selectProcess(n) | ||||
|         redrawMenu() | ||||
|         return true | ||||
|     end | ||||
|     return false | ||||
| end | ||||
|  | ||||
| function multishell.getTitle( n ) | ||||
| function multishell.getTitle(n) | ||||
|     expect(1, n, "number") | ||||
|     if n >= 1 and n <= #tProcesses then | ||||
|         return tProcesses[n].sTitle | ||||
| @@ -214,11 +214,11 @@ function multishell.getTitle( n ) | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| function multishell.setTitle( n, sTitle ) | ||||
| function multishell.setTitle(n, sTitle) | ||||
|     expect(1, n, "number") | ||||
|     expect(2, sTitle, "string") | ||||
|     if n >= 1 and n <= #tProcesses then | ||||
|         setProcessTitle( n, sTitle ) | ||||
|         setProcessTitle(n, sTitle) | ||||
|         redrawMenu() | ||||
|     end | ||||
| end | ||||
| @@ -227,14 +227,14 @@ function multishell.getCurrent() | ||||
|     return nRunningProcess | ||||
| end | ||||
|  | ||||
| function multishell.launch( tProgramEnv, sProgramPath, ... ) | ||||
| function multishell.launch(tProgramEnv, sProgramPath, ...) | ||||
|     expect(1, tProgramEnv, "table") | ||||
|     expect(2, sProgramPath, "string") | ||||
|     local previousTerm = term.current() | ||||
|     setMenuVisible( #tProcesses + 1 >= 2 ) | ||||
|     local nResult = launchProcess( false, tProgramEnv, sProgramPath, ... ) | ||||
|     setMenuVisible(#tProcesses + 1 >= 2) | ||||
|     local nResult = launchProcess(false, tProgramEnv, sProgramPath, ...) | ||||
|     redrawMenu() | ||||
|     term.redirect( previousTerm ) | ||||
|     term.redirect(previousTerm) | ||||
|     return nResult | ||||
| end | ||||
|  | ||||
| @@ -244,16 +244,16 @@ end | ||||
|  | ||||
| -- Begin | ||||
| parentTerm.clear() | ||||
| setMenuVisible( false ) | ||||
| launchProcess( true, { | ||||
| setMenuVisible(false) | ||||
| launchProcess(true, { | ||||
|     ["shell"] = shell, | ||||
|     ["multishell"] = multishell, | ||||
| }, "/rom/programs/shell.lua" ) | ||||
| }, "/rom/programs/shell.lua") | ||||
|  | ||||
| -- Run processes | ||||
| while #tProcesses > 0 do | ||||
|     -- Get the event | ||||
|     local tEventData = table.pack( os.pullEventRaw() ) | ||||
|     local tEventData = table.pack(os.pullEventRaw()) | ||||
|     local sEvent = tEventData[1] | ||||
|     if sEvent == "term_resize" then | ||||
|         -- Resize event | ||||
| @@ -264,9 +264,9 @@ while #tProcesses > 0 do | ||||
|     elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" then | ||||
|         -- Keyboard event | ||||
|         -- Passthrough to current process | ||||
|         resumeProcess( nCurrentProcess, table.unpack( tEventData, 1, tEventData.n ) ) | ||||
|         if cullProcess( nCurrentProcess ) then | ||||
|             setMenuVisible( #tProcesses >= 2 ) | ||||
|         resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n)) | ||||
|         if cullProcess(nCurrentProcess) then | ||||
|             setMenuVisible(#tProcesses >= 2) | ||||
|             redrawMenu() | ||||
|         end | ||||
|  | ||||
| @@ -289,7 +289,7 @@ while #tProcesses > 0 do | ||||
|                 for n = nScrollPos, #tProcesses do | ||||
|                     local tabEnd = tabStart + #tProcesses[n].sTitle + 1 | ||||
|                     if x >= tabStart and x <= tabEnd then | ||||
|                         selectProcess( n ) | ||||
|                         selectProcess(n) | ||||
|                         redrawMenu() | ||||
|                         break | ||||
|                     end | ||||
| @@ -298,9 +298,9 @@ while #tProcesses > 0 do | ||||
|             end | ||||
|         else | ||||
|             -- Passthrough to current process | ||||
|             resumeProcess( nCurrentProcess, sEvent, button, x, bShowMenu and y - 1 or y ) | ||||
|             if cullProcess( nCurrentProcess ) then | ||||
|                 setMenuVisible( #tProcesses >= 2 ) | ||||
|             resumeProcess(nCurrentProcess, sEvent, button, x, bShowMenu and y - 1 or y) | ||||
|             if cullProcess(nCurrentProcess) then | ||||
|                 setMenuVisible(#tProcesses >= 2) | ||||
|                 redrawMenu() | ||||
|             end | ||||
|         end | ||||
| @@ -318,9 +318,9 @@ while #tProcesses > 0 do | ||||
|             end | ||||
|         elseif not (bShowMenu and y == 1) then | ||||
|             -- Passthrough to current process | ||||
|             resumeProcess( nCurrentProcess, sEvent, p1, x, bShowMenu and y - 1 or y ) | ||||
|             if cullProcess( nCurrentProcess ) then | ||||
|                 setMenuVisible( #tProcesses >= 2 ) | ||||
|             resumeProcess(nCurrentProcess, sEvent, p1, x, bShowMenu and y - 1 or y) | ||||
|             if cullProcess(nCurrentProcess) then | ||||
|                 setMenuVisible(#tProcesses >= 2) | ||||
|                 redrawMenu() | ||||
|             end | ||||
|         end | ||||
| @@ -330,10 +330,10 @@ while #tProcesses > 0 do | ||||
|         -- Passthrough to all processes | ||||
|         local nLimit = #tProcesses -- Storing this ensures any new things spawned don't get the event | ||||
|         for n = 1, nLimit do | ||||
|             resumeProcess( n, table.unpack( tEventData, 1, tEventData.n ) ) | ||||
|             resumeProcess(n, table.unpack(tEventData, 1, tEventData.n)) | ||||
|         end | ||||
|         if cullProcesses() then | ||||
|             setMenuVisible( #tProcesses >= 2 ) | ||||
|             setMenuVisible(#tProcesses >= 2) | ||||
|             redrawMenu() | ||||
|         end | ||||
|     end | ||||
| @@ -342,15 +342,15 @@ while #tProcesses > 0 do | ||||
|         -- Pass term_resize to all processes | ||||
|         local nLimit = #tProcesses -- Storing this ensures any new things spawned don't get the event | ||||
|         for n = 1, nLimit do | ||||
|             resumeProcess( n, "term_resize" ) | ||||
|             resumeProcess(n, "term_resize") | ||||
|         end | ||||
|         bWindowsResized = false | ||||
|         if cullProcesses() then | ||||
|             setMenuVisible( #tProcesses >= 2 ) | ||||
|             setMenuVisible(#tProcesses >= 2) | ||||
|             redrawMenu() | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| -- Shutdown | ||||
| term.redirect( parentTerm ) | ||||
| term.redirect(parentTerm) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
|  | ||||
| local tArgs = { ... } | ||||
| if #tArgs > 2 then | ||||
|     print( "Usage: alias <alias> <program>" ) | ||||
|     print("Usage: alias <alias> <program>") | ||||
|     return | ||||
| end | ||||
|  | ||||
| @@ -10,17 +10,17 @@ local sProgram = tArgs[2] | ||||
|  | ||||
| if sAlias and sProgram then | ||||
|     -- Set alias | ||||
|     shell.setAlias( sAlias, sProgram ) | ||||
|     shell.setAlias(sAlias, sProgram) | ||||
| elseif sAlias then | ||||
|     -- Clear alias | ||||
|     shell.clearAlias( sAlias ) | ||||
|     shell.clearAlias(sAlias) | ||||
| else | ||||
|     -- List aliases | ||||
|     local tAliases = shell.aliases() | ||||
|     local tList = {} | ||||
|     for sAlias, sCommand in pairs( tAliases ) do | ||||
|         table.insert( tList, sAlias .. ":" .. sCommand ) | ||||
|     for sAlias, sCommand in pairs(tAliases) do | ||||
|         table.insert(tList, sAlias .. ":" .. sCommand) | ||||
|     end | ||||
|     table.sort( tList ) | ||||
|     textutils.pagedTabulate( tList ) | ||||
|     table.sort(tList) | ||||
|     textutils.pagedTabulate(tList) | ||||
| end | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
|  | ||||
| local tApis = {} | ||||
| for k, v in pairs( _G ) do | ||||
| for k, v in pairs(_G) do | ||||
|     if type(k) == "string" and type(v) == "table" and k ~= "_G" then | ||||
|         table.insert( tApis, k ) | ||||
|         table.insert(tApis, k) | ||||
|     end | ||||
| end | ||||
| table.insert( tApis, "shell" ) | ||||
| table.insert( tApis, "package" ) | ||||
| table.insert(tApis, "shell") | ||||
| table.insert(tApis, "package") | ||||
| if multishell then | ||||
|     table.insert( tApis, "multishell" ) | ||||
|     table.insert(tApis, "multishell") | ||||
| end | ||||
| table.sort( tApis ) | ||||
| table.sort(tApis) | ||||
|  | ||||
| textutils.pagedTabulate( tApis ) | ||||
| textutils.pagedTabulate(tApis) | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
|  | ||||
| local tArgs = { ... } | ||||
| if #tArgs < 1 then | ||||
|     print( "Usage: cd <path>" ) | ||||
|     print("Usage: cd <path>") | ||||
|     return | ||||
| end | ||||
|  | ||||
| local sNewDir = shell.resolve( tArgs[1] ) | ||||
| if fs.isDir( sNewDir ) then | ||||
|     shell.setDir( sNewDir ) | ||||
| local sNewDir = shell.resolve(tArgs[1]) | ||||
| if fs.isDir(sNewDir) then | ||||
|     shell.setDir(sNewDir) | ||||
| else | ||||
|     print( "Not a directory" ) | ||||
|     print("Not a directory") | ||||
|     return | ||||
| end | ||||
|   | ||||
| @@ -1,2 +1,2 @@ | ||||
| term.clear() | ||||
| term.setCursorPos( 1, 1 ) | ||||
| term.setCursorPos(1, 1) | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
|  | ||||
| if not commands then | ||||
|     printError( "Requires a Command Computer." ) | ||||
|     printError("Requires a Command Computer.") | ||||
|     return | ||||
| end | ||||
|  | ||||
| local tCommands = commands.list() | ||||
| table.sort( tCommands ) | ||||
| table.sort(tCommands) | ||||
|  | ||||
| if term.isColor() then | ||||
|     term.setTextColor( colors.green ) | ||||
|     term.setTextColor(colors.green) | ||||
| end | ||||
| print( "Available commands:" ) | ||||
| term.setTextColor( colors.white ) | ||||
| print("Available commands:") | ||||
| term.setTextColor(colors.white) | ||||
|  | ||||
| textutils.pagedTabulate( tCommands ) | ||||
| textutils.pagedTabulate(tCommands) | ||||
|   | ||||
| @@ -1,40 +1,40 @@ | ||||
|  | ||||
| local tArgs = { ... } | ||||
| if not commands then | ||||
|     printError( "Requires a Command Computer." ) | ||||
|     printError("Requires a Command Computer.") | ||||
|     return | ||||
| end | ||||
| if #tArgs == 0 then | ||||
|     printError( "Usage: exec <command>" ) | ||||
|     printError("Usage: exec <command>") | ||||
|     return | ||||
| end | ||||
|  | ||||
| local function printSuccess( text ) | ||||
| local function printSuccess(text) | ||||
|     if term.isColor() then | ||||
|         term.setTextColor( colors.green ) | ||||
|         term.setTextColor(colors.green) | ||||
|     end | ||||
|     print( text ) | ||||
|     term.setTextColor( colors.white ) | ||||
|     print(text) | ||||
|     term.setTextColor(colors.white) | ||||
| end | ||||
|  | ||||
| local sCommand = string.lower( tArgs[1] ) | ||||
| local sCommand = string.lower(tArgs[1]) | ||||
| for n = 2, #tArgs do | ||||
|     sCommand = sCommand .. " " .. tArgs[n] | ||||
| end | ||||
|  | ||||
| local bResult, tOutput = commands.exec( sCommand ) | ||||
| local bResult, tOutput = commands.exec(sCommand) | ||||
| if bResult then | ||||
|     printSuccess( "Success" ) | ||||
|     printSuccess("Success") | ||||
|     if #tOutput > 0 then | ||||
|         for n = 1, #tOutput do | ||||
|             print( tOutput[n] ) | ||||
|             print(tOutput[n]) | ||||
|         end | ||||
|     end | ||||
| else | ||||
|     printError( "Failed" ) | ||||
|     printError("Failed") | ||||
|     if #tOutput > 0 then | ||||
|         for n = 1, #tOutput do | ||||
|             print( tOutput[n] ) | ||||
|             print(tOutput[n]) | ||||
|         end | ||||
|     end | ||||
| end | ||||
|   | ||||
| @@ -1,32 +1,32 @@ | ||||
|  | ||||
| local tArgs = { ... } | ||||
| if #tArgs < 2 then | ||||
|     print( "Usage: cp <source> <destination>" ) | ||||
|     print("Usage: cp <source> <destination>") | ||||
|     return | ||||
| end | ||||
|  | ||||
| local sSource = shell.resolve( tArgs[1] ) | ||||
| local sDest = shell.resolve( tArgs[2] ) | ||||
| local tFiles = fs.find( sSource ) | ||||
| local sSource = shell.resolve(tArgs[1]) | ||||
| local sDest = shell.resolve(tArgs[2]) | ||||
| local tFiles = fs.find(sSource) | ||||
| if #tFiles > 0 then | ||||
|     for _, sFile in ipairs( tFiles ) do | ||||
|         if fs.isDir( sDest ) then | ||||
|             fs.copy( sFile, fs.combine( sDest, fs.getName(sFile) ) ) | ||||
|     for _, sFile in ipairs(tFiles) do | ||||
|         if fs.isDir(sDest) then | ||||
|             fs.copy(sFile, fs.combine(sDest, fs.getName(sFile))) | ||||
|         elseif #tFiles == 1 then | ||||
|             if fs.exists( sDest ) then | ||||
|                  printError( "Destination exists" ) | ||||
|             elseif fs.isReadOnly( sDest ) then | ||||
|                 printError( "Destination is read-only" ) | ||||
|             elseif fs.getFreeSpace( sDest ) < fs.getSize( sFile ) then | ||||
|                 printError( "Not enough space" ) | ||||
|             if fs.exists(sDest) then | ||||
|                  printError("Destination exists") | ||||
|             elseif fs.isReadOnly(sDest) then | ||||
|                 printError("Destination is read-only") | ||||
|             elseif fs.getFreeSpace(sDest) < fs.getSize(sFile) then | ||||
|                 printError("Not enough space") | ||||
|             else | ||||
|                  fs.copy( sFile, sDest ) | ||||
|                  fs.copy(sFile, sDest) | ||||
|             end | ||||
|         else | ||||
|             printError( "Cannot overwrite file multiple times" ) | ||||
|             printError("Cannot overwrite file multiple times") | ||||
|             return | ||||
|         end | ||||
|     end | ||||
| else | ||||
|     printError( "No matching files" ) | ||||
|     printError("No matching files") | ||||
| end | ||||
|   | ||||
| @@ -3,19 +3,19 @@ local tArgs = { ... } | ||||
| -- Get where a directory is mounted | ||||
| local sPath = shell.dir() | ||||
| if tArgs[1] ~= nil then | ||||
|     sPath = shell.resolve( tArgs[1] ) | ||||
|     sPath = shell.resolve(tArgs[1]) | ||||
| end | ||||
|  | ||||
| if fs.exists( sPath ) then | ||||
|     write( fs.getDrive( sPath ) .. " (" ) | ||||
|     local nSpace = fs.getFreeSpace( sPath ) | ||||
| if fs.exists(sPath) then | ||||
|     write(fs.getDrive(sPath) .. " (") | ||||
|     local nSpace = fs.getFreeSpace(sPath) | ||||
|     if nSpace >= 1000 * 1000 then | ||||
|         print( math.floor( nSpace / (100 * 1000) ) / 10 .. "MB remaining)" ) | ||||
|         print(math.floor(nSpace / (100 * 1000)) / 10 .. "MB remaining)") | ||||
|     elseif nSpace >= 1000 then | ||||
|         print( math.floor( nSpace / 100 ) / 10 .. "KB remaining)" ) | ||||
|         print(math.floor(nSpace / 100) / 10 .. "KB remaining)") | ||||
|     else | ||||
|         print( nSpace .. "B remaining)" ) | ||||
|         print(nSpace .. "B remaining)") | ||||
|     end | ||||
| else | ||||
|     print( "No such path" ) | ||||
|     print("No such path") | ||||
| end | ||||
|   | ||||
| @@ -1,22 +1,22 @@ | ||||
| -- Get file to edit | ||||
| local tArgs = { ... } | ||||
| if #tArgs == 0 then | ||||
|     print( "Usage: edit <path>" ) | ||||
|     print("Usage: edit <path>") | ||||
|     return | ||||
| end | ||||
|  | ||||
| -- Error checking | ||||
| local sPath = shell.resolve( tArgs[1] ) | ||||
| local bReadOnly = fs.isReadOnly( sPath ) | ||||
| if fs.exists( sPath ) and fs.isDir( sPath ) then | ||||
|     print( "Cannot edit a directory." ) | ||||
| local sPath = shell.resolve(tArgs[1]) | ||||
| local bReadOnly = fs.isReadOnly(sPath) | ||||
| if fs.exists(sPath) and fs.isDir(sPath) then | ||||
|     print("Cannot edit a directory.") | ||||
|     return | ||||
| end | ||||
|  | ||||
| -- Create .lua files by default | ||||
| if not fs.exists( sPath ) and not string.find( sPath, "%." ) then | ||||
|     local sExtension = settings.get("edit.default_extension", "" ) | ||||
|     if sExtension ~= "" and type( sExtension ) == "string" then | ||||
| if not fs.exists(sPath) and not string.find(sPath, "%.") then | ||||
|     local sExtension = settings.get("edit.default_extension", "") | ||||
|     if sExtension ~= "" and type(sExtension) == "string" then | ||||
|         sPath = sPath .. "." .. sExtension | ||||
|     end | ||||
| end | ||||
| @@ -51,59 +51,59 @@ local bMenu = false | ||||
| local nMenuItem = 1 | ||||
| local tMenuItems = {} | ||||
| if not bReadOnly then | ||||
|     table.insert( tMenuItems, "Save" ) | ||||
|     table.insert(tMenuItems, "Save") | ||||
| end | ||||
| if shell.openTab then | ||||
|     table.insert( tMenuItems, "Run" ) | ||||
|     table.insert(tMenuItems, "Run") | ||||
| end | ||||
| if peripheral.find( "printer" ) then | ||||
|     table.insert( tMenuItems, "Print" ) | ||||
| if peripheral.find("printer") then | ||||
|     table.insert(tMenuItems, "Print") | ||||
| end | ||||
| table.insert( tMenuItems, "Exit" ) | ||||
| table.insert(tMenuItems, "Exit") | ||||
|  | ||||
| local sStatus = "Press Ctrl to access menu" | ||||
| if #sStatus > w - 5 then | ||||
|     sStatus = "Press Ctrl for menu" | ||||
| end | ||||
|  | ||||
| local function load( _sPath ) | ||||
| local function load(_sPath) | ||||
|     tLines = {} | ||||
|     if fs.exists( _sPath ) then | ||||
|         local file = io.open( _sPath, "r" ) | ||||
|     if fs.exists(_sPath) then | ||||
|         local file = io.open(_sPath, "r") | ||||
|         local sLine = file:read() | ||||
|         while sLine do | ||||
|             table.insert( tLines, sLine ) | ||||
|             table.insert(tLines, sLine) | ||||
|             sLine = file:read() | ||||
|         end | ||||
|         file:close() | ||||
|     end | ||||
|  | ||||
|     if #tLines == 0 then | ||||
|         table.insert( tLines, "" ) | ||||
|         table.insert(tLines, "") | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function save( _sPath ) | ||||
| local function save(_sPath) | ||||
|     -- Create intervening folder | ||||
|     local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len() ) | ||||
|     if not fs.exists( sDir ) then | ||||
|         fs.makeDir( sDir ) | ||||
|     local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len()) | ||||
|     if not fs.exists(sDir) then | ||||
|         fs.makeDir(sDir) | ||||
|     end | ||||
|  | ||||
|     -- Save | ||||
|     local file, fileerr | ||||
|     local function innerSave() | ||||
|         file, fileerr = fs.open( _sPath, "w" ) | ||||
|         file, fileerr = fs.open(_sPath, "w") | ||||
|         if file then | ||||
|             for _, sLine in ipairs( tLines ) do | ||||
|                 file.write( sLine .. "\n" ) | ||||
|             for _, sLine in ipairs(tLines) do | ||||
|                 file.write(sLine .. "\n") | ||||
|             end | ||||
|         else | ||||
|             error( "Failed to open " .. _sPath ) | ||||
|             error("Failed to open " .. _sPath) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local ok, err = pcall( innerSave ) | ||||
|     local ok, err = pcall(innerSave) | ||||
|     if file then | ||||
|         file.close() | ||||
|     end | ||||
| @@ -134,38 +134,38 @@ local tKeywords = { | ||||
|     ["while"] = true, | ||||
| } | ||||
|  | ||||
| local function tryWrite( sLine, regex, colour ) | ||||
|     local match = string.match( sLine, regex ) | ||||
| local function tryWrite(sLine, regex, colour) | ||||
|     local match = string.match(sLine, regex) | ||||
|     if match then | ||||
|         if type(colour) == "number" then | ||||
|             term.setTextColour( colour ) | ||||
|             term.setTextColour(colour) | ||||
|         else | ||||
|             term.setTextColour( colour(match) ) | ||||
|             term.setTextColour(colour(match)) | ||||
|         end | ||||
|         term.write( match ) | ||||
|         term.setTextColour( textColour ) | ||||
|         return string.sub( sLine, #match + 1 ) | ||||
|         term.write(match) | ||||
|         term.setTextColour(textColour) | ||||
|         return string.sub(sLine, #match + 1) | ||||
|     end | ||||
|     return nil | ||||
| end | ||||
|  | ||||
| local function writeHighlighted( sLine ) | ||||
| local function writeHighlighted(sLine) | ||||
|     while #sLine > 0 do | ||||
|         sLine = | ||||
|             tryWrite( sLine, "^%-%-%[%[.-%]%]", commentColour ) or | ||||
|             tryWrite( sLine, "^%-%-.*", commentColour ) or | ||||
|             tryWrite( sLine, "^\"\"", stringColour ) or | ||||
|             tryWrite( sLine, "^\".-[^\\]\"", stringColour ) or | ||||
|             tryWrite( sLine, "^\'\'", stringColour ) or | ||||
|             tryWrite( sLine, "^\'.-[^\\]\'", stringColour ) or | ||||
|             tryWrite( sLine, "^%[%[.-%]%]", stringColour ) or | ||||
|             tryWrite( sLine, "^[%w_]+", function( match ) | ||||
|                 if tKeywords[ match ] then | ||||
|             tryWrite(sLine, "^%-%-%[%[.-%]%]", commentColour) or | ||||
|             tryWrite(sLine, "^%-%-.*", commentColour) or | ||||
|             tryWrite(sLine, "^\"\"", stringColour) or | ||||
|             tryWrite(sLine, "^\".-[^\\]\"", stringColour) or | ||||
|             tryWrite(sLine, "^\'\'", stringColour) or | ||||
|             tryWrite(sLine, "^\'.-[^\\]\'", stringColour) or | ||||
|             tryWrite(sLine, "^%[%[.-%]%]", stringColour) or | ||||
|             tryWrite(sLine, "^[%w_]+", function(match) | ||||
|                 if tKeywords[match] then | ||||
|                     return keywordColour | ||||
|                 end | ||||
|                 return textColour | ||||
|             end ) or | ||||
|             tryWrite( sLine, "^[^%w_]", textColour ) | ||||
|             end) or | ||||
|             tryWrite(sLine, "^[^%w_]", textColour) | ||||
|     end | ||||
| end | ||||
|  | ||||
| @@ -173,14 +173,14 @@ local tCompletions | ||||
| local nCompletion | ||||
|  | ||||
| local tCompleteEnv = _ENV | ||||
| local function complete( sLine ) | ||||
|     if settings.get( "edit.autocomplete" ) then | ||||
|         local nStartPos = string.find( sLine, "[a-zA-Z0-9_%.:]+$" ) | ||||
| local function complete(sLine) | ||||
|     if settings.get("edit.autocomplete") then | ||||
|         local nStartPos = string.find(sLine, "[a-zA-Z0-9_%.:]+$") | ||||
|         if nStartPos then | ||||
|             sLine = string.sub( sLine, nStartPos ) | ||||
|             sLine = string.sub(sLine, nStartPos) | ||||
|         end | ||||
|         if #sLine > 0 then | ||||
|             return textutils.complete( sLine, tCompleteEnv ) | ||||
|             return textutils.complete(sLine, tCompleteEnv) | ||||
|         end | ||||
|     end | ||||
|     return nil | ||||
| @@ -189,7 +189,7 @@ end | ||||
| local function recomplete() | ||||
|     local sLine = tLines[y] | ||||
|     if not bMenu and not bReadOnly and x == #sLine + 1 then | ||||
|         tCompletions = complete( sLine ) | ||||
|         tCompletions = complete(sLine) | ||||
|         if tCompletions and #tCompletions > 0 then | ||||
|             nCompletion = 1 | ||||
|         else | ||||
| @@ -201,85 +201,85 @@ local function recomplete() | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function writeCompletion( sLine ) | ||||
| local function writeCompletion(sLine) | ||||
|     if nCompletion then | ||||
|         local sCompletion = tCompletions[ nCompletion ] | ||||
|         term.setTextColor( colours.white ) | ||||
|         term.setBackgroundColor( colours.grey ) | ||||
|         term.write( sCompletion ) | ||||
|         term.setTextColor( textColour ) | ||||
|         term.setBackgroundColor( bgColour ) | ||||
|         local sCompletion = tCompletions[nCompletion] | ||||
|         term.setTextColor(colours.white) | ||||
|         term.setBackgroundColor(colours.grey) | ||||
|         term.write(sCompletion) | ||||
|         term.setTextColor(textColour) | ||||
|         term.setBackgroundColor(bgColour) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function redrawText() | ||||
|     local cursorX, cursorY = x, y | ||||
|     for y = 1, h - 1 do | ||||
|         term.setCursorPos( 1 - scrollX, y ) | ||||
|         term.setCursorPos(1 - scrollX, y) | ||||
|         term.clearLine() | ||||
|  | ||||
|         local sLine = tLines[ y + scrollY ] | ||||
|         local sLine = tLines[y + scrollY] | ||||
|         if sLine ~= nil then | ||||
|             writeHighlighted( sLine ) | ||||
|             writeHighlighted(sLine) | ||||
|             if cursorY == y and cursorX == #sLine + 1 then | ||||
|                 writeCompletion() | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|     term.setCursorPos( x - scrollX, y - scrollY ) | ||||
|     term.setCursorPos(x - scrollX, y - scrollY) | ||||
| end | ||||
|  | ||||
| local function redrawLine(_nY) | ||||
|     local sLine = tLines[_nY] | ||||
|     if sLine then | ||||
|         term.setCursorPos( 1 - scrollX, _nY - scrollY ) | ||||
|         term.setCursorPos(1 - scrollX, _nY - scrollY) | ||||
|         term.clearLine() | ||||
|         writeHighlighted( sLine ) | ||||
|         writeHighlighted(sLine) | ||||
|         if _nY == y and x == #sLine + 1 then | ||||
|             writeCompletion() | ||||
|         end | ||||
|         term.setCursorPos( x - scrollX, _nY - scrollY ) | ||||
|         term.setCursorPos(x - scrollX, _nY - scrollY) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function redrawMenu() | ||||
|     -- Clear line | ||||
|     term.setCursorPos( 1, h ) | ||||
|     term.setCursorPos(1, h) | ||||
|     term.clearLine() | ||||
|  | ||||
|     -- Draw line numbers | ||||
|     term.setCursorPos( w - #( "Ln " .. y ) + 1, h ) | ||||
|     term.setTextColour( highlightColour ) | ||||
|     term.write( "Ln " ) | ||||
|     term.setTextColour( textColour ) | ||||
|     term.write( y ) | ||||
|     term.setCursorPos(w - #("Ln " .. y) + 1, h) | ||||
|     term.setTextColour(highlightColour) | ||||
|     term.write("Ln ") | ||||
|     term.setTextColour(textColour) | ||||
|     term.write(y) | ||||
|  | ||||
|     term.setCursorPos( 1, h ) | ||||
|     term.setCursorPos(1, h) | ||||
|     if bMenu then | ||||
|         -- Draw menu | ||||
|         term.setTextColour( textColour ) | ||||
|         for nItem, sItem in pairs( tMenuItems ) do | ||||
|         term.setTextColour(textColour) | ||||
|         for nItem, sItem in pairs(tMenuItems) do | ||||
|             if nItem == nMenuItem then | ||||
|                 term.setTextColour( highlightColour ) | ||||
|                 term.write( "[" ) | ||||
|                 term.setTextColour( textColour ) | ||||
|                 term.write( sItem ) | ||||
|                 term.setTextColour( highlightColour ) | ||||
|                 term.write( "]" ) | ||||
|                 term.setTextColour( textColour ) | ||||
|                 term.setTextColour(highlightColour) | ||||
|                 term.write("[") | ||||
|                 term.setTextColour(textColour) | ||||
|                 term.write(sItem) | ||||
|                 term.setTextColour(highlightColour) | ||||
|                 term.write("]") | ||||
|                 term.setTextColour(textColour) | ||||
|             else | ||||
|                 term.write( " " .. sItem .. " " ) | ||||
|                 term.write(" " .. sItem .. " ") | ||||
|             end | ||||
|         end | ||||
|     else | ||||
|         -- Draw status | ||||
|         term.setTextColour( highlightColour ) | ||||
|         term.write( sStatus ) | ||||
|         term.setTextColour( textColour ) | ||||
|         term.setTextColour(highlightColour) | ||||
|         term.write(sStatus) | ||||
|         term.setTextColour(textColour) | ||||
|     end | ||||
|  | ||||
|     -- Reset cursor | ||||
|     term.setCursorPos( x - scrollX, y - scrollY ) | ||||
|     term.setCursorPos(x - scrollX, y - scrollY) | ||||
| end | ||||
|  | ||||
| local tMenuFuncs = { | ||||
| @@ -287,7 +287,7 @@ local tMenuFuncs = { | ||||
|         if bReadOnly then | ||||
|             sStatus = "Access denied" | ||||
|         else | ||||
|             local ok, _, fileerr  = save( sPath ) | ||||
|             local ok, _, fileerr  = save(sPath) | ||||
|             if ok then | ||||
|                 sStatus = "Saved to " .. sPath | ||||
|             else | ||||
| @@ -301,14 +301,14 @@ local tMenuFuncs = { | ||||
|         redrawMenu() | ||||
|     end, | ||||
|     Print = function() | ||||
|         local printer = peripheral.find( "printer" ) | ||||
|         local printer = peripheral.find("printer") | ||||
|         if not printer then | ||||
|             sStatus = "No printer attached" | ||||
|             return | ||||
|         end | ||||
|  | ||||
|         local nPage = 0 | ||||
|         local sName = fs.getName( sPath ) | ||||
|         local sName = fs.getName(sPath) | ||||
|         if printer.getInkLevel() < 1 then | ||||
|             sStatus = "Printer out of ink" | ||||
|             return | ||||
| @@ -326,7 +326,7 @@ local tMenuFuncs = { | ||||
|         } | ||||
|         printerTerminal.scroll = function() | ||||
|             if nPage == 1 then | ||||
|                 printer.setPageTitle( sName .. " (page " .. nPage .. ")" ) | ||||
|                 printer.setPageTitle(sName .. " (page " .. nPage .. ")") | ||||
|             end | ||||
|  | ||||
|             while not printer.newPage() do | ||||
| @@ -338,38 +338,38 @@ local tMenuFuncs = { | ||||
|                     sStatus = "Printer output tray full, please empty" | ||||
|                 end | ||||
|  | ||||
|                 term.redirect( screenTerminal ) | ||||
|                 term.redirect(screenTerminal) | ||||
|                 redrawMenu() | ||||
|                 term.redirect( printerTerminal ) | ||||
|                 term.redirect(printerTerminal) | ||||
|  | ||||
|                 sleep(0.5) | ||||
|             end | ||||
|  | ||||
|             nPage = nPage + 1 | ||||
|             if nPage == 1 then | ||||
|                 printer.setPageTitle( sName ) | ||||
|                 printer.setPageTitle(sName) | ||||
|             else | ||||
|                 printer.setPageTitle( sName .. " (page " .. nPage .. ")" ) | ||||
|                 printer.setPageTitle(sName .. " (page " .. nPage .. ")") | ||||
|             end | ||||
|         end | ||||
|  | ||||
|         bMenu = false | ||||
|         term.redirect( printerTerminal ) | ||||
|         local ok, error = pcall( function() | ||||
|         term.redirect(printerTerminal) | ||||
|         local ok, error = pcall(function() | ||||
|             term.scroll() | ||||
|             for _, sLine in ipairs( tLines ) do | ||||
|                 print( sLine ) | ||||
|             for _, sLine in ipairs(tLines) do | ||||
|                 print(sLine) | ||||
|             end | ||||
|         end ) | ||||
|         term.redirect( screenTerminal ) | ||||
|         end) | ||||
|         term.redirect(screenTerminal) | ||||
|         if not ok then | ||||
|             print( error ) | ||||
|             print(error) | ||||
|         end | ||||
|  | ||||
|         while not printer.endPage() do | ||||
|             sStatus = "Printer output tray full, please empty" | ||||
|             redrawMenu() | ||||
|             sleep( 0.5 ) | ||||
|             sleep(0.5) | ||||
|         end | ||||
|         bMenu = true | ||||
|  | ||||
| @@ -385,15 +385,15 @@ local tMenuFuncs = { | ||||
|     end, | ||||
|     Run = function() | ||||
|         local sTempPath = "/.temp" | ||||
|         local ok = save( sTempPath ) | ||||
|         local ok = save(sTempPath) | ||||
|         if ok then | ||||
|             local nTask = shell.openTab( sTempPath ) | ||||
|             local nTask = shell.openTab(sTempPath) | ||||
|             if nTask then | ||||
|                 shell.switchTab( nTask ) | ||||
|                 shell.switchTab(nTask) | ||||
|             else | ||||
|                 sStatus = "Error starting Task" | ||||
|             end | ||||
|             fs.delete( sTempPath ) | ||||
|             fs.delete(sTempPath) | ||||
|         else | ||||
|             sStatus = "Error saving to " .. sTempPath | ||||
|         end | ||||
| @@ -401,16 +401,16 @@ local tMenuFuncs = { | ||||
|     end, | ||||
| } | ||||
|  | ||||
| local function doMenuItem( _n ) | ||||
| local function doMenuItem(_n) | ||||
|     tMenuFuncs[tMenuItems[_n]]() | ||||
|     if bMenu then | ||||
|         bMenu = false | ||||
|         term.setCursorBlink( true ) | ||||
|         term.setCursorBlink(true) | ||||
|     end | ||||
|     redrawMenu() | ||||
| end | ||||
|  | ||||
| local function setCursor( newX, newY ) | ||||
| local function setCursor(newX, newY) | ||||
|     local _, oldY = x, y | ||||
|     x, y = newX, newY | ||||
|     local screenX = x - scrollX | ||||
| @@ -441,12 +441,12 @@ local function setCursor( newX, newY ) | ||||
|     if bRedraw then | ||||
|         redrawText() | ||||
|     elseif y ~= oldY then | ||||
|         redrawLine( oldY ) | ||||
|         redrawLine( y ) | ||||
|         redrawLine(oldY) | ||||
|         redrawLine(y) | ||||
|     else | ||||
|         redrawLine( y ) | ||||
|         redrawLine(y) | ||||
|     end | ||||
|     term.setCursorPos( screenX, screenY ) | ||||
|     term.setCursorPos(screenX, screenY) | ||||
|  | ||||
|     redrawMenu() | ||||
| end | ||||
| @@ -454,10 +454,10 @@ end | ||||
| -- Actual program functionality begins | ||||
| load(sPath) | ||||
|  | ||||
| term.setBackgroundColour( bgColour ) | ||||
| term.setBackgroundColour(bgColour) | ||||
| term.clear() | ||||
| term.setCursorPos(x, y) | ||||
| term.setCursorBlink( true ) | ||||
| term.setCursorBlink(true) | ||||
|  | ||||
| recomplete() | ||||
| redrawText() | ||||
| @@ -466,9 +466,9 @@ redrawMenu() | ||||
| local function acceptCompletion() | ||||
|     if nCompletion then | ||||
|         -- Append the completion | ||||
|         local sCompletion = tCompletions[ nCompletion ] | ||||
|         local sCompletion = tCompletions[nCompletion] | ||||
|         tLines[y] = tLines[y] .. sCompletion | ||||
|         setCursor( x + #sCompletion , y ) | ||||
|         setCursor(x + #sCompletion , y) | ||||
|     end | ||||
| end | ||||
|  | ||||
| @@ -490,7 +490,7 @@ while bRunning do | ||||
|                 elseif y > 1 then | ||||
|                     -- Move cursor up | ||||
|                     setCursor( | ||||
|                         math.min( x, #tLines[y - 1] + 1 ), | ||||
|                         math.min(x, #tLines[y - 1] + 1), | ||||
|                         y - 1 | ||||
|                     ) | ||||
|                 end | ||||
| @@ -511,7 +511,7 @@ while bRunning do | ||||
|                 elseif y < #tLines then | ||||
|                     -- Move cursor down | ||||
|                     setCursor( | ||||
|                         math.min( x, #tLines[y + 1] + 1 ), | ||||
|                         math.min(x, #tLines[y + 1] + 1), | ||||
|                         y + 1 | ||||
|                     ) | ||||
|                 end | ||||
| @@ -527,7 +527,7 @@ while bRunning do | ||||
|                     -- Indent line | ||||
|                     local sLine = tLines[y] | ||||
|                     tLines[y] = string.sub(sLine, 1, x - 1) .. "    " .. string.sub(sLine, x) | ||||
|                     setCursor( x + 4, y ) | ||||
|                     setCursor(x + 4, y) | ||||
|                 end | ||||
|             end | ||||
|  | ||||
| @@ -542,7 +542,7 @@ while bRunning do | ||||
|                     newY = 1 | ||||
|                 end | ||||
|                 setCursor( | ||||
|                     math.min( x, #tLines[newY] + 1 ), | ||||
|                     math.min(x, #tLines[newY] + 1), | ||||
|                     newY | ||||
|                 ) | ||||
|             end | ||||
| @@ -557,8 +557,8 @@ while bRunning do | ||||
|                 else | ||||
|                     newY = #tLines | ||||
|                 end | ||||
|                 local newX = math.min( x, #tLines[newY] + 1 ) | ||||
|                 setCursor( newX, newY ) | ||||
|                 local newX = math.min(x, #tLines[newY] + 1) | ||||
|                 setCursor(newX, newY) | ||||
|             end | ||||
|  | ||||
|         elseif param == keys.home then | ||||
| @@ -576,7 +576,7 @@ while bRunning do | ||||
|                 -- Move cursor to the end | ||||
|                 local nLimit = #tLines[y] + 1 | ||||
|                 if x < nLimit then | ||||
|                     setCursor( nLimit, y ) | ||||
|                     setCursor(nLimit, y) | ||||
|                 end | ||||
|             end | ||||
|  | ||||
| @@ -585,9 +585,9 @@ while bRunning do | ||||
|             if not bMenu then | ||||
|                 if x > 1 then | ||||
|                     -- Move cursor left | ||||
|                     setCursor( x - 1, y ) | ||||
|                     setCursor(x - 1, y) | ||||
|                 elseif x == 1 and y > 1 then | ||||
|                     setCursor( #tLines[y - 1] + 1, y - 1 ) | ||||
|                     setCursor(#tLines[y - 1] + 1, y - 1) | ||||
|                 end | ||||
|             else | ||||
|                 -- Move menu left | ||||
| @@ -604,13 +604,13 @@ while bRunning do | ||||
|                 local nLimit = #tLines[y] + 1 | ||||
|                 if x < nLimit then | ||||
|                     -- Move cursor right | ||||
|                     setCursor( x + 1, y ) | ||||
|                     setCursor(x + 1, y) | ||||
|                 elseif nCompletion and x == #tLines[y] + 1 then | ||||
|                     -- Accept autocomplete | ||||
|                     acceptCompletion() | ||||
|                 elseif x == nLimit and y < #tLines then | ||||
|                     -- Go to next line | ||||
|                     setCursor( 1, y + 1 ) | ||||
|                     setCursor(1, y + 1) | ||||
|                 end | ||||
|             else | ||||
|                 -- Move menu right | ||||
| @@ -632,7 +632,7 @@ while bRunning do | ||||
|                     redrawLine(y) | ||||
|                 elseif y < #tLines then | ||||
|                     tLines[y] = tLines[y] .. tLines[y + 1] | ||||
|                     table.remove( tLines, y + 1 ) | ||||
|                     table.remove(tLines, y + 1) | ||||
|                     recomplete() | ||||
|                     redrawText() | ||||
|                 end | ||||
| @@ -646,17 +646,17 @@ while bRunning do | ||||
|                     local sLine = tLines[y] | ||||
|                     if x > 4 and string.sub(sLine, x - 4, x - 1) == "    " and not string.sub(sLine, 1, x - 1):find("%S") then | ||||
|                         tLines[y] = string.sub(sLine, 1, x - 5) .. string.sub(sLine, x) | ||||
|                         setCursor( x - 4, y ) | ||||
|                         setCursor(x - 4, y) | ||||
|                     else | ||||
|                         tLines[y] = string.sub(sLine, 1, x - 2) .. string.sub(sLine, x) | ||||
|                         setCursor( x - 1, y ) | ||||
|                         setCursor(x - 1, y) | ||||
|                     end | ||||
|                 elseif y > 1 then | ||||
|                     -- Remove newline | ||||
|                     local sPrevLen = #tLines[y - 1] | ||||
|                     tLines[y - 1] = tLines[y - 1] .. tLines[y] | ||||
|                     table.remove( tLines, y ) | ||||
|                     setCursor( sPrevLen + 1, y - 1 ) | ||||
|                     table.remove(tLines, y) | ||||
|                     setCursor(sPrevLen + 1, y - 1) | ||||
|                     redrawText() | ||||
|                 end | ||||
|             end | ||||
| @@ -671,13 +671,13 @@ while bRunning do | ||||
|                     spaces = 0 | ||||
|                 end | ||||
|                 tLines[y] = string.sub(sLine, 1, x - 1) | ||||
|                 table.insert( tLines, y + 1, string.rep(' ', spaces) .. string.sub(sLine, x) ) | ||||
|                 setCursor( spaces + 1, y + 1 ) | ||||
|                 table.insert(tLines, y + 1, string.rep(' ', spaces) .. string.sub(sLine, x)) | ||||
|                 setCursor(spaces + 1, y + 1) | ||||
|                 redrawText() | ||||
|  | ||||
|             elseif bMenu then | ||||
|                 -- Menu selection | ||||
|                 doMenuItem( nMenuItem ) | ||||
|                 doMenuItem(nMenuItem) | ||||
|  | ||||
|             end | ||||
|  | ||||
| @@ -685,9 +685,9 @@ while bRunning do | ||||
|             -- Menu toggle | ||||
|             bMenu = not bMenu | ||||
|             if bMenu then | ||||
|                 term.setCursorBlink( false ) | ||||
|                 term.setCursorBlink(false) | ||||
|             else | ||||
|                 term.setCursorBlink( true ) | ||||
|                 term.setCursorBlink(true) | ||||
|             end | ||||
|             redrawMenu() | ||||
|  | ||||
| @@ -698,13 +698,13 @@ while bRunning do | ||||
|             -- Input text | ||||
|             local sLine = tLines[y] | ||||
|             tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x) | ||||
|             setCursor( x + 1, y ) | ||||
|             setCursor(x + 1, y) | ||||
|  | ||||
|         elseif bMenu then | ||||
|             -- Select menu items | ||||
|             for n, sMenuItem in ipairs( tMenuItems ) do | ||||
|             for n, sMenuItem in ipairs(tMenuItems) do | ||||
|                 if string.lower(string.sub(sMenuItem, 1, 1)) == string.lower(param) then | ||||
|                     doMenuItem( n ) | ||||
|                     doMenuItem(n) | ||||
|                     break | ||||
|                 end | ||||
|             end | ||||
| @@ -715,13 +715,13 @@ while bRunning do | ||||
|             -- Close menu if open | ||||
|             if bMenu then | ||||
|                 bMenu = false | ||||
|                 term.setCursorBlink( true ) | ||||
|                 term.setCursorBlink(true) | ||||
|                 redrawMenu() | ||||
|             end | ||||
|             -- Input text | ||||
|             local sLine = tLines[y] | ||||
|             tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x) | ||||
|             setCursor( x + #param , y ) | ||||
|             setCursor(x + #param , y) | ||||
|         end | ||||
|  | ||||
|     elseif sEvent == "mouse_click" then | ||||
| @@ -730,9 +730,9 @@ while bRunning do | ||||
|                 -- Left click | ||||
|                 local cx, cy = param2, param3 | ||||
|                 if cy < h then | ||||
|                     local newY = math.min( math.max( scrollY + cy, 1 ), #tLines ) | ||||
|                     local newX = math.min( math.max( scrollX + cx, 1 ), #tLines[newY] + 1 ) | ||||
|                     setCursor( newX, newY ) | ||||
|                     local newY = math.min(math.max(scrollY + cy, 1), #tLines) | ||||
|                     local newX = math.min(math.max(scrollX + cx, 1), #tLines[newY] + 1) | ||||
|                     setCursor(newX, newY) | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
| @@ -761,7 +761,7 @@ while bRunning do | ||||
|  | ||||
|     elseif sEvent == "term_resize" then | ||||
|         w, h = term.getSize() | ||||
|         setCursor( x, y ) | ||||
|         setCursor(x, y) | ||||
|         redrawMenu() | ||||
|         redrawText() | ||||
|  | ||||
| @@ -770,5 +770,5 @@ end | ||||
|  | ||||
| -- Cleanup | ||||
| term.clear() | ||||
| term.setCursorBlink( false ) | ||||
| term.setCursorPos( 1, 1 ) | ||||
| term.setCursorBlink(false) | ||||
| term.setCursorPos(1, 1) | ||||
|   | ||||
| @@ -2,17 +2,17 @@ | ||||
| -- Get arguments | ||||
| local tArgs = { ... } | ||||
| if #tArgs == 0 then | ||||
|     print( "Usage: eject <drive>" ) | ||||
|     print("Usage: eject <drive>") | ||||
|     return | ||||
| end | ||||
|  | ||||
| local sDrive = tArgs[1] | ||||
|  | ||||
| -- Check the disk exists | ||||
| local bPresent = disk.isPresent( sDrive ) | ||||
| local bPresent = disk.isPresent(sDrive) | ||||
| if not bPresent then | ||||
|     print( "Nothing in " .. sDrive .. " drive" ) | ||||
|     print("Nothing in " .. sDrive .. " drive") | ||||
|     return | ||||
| end | ||||
|  | ||||
| disk.eject( sDrive ) | ||||
| disk.eject(sDrive) | ||||
|   | ||||
| @@ -32,7 +32,7 @@ if not term.isColour() then | ||||
| end | ||||
|  | ||||
| -- Determines if the file exists, and can be edited on this computer | ||||
| local tArgs = {...} | ||||
| local tArgs = { ... } | ||||
| if #tArgs == 0 then | ||||
|     print("Usage: paint <path>") | ||||
|     return | ||||
| @@ -45,9 +45,9 @@ if fs.exists(sPath) and fs.isDir(sPath) then | ||||
| end | ||||
|  | ||||
| -- Create .nfp files by default | ||||
| if not fs.exists( sPath ) and not string.find( sPath, "%." ) then | ||||
|     local sExtension = settings.get("paint.default_extension", "" ) | ||||
|     if sExtension ~= "" and type( sExtension ) == "string" then | ||||
| if not fs.exists(sPath) and not string.find(sPath, "%.") then | ||||
|     local sExtension = settings.get("paint.default_extension", "") | ||||
|     if sExtension ~= "" and type(sExtension) == "string" then | ||||
|         sPath = sPath .. "." .. sExtension | ||||
|     end | ||||
| end | ||||
| @@ -57,7 +57,7 @@ end | ||||
| -- Functions -- | ||||
| --------------- | ||||
|  | ||||
| local function getCanvasPixel( x, y ) | ||||
| local function getCanvasPixel(x, y) | ||||
|     if canvas[y] then | ||||
|         return canvas[y][x] | ||||
|     end | ||||
| @@ -69,12 +69,12 @@ end | ||||
|     params: colour = the number to convert to a hex value | ||||
|     returns: a string representing the chosen colour | ||||
| ]] | ||||
| local function getCharOf( colour ) | ||||
| local function getCharOf(colour) | ||||
|     -- Incorrect values always convert to nil | ||||
|     if type(colour) == "number" then | ||||
|         local value = math.floor( math.log(colour) / math.log(2) ) + 1 | ||||
|         local value = math.floor(math.log(colour) / math.log(2)) + 1 | ||||
|         if value >= 1 and value <= 16 then | ||||
|             return string.sub( "0123456789abcdef", value, value ) | ||||
|             return string.sub("0123456789abcdef", value, value) | ||||
|         end | ||||
|     end | ||||
|     return " " | ||||
| @@ -87,9 +87,9 @@ end | ||||
| ]] | ||||
| local tColourLookup = {} | ||||
| for n = 1, 16 do | ||||
|     tColourLookup[ string.byte( "0123456789abcdef", n, n ) ] = 2 ^ (n - 1) | ||||
|     tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) | ||||
| end | ||||
| local function getColourOf( char ) | ||||
| local function getColourOf(char) | ||||
|     -- Values not in the hex table are transparent (canvas coloured) | ||||
|     return tColourLookup[char] | ||||
| end | ||||
| @@ -107,9 +107,9 @@ local function load(path) | ||||
|         while sLine do | ||||
|             local line = {} | ||||
|             for x = 1, w - 2 do | ||||
|                 line[x] = getColourOf( string.byte(sLine, x, x) ) | ||||
|                 line[x] = getColourOf(string.byte(sLine, x, x)) | ||||
|             end | ||||
|             table.insert( canvas, line ) | ||||
|             table.insert(canvas, line) | ||||
|             sLine = file.readLine() | ||||
|         end | ||||
|         file.close() | ||||
| @@ -128,7 +128,7 @@ local function save(path) | ||||
|         fs.makeDir(sDir) | ||||
|     end | ||||
|  | ||||
|     local file, err = fs.open( path, "w" ) | ||||
|     local file, err = fs.open(path, "w") | ||||
|     if not file then | ||||
|         return false, err | ||||
|     end | ||||
| @@ -140,13 +140,13 @@ local function save(path) | ||||
|         local sLine = "" | ||||
|         local nLastChar = 0 | ||||
|         for x = 1, w - 2 do | ||||
|             local c = getCharOf( getCanvasPixel( x, y ) ) | ||||
|             local c = getCharOf(getCanvasPixel(x, y)) | ||||
|             sLine = sLine .. c | ||||
|             if c ~= " " then | ||||
|                 nLastChar = x | ||||
|             end | ||||
|         end | ||||
|         sLine = string.sub( sLine, 1, nLastChar ) | ||||
|         sLine = string.sub(sLine, 1, nLastChar) | ||||
|         tLines[y] = sLine | ||||
|         if #sLine > 0 then | ||||
|             nLastLine = y | ||||
| @@ -155,7 +155,7 @@ local function save(path) | ||||
|  | ||||
|     -- Save out | ||||
|     for n = 1, nLastLine do | ||||
|            file.writeLine( tLines[ n ] ) | ||||
|            file.writeLine(tLines[n]) | ||||
|     end | ||||
|     file.close() | ||||
|     return true | ||||
| @@ -176,38 +176,38 @@ local function drawInterface() | ||||
|     -- Colour Picker | ||||
|     for i = 1, 16 do | ||||
|         term.setCursorPos(w - 1, i) | ||||
|         term.setBackgroundColour( 2 ^ (i - 1) ) | ||||
|         term.setBackgroundColour(2 ^ (i - 1)) | ||||
|         term.write("  ") | ||||
|     end | ||||
|  | ||||
|     term.setCursorPos(w - 1, 17) | ||||
|     term.setBackgroundColour( canvasColour ) | ||||
|     term.setTextColour( colours.grey ) | ||||
|     term.setBackgroundColour(canvasColour) | ||||
|     term.setTextColour(colours.grey) | ||||
|     term.write("\127\127") | ||||
|  | ||||
|     -- Left and Right Selected Colours | ||||
|     do | ||||
|         term.setCursorPos(w - 1, 18) | ||||
|         if leftColour ~= nil then | ||||
|             term.setBackgroundColour( leftColour ) | ||||
|             term.setBackgroundColour(leftColour) | ||||
|             term.write(" ") | ||||
|         else | ||||
|             term.setBackgroundColour( canvasColour ) | ||||
|             term.setTextColour( colours.grey ) | ||||
|             term.setBackgroundColour(canvasColour) | ||||
|             term.setTextColour(colours.grey) | ||||
|             term.write("\127") | ||||
|         end | ||||
|         if rightColour ~= nil then | ||||
|             term.setBackgroundColour( rightColour ) | ||||
|             term.setBackgroundColour(rightColour) | ||||
|             term.write(" ") | ||||
|         else | ||||
|             term.setBackgroundColour( canvasColour ) | ||||
|             term.setTextColour( colours.grey ) | ||||
|             term.setBackgroundColour(canvasColour) | ||||
|             term.setTextColour(colours.grey) | ||||
|             term.write("\127") | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     -- Padding | ||||
|     term.setBackgroundColour( canvasColour ) | ||||
|     term.setBackgroundColour(canvasColour) | ||||
|     for i = 20, h - 1 do | ||||
|         term.setCursorPos(w - 1, i) | ||||
|         term.write("  ") | ||||
| @@ -218,15 +218,15 @@ end | ||||
|     Converts a single pixel of a single line of the canvas and draws it | ||||
|     returns: nil | ||||
| ]] | ||||
| local function drawCanvasPixel( x, y ) | ||||
|     local pixel = getCanvasPixel( x, y ) | ||||
| local function drawCanvasPixel(x, y) | ||||
|     local pixel = getCanvasPixel(x, y) | ||||
|     if pixel then | ||||
|         term.setBackgroundColour( pixel or canvasColour ) | ||||
|         term.setBackgroundColour(pixel or canvasColour) | ||||
|         term.setCursorPos(x, y) | ||||
|         term.write(" ") | ||||
|     else | ||||
|         term.setBackgroundColour( canvasColour ) | ||||
|         term.setTextColour( colours.grey ) | ||||
|         term.setBackgroundColour(canvasColour) | ||||
|         term.setTextColour(colours.grey) | ||||
|         term.setCursorPos(x, y) | ||||
|         term.write("\127") | ||||
|     end | ||||
| @@ -236,9 +236,9 @@ end | ||||
|     Converts each colour in a single line of the canvas and draws it | ||||
|     returns: nil | ||||
| ]] | ||||
| local function drawCanvasLine( y ) | ||||
| local function drawCanvasLine(y) | ||||
|     for x = 1, w - 2 do | ||||
|         drawCanvasPixel( x, y ) | ||||
|         drawCanvasPixel(x, y) | ||||
|     end | ||||
| end | ||||
|  | ||||
| @@ -248,7 +248,7 @@ end | ||||
| ]] | ||||
| local function drawCanvas() | ||||
|     for y = 1, h - 1 do | ||||
|         drawCanvasLine( y ) | ||||
|         drawCanvasLine(y) | ||||
|     end | ||||
| end | ||||
|  | ||||
| @@ -377,7 +377,7 @@ local function handleEvents() | ||||
|                 end | ||||
|                 canvas[p3][p2] = paintColour | ||||
|  | ||||
|                 drawCanvasPixel( p2, p3 ) | ||||
|                 drawCanvasPixel(p2, p3) | ||||
|             end | ||||
|         elseif id == "key" then | ||||
|             if p1 == keys.leftCtrl or p1 == keys.rightCtrl then | ||||
|   | ||||
| @@ -59,10 +59,10 @@ local cR4 = colors.yellow | ||||
| local tArgs = { ... } | ||||
|  | ||||
| --Functions-- | ||||
| local function printCentred( yc, stg ) | ||||
| local function printCentred(yc, stg) | ||||
|     local xc = math.floor((TermW - #stg) / 2) + 1 | ||||
|     term.setCursorPos(xc, yc) | ||||
|     term.write( stg ) | ||||
|     term.write(stg) | ||||
| end | ||||
|  | ||||
| local function centerOrgin() | ||||
| @@ -173,9 +173,9 @@ end | ||||
| local function loadLevel(nNum) | ||||
|     sLevelTitle = "Level " .. nNum | ||||
|     if nNum == nil then return error("nNum == nil") end | ||||
|     local sDir = fs.getDir( shell.getRunningProgram() ) | ||||
|     local sDir = fs.getDir(shell.getRunningProgram()) | ||||
|     local sLevelD = sDir .. "/levels/" .. tostring(nNum) .. ".dat" | ||||
|     if not ( fs.exists(sLevelD) or fs.isDir(sLevelD) ) then return error("Level Not Exists : " .. sLevelD) end | ||||
|     if not (fs.exists(sLevelD) or fs.isDir(sLevelD)) then return error("Level Not Exists : " .. sLevelD) end | ||||
|     fLevel = fs.open(sLevelD, "r") | ||||
|     local wl = true | ||||
|     Blocks = tonumber(string.sub(fLevel.readLine(), 1, 1)) | ||||
| @@ -512,20 +512,20 @@ local function gRender(sContext) | ||||
| end | ||||
|  | ||||
| function InterFace.drawBar() | ||||
|     term.setBackgroundColor( colors.black ) | ||||
|     term.setTextColor( InterFace.cTitle ) | ||||
|     printCentred( 1, "  " .. sLevelTitle .. "  " ) | ||||
|     term.setBackgroundColor(colors.black) | ||||
|     term.setTextColor(InterFace.cTitle) | ||||
|     printCentred(1, "  " .. sLevelTitle .. "  ") | ||||
|  | ||||
|     term.setCursorPos(1, 1) | ||||
|     term.setBackgroundColor( cW ) | ||||
|     write( " " ) | ||||
|     term.setBackgroundColor( colors.black ) | ||||
|     write( " x " .. tostring(Blocks) .. " " ) | ||||
|     term.setBackgroundColor(cW) | ||||
|     write(" ") | ||||
|     term.setBackgroundColor(colors.black) | ||||
|     write(" x " .. tostring(Blocks) .. " ") | ||||
|  | ||||
|     term.setCursorPos( TermW - 8, TermH ) | ||||
|     term.setBackgroundColor( colors.black ) | ||||
|     term.setCursorPos(TermW - 8, TermH) | ||||
|     term.setBackgroundColor(colors.black) | ||||
|     term.setTextColour(InterFace.cSpeedD) | ||||
|     write(" <<" ) | ||||
|     write(" <<") | ||||
|     if bPaused then | ||||
|         term.setTextColour(InterFace.cSpeedA) | ||||
|     else | ||||
| @@ -539,9 +539,9 @@ function InterFace.drawBar() | ||||
|     end | ||||
|     write(" >>") | ||||
|  | ||||
|     term.setCursorPos( TermW - 1, 1 ) | ||||
|     term.setBackgroundColor( colors.black ) | ||||
|     term.setTextColour( InterFace.cExit ) | ||||
|     term.setCursorPos(TermW - 1, 1) | ||||
|     term.setBackgroundColor(colors.black) | ||||
|     term.setTextColour(InterFace.cExit) | ||||
|     write(" X") | ||||
|     term.setBackgroundColor(colors.black) | ||||
| end | ||||
| @@ -612,7 +612,7 @@ local function startG(LevelN) | ||||
|         elseif isExit == "retry" then | ||||
|             return LevelN | ||||
|         elseif fExit == "yes" then | ||||
|             if fs.exists( fs.getDir( shell.getRunningProgram() ) .. "/levels/" .. tostring(LevelN + 1) .. ".dat" ) then | ||||
|             if fs.exists(fs.getDir(shell.getRunningProgram()) .. "/levels/" .. tostring(LevelN + 1) .. ".dat") then | ||||
|                 return LevelN + 1 | ||||
|             else | ||||
|                 return nil | ||||
| @@ -629,26 +629,26 @@ local ok, err = true, nil | ||||
| --Menu-- | ||||
| local sStartLevel = tArgs[1] | ||||
| if ok and not sStartLevel then | ||||
|     ok, err = pcall( function() | ||||
|     ok, err = pcall(function() | ||||
|         term.setTextColor(colors.white) | ||||
|         term.setBackgroundColor( colors.black ) | ||||
|         term.setBackgroundColor(colors.black) | ||||
|         term.clear() | ||||
|         drawStars() | ||||
|         term.setTextColor( colors.red ) | ||||
|         printCentred( TermH / 2 - 1, "  REDIRECTION  " ) | ||||
|         printCentred( TermH / 2 - 0, "  ComputerCraft Edition  " ) | ||||
|         term.setTextColor( colors.yellow ) | ||||
|         printCentred( TermH / 2 + 2, "  Click to Begin  " ) | ||||
|         os.pullEvent( "mouse_click" ) | ||||
|     end ) | ||||
|         term.setTextColor(colors.red) | ||||
|         printCentred(TermH / 2 - 1, "  REDIRECTION  ") | ||||
|         printCentred(TermH / 2 - 0, "  ComputerCraft Edition  ") | ||||
|         term.setTextColor(colors.yellow) | ||||
|         printCentred(TermH / 2 + 2, "  Click to Begin  ") | ||||
|         os.pullEvent("mouse_click") | ||||
|     end) | ||||
| end | ||||
|  | ||||
| --Game-- | ||||
| if ok then | ||||
|     ok, err = pcall( function() | ||||
|     ok, err = pcall(function() | ||||
|         local nLevel | ||||
|         if sStartLevel then | ||||
|             nLevel = tonumber( sStartLevel ) | ||||
|             nLevel = tonumber(sStartLevel) | ||||
|         else | ||||
|             nLevel = 1 | ||||
|         end | ||||
| @@ -656,36 +656,36 @@ if ok then | ||||
|             reset() | ||||
|             nLevel = startG(nLevel) | ||||
|         end | ||||
|     end ) | ||||
|     end) | ||||
| end | ||||
|  | ||||
| --Upsell screen-- | ||||
| if ok then | ||||
|     ok, err = pcall( function() | ||||
|     ok, err = pcall(function() | ||||
|         term.setTextColor(colors.white) | ||||
|         term.setBackgroundColor( colors.black ) | ||||
|         term.setBackgroundColor(colors.black) | ||||
|         term.clear() | ||||
|         drawStars() | ||||
|         term.setTextColor( colors.red ) | ||||
|         term.setTextColor(colors.red) | ||||
|         if TermW >= 40 then | ||||
|             printCentred( TermH / 2 - 1, "  Thank you for playing Redirection  " ) | ||||
|             printCentred( TermH / 2 - 0, "  ComputerCraft Edition  " ) | ||||
|             printCentred( TermH / 2 + 2, "  Check out the full game:  " ) | ||||
|             term.setTextColor( colors.yellow ) | ||||
|             printCentred( TermH / 2 + 3, "  http://www.redirectiongame.com  " ) | ||||
|             printCentred(TermH / 2 - 1, "  Thank you for playing Redirection  ") | ||||
|             printCentred(TermH / 2 - 0, "  ComputerCraft Edition  ") | ||||
|             printCentred(TermH / 2 + 2, "  Check out the full game:  ") | ||||
|             term.setTextColor(colors.yellow) | ||||
|             printCentred(TermH / 2 + 3, "  http://www.redirectiongame.com  ") | ||||
|         else | ||||
|             printCentred( TermH / 2 - 2, "  Thank you for  " ) | ||||
|             printCentred( TermH / 2 - 1, "  playing Redirection  " ) | ||||
|             printCentred( TermH / 2 - 0, "  ComputerCraft Edition  " ) | ||||
|             printCentred( TermH / 2 + 2, "  Check out the full game:  " ) | ||||
|             term.setTextColor( colors.yellow ) | ||||
|             printCentred( TermH / 2 + 3, "  www.redirectiongame.com  " ) | ||||
|             printCentred(TermH / 2 - 2, "  Thank you for  ") | ||||
|             printCentred(TermH / 2 - 1, "  playing Redirection  ") | ||||
|             printCentred(TermH / 2 - 0, "  ComputerCraft Edition  ") | ||||
|             printCentred(TermH / 2 + 2, "  Check out the full game:  ") | ||||
|             term.setTextColor(colors.yellow) | ||||
|             printCentred(TermH / 2 + 3, "  www.redirectiongame.com  ") | ||||
|         end | ||||
|         parallel.waitForAll( | ||||
|             function() sleep(2) end, | ||||
|             function() os.pullEvent( "mouse_click" ) end | ||||
|             function() os.pullEvent("mouse_click") end | ||||
|         ) | ||||
|     end ) | ||||
|     end) | ||||
| end | ||||
|  | ||||
| --Clear and exit-- | ||||
| @@ -695,9 +695,9 @@ term.setBackgroundColor(colors.black) | ||||
| term.clear() | ||||
| if not ok then | ||||
|     if err == "Terminated" then | ||||
|         print( "Check out the full version of Redirection:" ) | ||||
|         print( "http://www.redirectiongame.com" ) | ||||
|         print("Check out the full version of Redirection:") | ||||
|         print("http://www.redirectiongame.com") | ||||
|     else | ||||
|         printError( err ) | ||||
|         printError(err) | ||||
|     end | ||||
| end | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,10 +1,10 @@ | ||||
| local tArgs = { ... } | ||||
|  | ||||
| local function printUsage() | ||||
|     print( "Usages:") | ||||
|     print( "dj play" ) | ||||
|     print( "dj play <drive>" ) | ||||
|     print( "dj stop" ) | ||||
|     print("Usages:") | ||||
|     print("dj play") | ||||
|     print("dj play <drive>") | ||||
|     print("dj stop") | ||||
| end | ||||
|  | ||||
| if #tArgs > 2 then | ||||
| @@ -23,24 +23,24 @@ elseif sCommand == "play" or sCommand == nil then | ||||
|     if sName == nil then | ||||
|         -- No disc specified, pick one at random | ||||
|         local tNames = {} | ||||
|         for _, sName in ipairs( peripheral.getNames() ) do | ||||
|             if disk.isPresent( sName ) and disk.hasAudio( sName ) then | ||||
|                 table.insert( tNames, sName ) | ||||
|         for _, sName in ipairs(peripheral.getNames()) do | ||||
|             if disk.isPresent(sName) and disk.hasAudio(sName) then | ||||
|                 table.insert(tNames, sName) | ||||
|             end | ||||
|         end | ||||
|         if #tNames == 0 then | ||||
|             print( "No Music Discs in attached disk drives" ) | ||||
|             print("No Music Discs in attached disk drives") | ||||
|             return | ||||
|         end | ||||
|         sName = tNames[ math.random(1, #tNames) ] | ||||
|         sName = tNames[math.random(1, #tNames)] | ||||
|     end | ||||
|  | ||||
|     -- Play the disc | ||||
|     if disk.isPresent( sName ) and disk.hasAudio( sName ) then | ||||
|         print( "Playing " .. disk.getAudioTitle( sName ) ) | ||||
|         disk.playAudio( sName ) | ||||
|     if disk.isPresent(sName) and disk.hasAudio(sName) then | ||||
|         print("Playing " .. disk.getAudioTitle(sName)) | ||||
|         disk.playAudio(sName) | ||||
|     else | ||||
|         print( "No Music Disc in disk drive: " .. sName ) | ||||
|         print("No Music Disc in disk drive: " .. sName) | ||||
|         return | ||||
|     end | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| if term.isColour() then | ||||
|     term.setTextColour( 2 ^ math.random(0, 15) ) | ||||
|     term.setTextColour(2 ^ math.random(0, 15)) | ||||
| end | ||||
| textutils.slowPrint( "Hello World!" ) | ||||
| term.setTextColour( colours.white ) | ||||
| textutils.slowPrint("Hello World!") | ||||
| term.setTextColour(colours.white) | ||||
|   | ||||
| @@ -15,11 +15,11 @@ else | ||||
|     fruitColour = colours.white | ||||
| end | ||||
|  | ||||
| local function printCentred( y, s ) | ||||
| local function printCentred(y, s) | ||||
|     local x = math.floor((w - #s) / 2) | ||||
|     term.setCursorPos(x, y) | ||||
|     --term.clearLine() | ||||
|     term.write( s ) | ||||
|     term.write(s) | ||||
| end | ||||
|  | ||||
| local xVel, yVel = 1, 0 | ||||
| @@ -65,9 +65,9 @@ local function addFruit() | ||||
|         if fruit.snake == nil and fruit.wall == nil and fruit.fruit == nil then | ||||
|             screen[x][y] = { fruit = true } | ||||
|             term.setCursorPos(x, y) | ||||
|             term.setBackgroundColour( fruitColour ) | ||||
|             term.setBackgroundColour(fruitColour) | ||||
|             term.write(" ") | ||||
|             term.setBackgroundColour( colours.black ) | ||||
|             term.setBackgroundColour(colours.black) | ||||
|             break | ||||
|         end | ||||
|     end | ||||
| @@ -79,23 +79,23 @@ local function addFruit() | ||||
| end | ||||
|  | ||||
| local function drawMenu() | ||||
|     term.setTextColour( headingColour ) | ||||
|     term.setTextColour(headingColour) | ||||
|     term.setCursorPos(1, 1) | ||||
|     term.write( "SCORE " ) | ||||
|     term.write("SCORE ") | ||||
|  | ||||
|     term.setTextColour( textColour ) | ||||
|     term.setTextColour(textColour) | ||||
|     term.setCursorPos(7, 1) | ||||
|     term.write( tostring(nScore) ) | ||||
|     term.write(tostring(nScore)) | ||||
|  | ||||
|     term.setTextColour( headingColour ) | ||||
|     term.setTextColour(headingColour) | ||||
|     term.setCursorPos(w - 11, 1) | ||||
|     term.write( "DIFFICULTY ") | ||||
|     term.write("DIFFICULTY ") | ||||
|  | ||||
|     term.setTextColour( textColour ) | ||||
|     term.setTextColour(textColour) | ||||
|     term.setCursorPos(w, 1) | ||||
|     term.write( tostring(nDifficulty or "?") ) | ||||
|     term.write(tostring(nDifficulty or "?")) | ||||
|  | ||||
|     term.setTextColour( colours.white ) | ||||
|     term.setTextColour(colours.white) | ||||
| end | ||||
|  | ||||
| local function update( ) | ||||
| @@ -150,9 +150,9 @@ local function update( ) | ||||
|     end | ||||
|  | ||||
|     term.setCursorPos(xPos, yPos) | ||||
|     term.setBackgroundColour( wormColour ) | ||||
|     term.setBackgroundColour(wormColour) | ||||
|     term.write(" ") | ||||
|     term.setBackgroundColour( colours.black ) | ||||
|     term.setBackgroundColour(colours.black) | ||||
|  | ||||
|     drawMenu() | ||||
| end | ||||
| @@ -163,29 +163,29 @@ local function drawFrontend() | ||||
|     --term.setTextColour( titleColour ) | ||||
|     --printCentred( math.floor(h/2) - 4, " W O R M " ) | ||||
|  | ||||
|     term.setTextColour( headingColour ) | ||||
|     printCentred( math.floor(h / 2) - 3, "" ) | ||||
|     printCentred( math.floor(h / 2) - 2, " SELECT DIFFICULTY " ) | ||||
|     printCentred( math.floor(h / 2) - 1, "" ) | ||||
|     term.setTextColour(headingColour) | ||||
|     printCentred(math.floor(h / 2) - 3, "") | ||||
|     printCentred(math.floor(h / 2) - 2, " SELECT DIFFICULTY ") | ||||
|     printCentred(math.floor(h / 2) - 1, "") | ||||
|  | ||||
|     printCentred( math.floor(h / 2) + 0, "            " ) | ||||
|     printCentred( math.floor(h / 2) + 1, "            " ) | ||||
|     printCentred( math.floor(h / 2) + 2, "            " ) | ||||
|     printCentred( math.floor(h / 2) - 1 + nDifficulty, " [        ] " ) | ||||
|     printCentred(math.floor(h / 2) + 0, "            ") | ||||
|     printCentred(math.floor(h / 2) + 1, "            ") | ||||
|     printCentred(math.floor(h / 2) + 2, "            ") | ||||
|     printCentred(math.floor(h / 2) - 1 + nDifficulty, " [        ] ") | ||||
|  | ||||
|     term.setTextColour( textColour ) | ||||
|     printCentred( math.floor(h / 2) + 0, "EASY" ) | ||||
|     printCentred( math.floor(h / 2) + 1, "MEDIUM" ) | ||||
|     printCentred( math.floor(h / 2) + 2, "HARD" ) | ||||
|     printCentred( math.floor(h / 2) + 3, "" ) | ||||
|     term.setTextColour(textColour) | ||||
|     printCentred(math.floor(h / 2) + 0, "EASY") | ||||
|     printCentred(math.floor(h / 2) + 1, "MEDIUM") | ||||
|     printCentred(math.floor(h / 2) + 2, "HARD") | ||||
|     printCentred(math.floor(h / 2) + 3, "") | ||||
|  | ||||
|     term.setTextColour( colours.white ) | ||||
|     term.setTextColour(colours.white) | ||||
| end | ||||
|  | ||||
| drawMenu() | ||||
| drawFrontend() | ||||
| while true do | ||||
|     local _, key = os.pullEvent( "key" ) | ||||
|     local _, key = os.pullEvent("key") | ||||
|     if key == keys.up or key == keys.w then | ||||
|         -- Up | ||||
|         if nDifficulty > 1 then | ||||
| @@ -226,7 +226,7 @@ while bRunning do | ||||
|     local event, p1 = os.pullEvent() | ||||
|     if event == "timer" and p1 == timer then | ||||
|         timer = os.startTimer(nInterval) | ||||
|         update( false ) | ||||
|         update(false) | ||||
|  | ||||
|     elseif event == "key" then | ||||
|         local key = p1 | ||||
| @@ -257,24 +257,24 @@ while bRunning do | ||||
| end | ||||
|  | ||||
| -- Display the gameover screen | ||||
| term.setTextColour( headingColour ) | ||||
| printCentred( math.floor(h / 2) - 2, "                   " ) | ||||
| printCentred( math.floor(h / 2) - 1, " G A M E   O V E R " ) | ||||
| term.setTextColour(headingColour) | ||||
| printCentred(math.floor(h / 2) - 2, "                   ") | ||||
| printCentred(math.floor(h / 2) - 1, " G A M E   O V E R ") | ||||
|  | ||||
| term.setTextColour( textColour ) | ||||
| printCentred( math.floor(h / 2) + 0, "                 " ) | ||||
| printCentred( math.floor(h / 2) + 1, " FINAL SCORE " .. nScore .. " " ) | ||||
| printCentred( math.floor(h / 2) + 2, "                 " ) | ||||
| term.setTextColour( colours.white ) | ||||
| term.setTextColour(textColour) | ||||
| printCentred(math.floor(h / 2) + 0, "                 ") | ||||
| printCentred(math.floor(h / 2) + 1, " FINAL SCORE " .. nScore .. " ") | ||||
| printCentred(math.floor(h / 2) + 2, "                 ") | ||||
| term.setTextColour(colours.white) | ||||
|  | ||||
| local timer = os.startTimer(2.5) | ||||
| repeat | ||||
|     local e, p = os.pullEvent() | ||||
|     if e == "timer" and p == timer then | ||||
|         term.setTextColour( textColour ) | ||||
|         printCentred( math.floor(h / 2) + 2, " PRESS ANY KEY " ) | ||||
|         printCentred( math.floor(h / 2) + 3, "               " ) | ||||
|         term.setTextColour( colours.white ) | ||||
|         term.setTextColour(textColour) | ||||
|         printCentred(math.floor(h / 2) + 2, " PRESS ANY KEY ") | ||||
|         printCentred(math.floor(h / 2) + 3, "               ") | ||||
|         term.setTextColour(colours.white) | ||||
|     end | ||||
| until e == "char" | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
|  | ||||
| local function printUsage() | ||||
|     print( "Usages:" ) | ||||
|     print( "gps host" ) | ||||
|     print( "gps host <x> <y> <z>" ) | ||||
|     print( "gps locate" ) | ||||
|     print("Usages:") | ||||
|     print("gps host") | ||||
|     print("gps host <x> <y> <z>") | ||||
|     print("gps locate") | ||||
| end | ||||
|  | ||||
| local tArgs = { ... } | ||||
| @@ -16,27 +16,27 @@ end | ||||
| if sCommand == "locate" then | ||||
|     -- "gps locate" | ||||
|     -- Just locate this computer (this will print the results) | ||||
|     gps.locate( 2, true ) | ||||
|     gps.locate(2, true) | ||||
|  | ||||
| elseif sCommand == "host" then | ||||
|     -- "gps host" | ||||
|     -- Act as a GPS host | ||||
|     if pocket then | ||||
|         print( "GPS Hosts must be stationary" ) | ||||
|         print("GPS Hosts must be stationary") | ||||
|         return | ||||
|     end | ||||
|  | ||||
|     -- Find a modem | ||||
|     local sModemSide = nil | ||||
|     for _, sSide in ipairs( rs.getSides() ) do | ||||
|         if peripheral.getType( sSide ) == "modem" and peripheral.call( sSide, "isWireless" ) then | ||||
|     for _, sSide in ipairs(rs.getSides()) do | ||||
|         if peripheral.getType(sSide) == "modem" and peripheral.call(sSide, "isWireless") then | ||||
|             sModemSide = sSide | ||||
|             break | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     if sModemSide == nil then | ||||
|         print( "No wireless modems found. 1 required." ) | ||||
|         print("No wireless modems found. 1 required.") | ||||
|         return | ||||
|     end | ||||
|  | ||||
| @@ -51,31 +51,31 @@ elseif sCommand == "host" then | ||||
|             printUsage() | ||||
|             return | ||||
|         end | ||||
|         print( "Position is " .. x .. "," .. y .. "," .. z ) | ||||
|         print("Position is " .. x .. "," .. y .. "," .. z) | ||||
|     else | ||||
|         -- Position is to be determined using locate | ||||
|         x, y, z = gps.locate( 2, true ) | ||||
|         x, y, z = gps.locate(2, true) | ||||
|         if x == nil then | ||||
|             print( "Run \"gps host <x> <y> <z>\" to set position manually" ) | ||||
|             print("Run \"gps host <x> <y> <z>\" to set position manually") | ||||
|             return | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     -- Open a channel | ||||
|     local modem = peripheral.wrap( sModemSide ) | ||||
|     print( "Opening channel on modem " .. sModemSide ) | ||||
|     modem.open( gps.CHANNEL_GPS ) | ||||
|     local modem = peripheral.wrap(sModemSide) | ||||
|     print("Opening channel on modem " .. sModemSide) | ||||
|     modem.open(gps.CHANNEL_GPS) | ||||
|  | ||||
|     -- Serve requests indefinately | ||||
|     local nServed = 0 | ||||
|     while true do | ||||
|         local e, p1, p2, p3, p4, p5 = os.pullEvent( "modem_message" ) | ||||
|         local e, p1, p2, p3, p4, p5 = os.pullEvent("modem_message") | ||||
|         if e == "modem_message" then | ||||
|             -- We received a message from a modem | ||||
|             local sSide, sChannel, sReplyChannel, sMessage, nDistance = p1, p2, p3, p4, p5 | ||||
|             if sSide == sModemSide and sChannel == gps.CHANNEL_GPS and sMessage == "PING" and nDistance then | ||||
|                 -- We received a ping message on the GPS channel, send a response | ||||
|                 modem.transmit( sReplyChannel, gps.CHANNEL_GPS, { x, y, z } ) | ||||
|                 modem.transmit(sReplyChannel, gps.CHANNEL_GPS, { x, y, z }) | ||||
|  | ||||
|                 -- Print the number of requests handled | ||||
|                 nServed = nServed + 1 | ||||
| @@ -83,7 +83,7 @@ elseif sCommand == "host" then | ||||
|                     local _, y = term.getCursorPos() | ||||
|                     term.setCursorPos(1, y - 1) | ||||
|                 end | ||||
|                 print( nServed .. " GPS requests served" ) | ||||
|                 print(nServed .. " GPS requests served") | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 SquidDev
					SquidDev