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:
SquidDev 2020-04-22 09:45:23 +01:00
commit d847a4d9e0
537 changed files with 8869 additions and 4413 deletions

View File

@ -14,5 +14,9 @@ trim_trailing_whitespace = false
[*.sexp]
indent_size = 2
[*.yml]
indent_size = 2
[*.properties]
insert_final_newline = false

View File

@ -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
View 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
View 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
View File

@ -3,6 +3,8 @@
/logs
/build
/out
/doc/**/*.html
/doc/index.json
# Runtime directories
/run

36
CONTRIBUTING.md Normal file
View 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

View File

@ -37,20 +37,14 @@ ## Relation to CCTweaks?
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!

View File

@ -104,7 +104,7 @@ accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg')
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
View File

@ -0,0 +1,11 @@
# ![CC: Tweaked](logo.png) [![Download CC: Tweaked on CurseForge](http://cf.way2muchnoise.eu/title/cc-tweaked.svg)](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"

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

6
doc/stub/commands.lua Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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; }

View File

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

View File

@ -24,6 +24,7 @@
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;

View File

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

View File

@ -27,7 +27,7 @@ public FileOperationException( @Nullable String filename, @Nonnull String messag
this.filename = filename;
}
public FileOperationException( String message )
public FileOperationException( @Nonnull String message )
{
super( Objects.requireNonNull( message, "message cannot be null" ) );
this.filename = null;

View File

@ -14,6 +14,7 @@
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 @@ default ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IO
{
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 ) );
}
}

View File

@ -14,6 +14,7 @@
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 @@ default WritableByteChannel openChannelForAppend( @Nonnull String path ) throws
* @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();
}
}

View File

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

View File

@ -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 void draw( int originX, int originY )
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 );
}
}
}

View File

@ -7,15 +7,12 @@
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.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 @@ protected void renderItem( ItemStack stack )
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 @@ private static void renderLight( int colour, int width, int height )
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 );

View File

@ -63,11 +63,12 @@ private 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 static void drawText( int x, int y, int start, String[] text, String[] co
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
);
}
}

View File

@ -10,33 +10,29 @@
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 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
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 );
}
}

View File

@ -22,6 +22,11 @@
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 String[] getMethodNames()
"getFreeSpace",
"find",
"getDir",
"getCapacity",
"attributes",
};
}
@ -315,9 +322,8 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
throw new LuaException( e.getMessage() );
}
}
case 14:
case 14: // find
{
// find
String path = getString( args, 0 );
try
{
@ -329,15 +335,50 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
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();
}
}

View File

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

View File

@ -18,6 +18,8 @@
public interface IAPIEnvironment
{
String TIMER_EVENT = "timer";
@FunctionalInterface
interface IPeripheralChangeListener
{
@ -64,6 +66,10 @@ interface IPeripheralChangeListener
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 )

View File

@ -9,6 +9,8 @@
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 int compareTo( @Nonnull Alarm o )
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 void startup()
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 void startup()
@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 void update()
@Override
public void shutdown()
{
synchronized( m_timers )
{
m_timers.clear();
}
synchronized( m_alarms )
{
m_alarms.clear();
@ -229,11 +186,8 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
{
// 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 Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
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 Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
{
// cancelTimer
int token = getInt( args, 0 );
synchronized( m_timers )
{
m_timers.remove( token );
}
m_apiEnvironment.cancelTimer( token );
return null;
}
case 14:

View File

@ -383,22 +383,30 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
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;
}
}
}

View File

@ -40,7 +40,11 @@
*/
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";

View File

@ -7,6 +7,7 @@
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.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 Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
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 Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
}
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
{

View File

@ -181,7 +181,7 @@ public void tick()
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 );

View File

@ -426,6 +426,7 @@ private void turnOn() throws InterruptedException
}
// Init APIs
computer.getEnvironment().reset();
for( ILuaAPI api : apis ) api.startup();
// Init lua
@ -469,6 +470,7 @@ private void shutdown() throws InterruptedException
// Shutdown our APIs
for( ILuaAPI api : apis ) api.shutdown();
computer.getEnvironment().reset();
// Unload filesystem
if( fileSystem != null )

View File

@ -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.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 void setBundledRedstoneInput( ComputerSide side, int combination )
}
/**
* 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 void setLabel( String label )
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;
}
}
}

View File

@ -12,6 +12,7 @@
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 ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOE
}
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" );
}
}

View 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 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 ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOE
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 long getRemainingSpace()
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 @@ private void create() throws IOException
}
}
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;
}
}
}

View File

@ -7,7 +7,6 @@
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.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 synchronized void mount( String label, String location, IMount mount ) th
{
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 synchronized void mountWritable( String label, String location, IWritable
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 static String getDirectory( String path )
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 synchronized String[] list( String path ) throws FileSystemException
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 synchronized void copy( String sourcePath, String destPath ) throws FileS
{
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 @@ private synchronized void copyRecursive( String sourcePath, MountWrapper sourceM
{
copyRecursive(
combine( sourcePath, child ), sourceMount,
combine( destinationPath, child ), destinationMount
combine( destinationPath, child ), destinationMount,
depth + 1
);
}
}
@ -726,17 +427,25 @@ public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite( Str
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 @@ else if( part.length() >= 255 )
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( ".." ) )
{

View File

@ -23,6 +23,9 @@
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 ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOE
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 @@ private static void cleanup()
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;
}
}
}

View File

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

View File

@ -11,43 +11,42 @@
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 long getSize( @Nonnull String path ) throws IOException
@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;
}
}

View File

@ -92,6 +92,7 @@ public CobaltLuaMachine( Computer computer, TimeoutState timeout )
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

View File

@ -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.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 TileMonitor getOrigin()
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 static void destroyAll()
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();
}

View File

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

View File

@ -48,13 +48,13 @@ public void resetColour( int i )
{
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 );
}

View 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)

View 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=터틀 액션 미사용

View File

@ -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" }

View File

@ -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" }
}
}
]

View File

@ -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" }
}
}
]

View File

@ -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" }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
adventure is a text adventure game for CraftOS. To navigate around the world of adventure, type simple instructions to the interpreter, for example: "go north", "punch tree", "craft planks", "mine coal with pickaxe", "hit creeper with sword"
adventure is a text adventure game for CraftOS. To navigate around the world of adventure, type simple instructions to the interpreter, for example: "go north", "punch tree", "craft planks", "mine coal with pickaxe", "hit creeper with sword"

View File

@ -3,4 +3,4 @@ alias assigns shell commands to run other programs.
ex:
"alias dir ls" will make the "dir" command run the "ls" program
"alias dir" will remove the alias set on "dir"
"alias" will list all current aliases.
"alias" will list all current aliases.

View File

@ -2,4 +2,4 @@ Surf the rednet superhighway with "chat", the networked chat program for CraftOS
ex:
"chat host forgecraft" will create a chatroom with the name "forgecraft"
"chat join forgecraft direwolf20" will connect to the chatroom with the name "forgecraft", using the nickname "direwolf20"
"chat join forgecraft direwolf20" will connect to the chatroom with the name "forgecraft", using the nickname "direwolf20"

View File

@ -1 +1 @@
clear clears the screen.
clear clears the screen.

View File

@ -1,2 +1,2 @@
coroutine is a standard Lua5.1 API.
Refer to http://www.lua.org/manual/5.1/ for more information.
Refer to http://www.lua.org/manual/5.1/ for more information.

View File

@ -1 +1 @@
dance is a program for Turtles. Turtles love to get funky.
dance is a program for Turtles. Turtles love to get funky.

View File

@ -14,4 +14,4 @@ disk.getID( drive )
Events fired by the disk API:
"disk" when a disk or other item is inserted into a disk drive. Argument is the name of the drive
"disk_eject" when a disk is removed from a disk drive. Argument is the name of the drive
Type "help events" to learn about the event system.
Type "help events" to learn about the event system.

View File

@ -3,4 +3,4 @@ dj plays Music Discs from disk drives attached to the computer.
ex:
"dj" or "dj play" plays a random disc.
"dj play left" plays the disc in the drive on the left of the computer.
"dj stop" stops the current disc.
"dj stop" stops the current disc.

View File

@ -2,4 +2,4 @@ drive tells you which disk drive the current or specified directory is located i
ex:
"drive" tell you the disk drive of the current directory.
"drive foo" tells you the disk drive of the subdirectory "foo"
"drive foo" tells you the disk drive of the subdirectory "foo"

View File

@ -16,4 +16,4 @@ getDiskID()
Events fired by the Disk Drive:
"disk" when a disk or other item is inserted into the drive. Argument is the name of the drive.
"disk_eject" when a disk is removed from a drive. Argument is the name of the drive.
Type "help events" to learn about the event system.
Type "help events" to learn about the event system.

View File

@ -1 +1 @@
Mostly harmless.
Mostly harmless.

View File

@ -1,4 +1,4 @@
edit is a text editor for creating or modifying programs or text files. After creating a program with edit, type its filename in the shell to run it. You can open any of the builtin programs with edit to learn how to program.
ex:
"edit hello" opens a file called "hello" for editing.
"edit hello" opens a file called "hello" for editing.

View File

@ -1 +1 @@
exit will exit the current shell.
exit will exit the current shell.

View File

@ -4,7 +4,7 @@ Type "help gpsapi" for help using GPS functions in lua programs.
ex:
"gps locate" will connect to nearby GPS servers, and try to determine the position of the computer or turtle.
"gps host" will try to determine the position, and host a GPS server if successful.
"gps host 10 20 30" will host a GPS server, using the manually entered position 10,20,30.
"gps host 10 20 30" will host a GPS server, using the manually entered position 10,20,30.
Take care when manually entering host positions. If the positions entered into multiple GPS hosts
are not consistent, the results of locate calls will be incorrect.
are not consistent, the results of locate calls will be incorrect.

View File

@ -1,4 +1,4 @@
Functions in the GPS API:
gps.locate( timeout )
The locate function will send a signal to nearby gps servers, and wait for responses before the timeout. If it receives enough responses to determine this computers position then x, y and z co-ordinates will be returned, otherwise it will return nil. If GPS hosts do not have their positions configured correctly, results will be inaccurate.
The locate function will send a signal to nearby gps servers, and wait for responses before the timeout. If it receives enough responses to determine this computers position then x, y and z co-ordinates will be returned, otherwise it will return nil. If GPS hosts do not have their positions configured correctly, results will be inaccurate.

View File

@ -1 +1 @@
hello prints the text "Hello World!" to the screen.
hello prints the text "Hello World!" to the screen.

View File

@ -1,4 +1,4 @@
help is the help tool you're currently using.
Type "help index" to see all help topics.
Type "help" to see the help intro.
Type "help helpapi" for information on the help Lua API.
Type "help helpapi" for information on the help Lua API.

View File

@ -2,4 +2,4 @@ id prints the unique identifier of this computer, or a Disk in an attached Disk
ex:
"id" will print this Computers ID and label
"id left" will print the ID and label of the disk in the Disk Drive on the left
"id left" will print the ID and label of the disk in the Disk Drive on the left

View File

@ -1,2 +1,2 @@
io is a standard Lua5.1 API, reimplemented for CraftOS. Not all the features are availiable.
Refer to http://www.lua.org/manual/5.1/ for more information.
Refer to http://www.lua.org/manual/5.1/ for more information.

View File

@ -3,7 +3,7 @@ The keys API contains constants for all the key codes that can be returned by th
Example usage:
local sEvent, nKey = os.pullEvent()
if sEvent == "key" and nKey == keys.enter then
-- Do something
-- Do something
end
See http://www.minecraftwiki.net/wiki/Key_codes, or the source code, for a complete reference.
See http://www.minecraftwiki.net/wiki/Key_codes, or the source code, for a complete reference.

View File

@ -1 +1 @@
ls will list all the directories and files in the current location. Use "type" to find out if an item is a file or a directory.
ls will list all the directories and files in the current location. Use "type" to find out if an item is a file or a directory.

View File

@ -1 +1 @@
lua is an interactive prompt for the lua programming language. It's a useful tool for learning the language.
lua is an interactive prompt for the lua programming language. It's a useful tool for learning the language.

View File

@ -1,2 +1,2 @@
math is a standard Lua5.1 API.
Refer to http://www.lua.org/manual/5.1/ for more information.
Refer to http://www.lua.org/manual/5.1/ for more information.

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