mirror of
https://github.com/lune-org/lune.git
synced 2025-04-04 10:30:54 +01:00
Compare commits
179 commits
Author | SHA1 | Date | |
---|---|---|---|
|
27e3efca97 | ||
|
8bd1a9b77d | ||
|
bb8c4bce82 | ||
|
6902ecaa7c | ||
|
dc08b91314 | ||
|
822dd19393 | ||
6cd0234a5f | |||
|
19e7f57284 | ||
|
5d1401cdf6 | ||
|
91af86cca2 | ||
|
c935149c1e | ||
|
e5bda57665 | ||
|
ef294f207c | ||
|
f89d02a60d | ||
|
d090cd2420 | ||
|
99c17795c1 | ||
|
138221b93e | ||
|
8abfc21181 | ||
309c461e11 | |||
|
93fa14d832 | ||
df4fb9be91 | |||
eaac9ff53a | |||
|
0d2f5539b6 | ||
|
0f4cac29aa | ||
|
010cd36375 | ||
|
c17da72815 | ||
|
ff83c401b8 | ||
|
a007fa94a6 | ||
|
1d4d1635eb | ||
|
56f08a88aa | ||
|
833d0e244b | ||
|
3e09807638 | ||
|
ea7013322f | ||
|
98b31b9f67 | ||
|
8364a8e4de | ||
|
473ad80e8f | ||
|
180d20ce4a | ||
|
b585234b08 | ||
|
5379c79488 | ||
|
8aefe88104 | ||
|
cb552af660 | ||
|
95c2ca0965 | ||
|
5167a71e6f | ||
|
eac34d2e7e | ||
|
ff80981282 | ||
|
45493dc23b | ||
|
359f28133f | ||
|
c7cbda98fe | ||
|
a7ac864ca5 | ||
|
9993e03f04 | ||
|
997653eb4a | ||
|
f94fbc685a | ||
|
e2e8beb45c | ||
6b38a21454 | |||
|
430d5683f0 | ||
|
59a7955132 | ||
|
1fb1d3e7b5 | ||
|
0efc2c565b | ||
|
c94ab0cde1 | ||
|
d3b9a4b9e8 | ||
|
3cf2be51bc | ||
|
a3f0f279a8 | ||
|
a94c9d6d54 | ||
|
63493e78de | ||
|
8cb7b8a13a | ||
|
9d9f1685d8 | ||
|
91ac6b00c1 | ||
|
2a85532448 | ||
|
5a292aabc5 | ||
|
cf513c6724 | ||
|
b628601cc8 | ||
|
649bdc4c31 | ||
|
3030158159 | ||
|
23456ae041 | ||
|
4f6f1835d2 | ||
|
636d0bf277 | ||
|
adc74f47c0 | ||
|
1fd17ca0b3 | ||
|
9498620e03 | ||
|
0850f41617 | ||
|
f2c40a4bd5 | ||
|
bfb89dec01 | ||
|
395c36fa8b | ||
|
7e784ba361 | ||
|
95b81d65fe | ||
|
43de7b277a | ||
|
1d0bab5e65 | ||
|
e0f065b678 | ||
|
5b5c46f802 | ||
|
489c8a6609 | ||
|
a13b17fee6 | ||
|
e0b9ceb86d | ||
|
6bc1fa2343 | ||
|
763b3ff6a7 | ||
|
816b6654da | ||
|
efcc3c6028 | ||
|
de71558c5d | ||
|
3f53fc983c | ||
|
089ecb3a4c | ||
|
7a46f12c02 | ||
|
96eed54a65 | ||
|
abe7217a58 | ||
|
54081c1b0f | ||
|
12f5824da9 | ||
|
e11302766b | ||
|
70f0c55d35 | ||
|
476125cc74 | ||
|
b8196d6284 | ||
|
a11c1558ed | ||
|
cec77a9bd9 | ||
|
0685e62a8f | ||
|
53463641b8 | ||
66ed1a0b72 | |||
f830ce7fad | |||
7fb48dfa1f | |||
|
fa7f6c6f51 | ||
|
753897222a | ||
|
4a28499aaa | ||
|
03c773fd79 | ||
fe55442dac | |||
|
34fc23d024 | ||
|
fcfb5ed3a8 | ||
|
f36ec1483a | ||
|
aa04fb345c | ||
|
3f79756f70 | ||
|
8220216893 | ||
0d302cf0c1 | |||
|
1e3a604d0f | ||
|
a65ef2ae1b | ||
|
94ba331ed0 | ||
|
59aa780654 | ||
|
0116d405b2 | ||
|
9f58414e99 | ||
|
a52dfc1483 | ||
|
65413e1c94 | ||
|
edeb4003fd | ||
|
4844fa1ead | ||
|
cd34dcb0dd | ||
1f211ca0ab | |||
20b9fce7df | |||
|
baa03424d4 | ||
|
347da823b7 | ||
|
fe4ba1db02 | ||
|
3c68d8e314 | ||
|
1b25ed2904 | ||
|
1ab6a7a9fd | ||
|
474ceb139a | ||
|
b322d3dfa5 | ||
|
7b662801f2 | ||
|
ce21063d7c | ||
|
7b4942fc3d | ||
|
b07202a64b | ||
|
0392c163a0 | ||
|
e5e83d1ad6 | ||
|
ef9550ced5 | ||
|
d7c603e881 | ||
|
3565c897c0 | ||
|
9c27057bf3 | ||
6f8b1e4896 | |||
|
5040deddb6 | ||
|
c9ce29741b | ||
|
cd78fea1f5 | ||
|
0e54d7f124 | ||
|
2511bb00a7 | ||
|
39d9557559 | ||
|
507d88e63e | ||
|
779a11c85a | ||
|
f29636b038 | ||
|
f860821498 | ||
|
e3981c8db2 | ||
|
1c814285c6 | ||
|
56949aad09 | ||
|
7e9aaeafe6 | ||
|
4e68c64daf | ||
|
37afe7b05e | ||
ec6060a6cb | |||
|
e16c28fd40 | ||
|
1aa6aef679 | ||
|
9fe3b02d71 |
312 changed files with 15143 additions and 6480 deletions
4
.gitattributes
vendored
4
.gitattributes
vendored
|
@ -1,9 +1,5 @@
|
|||
* text=auto
|
||||
|
||||
# Temporarily highlight luau as normal lua files
|
||||
# until we get native linguist support for Luau
|
||||
*.luau linguist-language=Lua
|
||||
|
||||
# Ensure all lua files use LF
|
||||
*.lua eol=lf
|
||||
*.luau eol=lf
|
||||
|
|
19
.github/workflows/ci.yaml
vendored
19
.github/workflows/ci.yaml
vendored
|
@ -23,11 +23,8 @@ jobs:
|
|||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v1
|
||||
|
||||
- name: Install Tooling
|
||||
uses: ok-nick/setup-aftman@v0.4.2
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Check Formatting
|
||||
run: just fmt-check
|
||||
|
@ -40,11 +37,8 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v1
|
||||
|
||||
- name: Install Tooling
|
||||
uses: ok-nick/setup-aftman@v0.4.2
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Analyze
|
||||
run: just analyze
|
||||
|
@ -64,9 +58,13 @@ jobs:
|
|||
cargo-target: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: macOS x86_64
|
||||
runner-os: macos-latest
|
||||
runner-os: macos-13
|
||||
cargo-target: x86_64-apple-darwin
|
||||
|
||||
- name: macOS aarch64
|
||||
runner-os: macos-14
|
||||
cargo-target: aarch64-apple-darwin
|
||||
|
||||
name: CI - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runner-os }}
|
||||
steps:
|
||||
|
@ -84,17 +82,20 @@ jobs:
|
|||
- name: Build
|
||||
run: |
|
||||
cargo build \
|
||||
--workspace \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
cargo clippy \
|
||||
--workspace \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
cargo test \
|
||||
--lib --workspace \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
|
|
24
.github/workflows/publish.yaml
vendored
24
.github/workflows/publish.yaml
vendored
|
@ -1,24 +0,0 @@
|
|||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish to crates.io
|
||||
uses: katyo/publish-crates@v2
|
||||
with:
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
ignore-unpublished-changes: true
|
58
.github/workflows/release.yaml
vendored
58
.github/workflows/release.yaml
vendored
|
@ -21,14 +21,32 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from manifest
|
||||
uses: SebRollen/toml-action@9062fbef52816d61278d24ce53c8070440e1e8dd
|
||||
uses: SebRollen/toml-action@v1.2.0
|
||||
id: get_version
|
||||
with:
|
||||
file: Cargo.toml
|
||||
file: crates/lune/Cargo.toml
|
||||
field: package.version
|
||||
|
||||
# dry-run:
|
||||
# name: Dry-run
|
||||
# needs: ["init"]
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
# - name: Install Rust
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# - name: Publish (dry-run)
|
||||
# uses: katyo/publish-crates@v2
|
||||
# with:
|
||||
# dry-run: true
|
||||
# check-repo: true
|
||||
# registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
build:
|
||||
needs: ["init"]
|
||||
needs: ["init"] # , "dry-run"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
@ -70,7 +88,7 @@ jobs:
|
|||
targets: ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v1
|
||||
uses: extractions/setup-just@v2
|
||||
|
||||
- name: Install build tooling (aarch64-unknown-linux-gnu)
|
||||
if: matrix.cargo-target == 'aarch64-unknown-linux-gnu'
|
||||
|
@ -86,24 +104,24 @@ jobs:
|
|||
run: just zip-release ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Upload release artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
path: release.zip
|
||||
|
||||
release:
|
||||
name: Release
|
||||
release-github:
|
||||
name: Release (GitHub)
|
||||
runs-on: ubuntu-latest
|
||||
needs: ["init", "build"]
|
||||
needs: ["init", "build"] # , "dry-run", "build"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v1
|
||||
uses: extractions/setup-just@v2
|
||||
|
||||
- name: Download releases
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./releases
|
||||
|
||||
|
@ -111,7 +129,7 @@ jobs:
|
|||
run: just unpack-releases "./releases"
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -120,3 +138,21 @@ jobs:
|
|||
fail_on_unmatched_files: true
|
||||
files: ./releases/*.zip
|
||||
draft: true
|
||||
|
||||
# release-crates:
|
||||
# name: Release (crates.io)
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: ["init", "dry-run", "build"]
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
# - name: Install Rust
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# - name: Publish crates
|
||||
# uses: katyo/publish-crates@v2
|
||||
# with:
|
||||
# dry-run: false
|
||||
# check-repo: true
|
||||
# registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
|
|
@ -24,7 +24,7 @@ build *ARGS:
|
|||
run FILE_PATH:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo run --bin {{BIN_NAME}} -- "{{FILE_PATH}}"
|
||||
cargo run --bin {{BIN_NAME}} -- run "{{FILE_PATH}}"
|
||||
|
||||
# Run tests for the Lune library
|
||||
[no-exit-message]
|
||||
|
@ -65,9 +65,10 @@ fmt-check:
|
|||
analyze:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
luau-lsp analyze .lune scripts tests types \
|
||||
luau-lsp analyze \
|
||||
--settings=".vscode/settings.json" \
|
||||
--ignore="tests/roblox/rbx-test-files/**"
|
||||
--ignore="tests/roblox/rbx-test-files/**" \
|
||||
.lune scripts tests types
|
||||
|
||||
# Zips up the built binary into a single zip file
|
||||
[no-exit-message]
|
||||
|
|
7
.luaurc
7
.luaurc
|
@ -7,5 +7,10 @@
|
|||
"typeErrors": true,
|
||||
"globals": [
|
||||
"warn"
|
||||
]
|
||||
],
|
||||
"aliases": {
|
||||
"lune": "./types/",
|
||||
"tests": "./tests",
|
||||
"require-tests": "./tests/require/tests"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ end
|
|||
]]
|
||||
|
||||
print("Sending 4 pings to google 🌏")
|
||||
local result = process.spawn("ping", {
|
||||
local result = process.exec("ping", {
|
||||
"google.com",
|
||||
"-c 4",
|
||||
})
|
||||
|
@ -171,7 +171,7 @@ local apiResult = net.request({
|
|||
method = "PATCH",
|
||||
headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
},
|
||||
} :: { [string]: string },
|
||||
body = net.jsonEncode({
|
||||
title = "foo",
|
||||
body = "bar",
|
||||
|
|
|
@ -28,8 +28,8 @@ end)
|
|||
|
||||
for _ = 1, 5 do
|
||||
local start = os.clock()
|
||||
socket.send(tostring(1))
|
||||
local response = socket.next()
|
||||
socket:send(tostring(1))
|
||||
local response = socket:next()
|
||||
local elapsed = os.clock() - start
|
||||
print(`Got response '{response}' in {elapsed * 1_000} milliseconds`)
|
||||
task.wait(1 - elapsed)
|
||||
|
@ -38,7 +38,7 @@ end
|
|||
-- Everything went well, and we are done with the socket, so we can close it
|
||||
|
||||
print("Closing web socket...")
|
||||
socket.close()
|
||||
socket:close()
|
||||
|
||||
task.cancel(forceExit)
|
||||
print("Done! 🌙")
|
||||
|
|
|
@ -15,9 +15,9 @@ local handle = net.serve(PORT, {
|
|||
handleWebSocket = function(socket)
|
||||
print("Got new web socket connection!")
|
||||
repeat
|
||||
local message = socket.next()
|
||||
local message = socket:next()
|
||||
if message ~= nil then
|
||||
socket.send("Echo - " .. message)
|
||||
socket:send("Echo - " .. message)
|
||||
end
|
||||
until message == nil
|
||||
print("Web socket disconnected.")
|
||||
|
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -3,7 +3,9 @@
|
|||
"luau-lsp.types.roblox": false,
|
||||
"luau-lsp.require.mode": "relativeToFile",
|
||||
"luau-lsp.require.directoryAliases": {
|
||||
"@lune/": "./types/"
|
||||
"@lune/": "./types/",
|
||||
"@tests/": "./tests/",
|
||||
"@require-tests/": "./tests/require/tests/"
|
||||
},
|
||||
"luau-lsp.ignoreGlobs": [
|
||||
"tests/roblox/rbx-test-files/**/*.lua",
|
||||
|
|
442
CHANGELOG.md
442
CHANGELOG.md
|
@ -8,6 +8,416 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## `0.9.0`
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- Added two new process spawning functions - `process.create` and `process.exec`, removing the previous `process.spawn` API completely. ([#211])
|
||||
|
||||
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function.
|
||||
|
||||
The new `process.create` function is a non-blocking process creation API and can be used to interactively
|
||||
read and write stdio of the process.
|
||||
|
||||
```lua
|
||||
local child = process.create("program", {
|
||||
"cli-argument",
|
||||
"other-cli-argument"
|
||||
})
|
||||
|
||||
-- Writing to stdin
|
||||
child.stdin:write("Hello from Lune!")
|
||||
|
||||
-- Reading from stdout
|
||||
local data = child.stdout:read()
|
||||
print(buffer.tostring(data))
|
||||
```
|
||||
|
||||
- WebSocket methods in `net.socket` and `net.serve` now use standard Lua method calling convention and colon syntax.
|
||||
This means `socket.send(...)` is now `socket:send(...)`, `socket.close(...)` is now `socket:close(...)`, and so on.
|
||||
|
||||
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
|
||||
|
||||
### Changed
|
||||
|
||||
- Documentation comments for several standard library properties have been improved ([#248], [#250])
|
||||
- Error messages no longer contain redundant or duplicate stack trace information
|
||||
|
||||
[#178]: https://github.com/lune-org/lune/pull/178
|
||||
[#211]: https://github.com/lune-org/lune/pull/211
|
||||
[#248]: https://github.com/lune-org/lune/pull/248
|
||||
[#250]: https://github.com/lune-org/lune/pull/250
|
||||
|
||||
## `0.8.9` - October 7th, 2024
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated to Luau version `0.640`
|
||||
|
||||
## `0.8.8` - August 22nd, 2024
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed errors when deserializing `Lighting.AttributesSerialize` by updating `rbx-dom` dependencies ([#245])
|
||||
|
||||
[#245]: https://github.com/lune-org/lune/pull/245
|
||||
|
||||
## `0.8.7` - August 10th, 2024
|
||||
|
||||
### Added
|
||||
|
||||
- Added a compression level option to `serde.compress` ([#224])
|
||||
- Added missing vector methods to the `roblox` library ([#228])
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated to Luau version `0.635`
|
||||
- Updated to rbx-dom database version `0.634`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `fs.readDir` with trailing forward-slash on Windows ([#220])
|
||||
- Fixed `__type` and `__tostring` metamethods not always being respected when formatting tables
|
||||
|
||||
[#220]: https://github.com/lune-org/lune/pull/220
|
||||
[#224]: https://github.com/lune-org/lune/pull/224
|
||||
[#228]: https://github.com/lune-org/lune/pull/228
|
||||
|
||||
## `0.8.6` - June 23rd, 2024
|
||||
|
||||
### Added
|
||||
|
||||
- Added a builtin API for hashing and calculating HMACs as part of the `serde` library ([#193])
|
||||
|
||||
Basic usage:
|
||||
|
||||
```lua
|
||||
local serde = require("@lune/serde")
|
||||
local hash = serde.hash("sha256", "a message to hash")
|
||||
local hmac = serde.hmac("sha256", "a message to hash", "a secret string")
|
||||
|
||||
print(hash)
|
||||
print(hmac)
|
||||
```
|
||||
|
||||
The returned hashes are sequences of lowercase hexadecimal digits. The following algorithms are supported:
|
||||
`md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`, `blake3`
|
||||
|
||||
- Added two new options to `luau.load`:
|
||||
|
||||
- `codegenEnabled` - whether or not codegen should be enabled for the loaded chunk.
|
||||
- `injectGlobals` - whether or not to inject globals into a passed `environment`.
|
||||
|
||||
By default, globals are injected and codegen is disabled.
|
||||
Check the documentation for the `luau` standard library for more information.
|
||||
|
||||
- Implemented support for floor division operator / `__idiv` for the `Vector2` and `Vector3` types in the `roblox` standard library ([#196])
|
||||
- Fixed the `_VERSION` global containing an incorrect Lune version string.
|
||||
|
||||
### Changed
|
||||
|
||||
- Sandboxing and codegen in the Luau VM is now fully enabled, resulting in up to 2x or faster code execution.
|
||||
This should not result in any behavior differences in Lune, but if it does, please open an issue.
|
||||
- Improved formatting of custom error objects (such as when `fs.readFile` returns an error) when printed or formatted using `stdio.format`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `__type` and `__tostring` metamethods on userdatas and tables not being respected when printed or formatted using `stdio.format`.
|
||||
|
||||
[#193]: https://github.com/lune-org/lune/pull/193
|
||||
[#196]: https://github.com/lune-org/lune/pull/196
|
||||
|
||||
## `0.8.5` - June 1st, 2024
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved table pretty formatting when using `print`, `warn`, and `stdio.format`:
|
||||
|
||||
- Keys are sorted numerically / alphabetically when possible.
|
||||
- Keys of different types are put in distinct sections for mixed tables.
|
||||
- Tables that are arrays no longer display their keys.
|
||||
- Empty tables are no longer spread across lines.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fixed formatted values in tables not being separated by newlines.
|
||||
- Fixed panicking (crashing) when using `process.spawn` with a program that does not exist.
|
||||
- Fixed `instance:SetAttribute("name", nil)` throwing an error and not removing the attribute.
|
||||
|
||||
## `0.8.4` - May 12th, 2024
|
||||
|
||||
### Added
|
||||
|
||||
- Added a builtin API for regular expressions.
|
||||
|
||||
Example basic usage:
|
||||
|
||||
```lua
|
||||
local Regex = require("@lune/regex")
|
||||
|
||||
local re = Regex.new("hello")
|
||||
|
||||
if re:isMatch("hello, world!") then
|
||||
print("Matched!")
|
||||
end
|
||||
|
||||
local caps = re:captures("hello, world! hello, again!")
|
||||
|
||||
print(#caps) -- 2
|
||||
print(caps:get(1)) -- "hello"
|
||||
print(caps:get(2)) -- "hello"
|
||||
print(caps:get(3)) -- nil
|
||||
```
|
||||
|
||||
Check out the documentation for more details.
|
||||
|
||||
- Added support for buffers as arguments in builtin APIs ([#148])
|
||||
|
||||
This includes APIs such as `fs.writeFile`, `serde.encode`, and more.
|
||||
|
||||
- Added support for cross-compilation of standalone binaries ([#162])
|
||||
|
||||
You can now compile standalone binaries for other platforms by passing
|
||||
an additional `target` argument to the `build` subcommand:
|
||||
|
||||
```sh
|
||||
lune build my-file.luau --output my-bin --target windows-x86_64
|
||||
```
|
||||
|
||||
Currently supported targets are the same as the ones included with each
|
||||
release of Lune on GitHub. Check releases for a full list of targets.
|
||||
|
||||
- Added `stdio.readToEnd()` for reading the entire stdin passed to Lune
|
||||
|
||||
### Changed
|
||||
|
||||
- Split the repository into modular crates instead of a monolith. ([#188])
|
||||
|
||||
If you previously depended on Lune as a crate, nothing about it has changed for version `0.8.4`, but now each individual sub-crate has also been published and is available for use:
|
||||
|
||||
- `lune` (old)
|
||||
- `lune-utils`
|
||||
- `lune-roblox`
|
||||
- `lune-std-*` for every builtin library
|
||||
|
||||
When depending on the main `lune` crate, each builtin library also has a feature flag that can be toggled in the format `std-*`.
|
||||
|
||||
In general, this should mean that it is now much easier to make your own Lune builtin, publish your own flavor of a Lune CLI, or take advantage of all the work that has been done for Lune as a runtime when making your own Rust programs.
|
||||
|
||||
- Changed the `User-Agent` header in `net.request` to be more descriptive ([#186])
|
||||
- Updated to Luau version `0.622`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed not being able to decompress `lz4` format in high compression mode
|
||||
- Fixed stack overflow for tables with circular keys ([#183])
|
||||
- Fixed `net.serve` no longer accepting ipv6 addresses
|
||||
- Fixed headers in `net.serve` being raw bytes instead of strings
|
||||
|
||||
[#148]: https://github.com/lune-org/lune/pull/148
|
||||
[#162]: https://github.com/lune-org/lune/pull/162
|
||||
[#183]: https://github.com/lune-org/lune/pull/183
|
||||
[#186]: https://github.com/lune-org/lune/pull/186
|
||||
[#188]: https://github.com/lune-org/lune/pull/188
|
||||
|
||||
## `0.8.3` - April 15th, 2024
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `require` not throwing syntax errors ([#168])
|
||||
- Fixed `require` caching not working correctly ([#171])
|
||||
- Fixed case-sensitivity issue in `require` with aliases ([#173])
|
||||
- Fixed `itertools` dependency being marked optional even though it is mandatory ([#176])
|
||||
- Fixed test cases for the `net` built-in library on Windows ([#177])
|
||||
|
||||
[#168]: https://github.com/lune-org/lune/pull/168
|
||||
[#171]: https://github.com/lune-org/lune/pull/171
|
||||
[#173]: https://github.com/lune-org/lune/pull/173
|
||||
[#176]: https://github.com/lune-org/lune/pull/176
|
||||
[#177]: https://github.com/lune-org/lune/pull/177
|
||||
|
||||
## `0.8.2` - March 12th, 2024
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed REPL panicking after the first evaluation / run.
|
||||
- Fixed globals reloading on each run in the REPL, causing unnecessary slowdowns.
|
||||
- Fixed `net.serve` requests no longer being plain tables in Lune `0.8.1`, breaking usage of things such as `table.clone`.
|
||||
|
||||
## `0.8.1` - March 11th, 2024
|
||||
|
||||
### Added
|
||||
|
||||
- Added the ability to specify an address in `net.serve`. ([#142])
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Luau version `0.616`.
|
||||
- Major performance improvements when using a large amount of threads / asynchronous Lune APIs. ([#165])
|
||||
- Minor performance improvements and less overhead for `net.serve` and `net.socket`. ([#165])
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `fs.copy` not working with empty dirs. ([#155])
|
||||
- Fixed stack overflow when printing tables with cyclic references. ([#158])
|
||||
- Fixed not being able to yield in `net.serve` handlers without blocking other requests. ([#165])
|
||||
- Fixed various scheduler issues / panics. ([#165])
|
||||
|
||||
[#142]: https://github.com/lune-org/lune/pull/142
|
||||
[#155]: https://github.com/lune-org/lune/pull/155
|
||||
[#158]: https://github.com/lune-org/lune/pull/158
|
||||
[#165]: https://github.com/lune-org/lune/pull/165
|
||||
|
||||
## `0.8.0` - January 14th, 2024
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- The Lune CLI now uses subcommands instead of flag options: <br/>
|
||||
|
||||
- `lune script_name arg1 arg2 arg3` -> `lune run script_name arg1 arg2 arg3`
|
||||
- `lune --list` -> `lune list`
|
||||
- `lune --setup` -> `lune setup`
|
||||
|
||||
This unfortunately hurts ergonomics for quickly running scripts but is a necessary change to allow us to add more commands, such as the new `build` subcommand.
|
||||
|
||||
- The `createdAt`, `modifiedAt`, and `accessedAt` properties returned from `fs.metadata` are now `DateTime` values instead of numbers.
|
||||
|
||||
- The `Lune` struct has been renamed to `Runtime` in the Lune rust crate.
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for compiling single Lune scripts into standalone executables! ([#140])
|
||||
|
||||
Example usage:
|
||||
|
||||
```lua
|
||||
-- my_cool_script.luau
|
||||
print("Hello, standalone!")
|
||||
```
|
||||
|
||||
```sh
|
||||
> lune build my_cool_script.luau
|
||||
# Creates `my_cool_script.exe` (Windows) or `my_cool_script` (macOS / Linux)
|
||||
```
|
||||
|
||||
```sh
|
||||
> ./my_cool_script.exe # Windows
|
||||
> ./my_cool_script # macOS / Linux
|
||||
> "Hello, standalone!"
|
||||
```
|
||||
|
||||
To compile scripts that use `require` and reference multiple files, a bundler such as [darklua](https://github.com/seaofvoices/darklua) should preferrably be used. You may also distribute files alongside the standalone binary, they will still be able to be `require`-d. This limitation will be lifted in the future and Lune will automatically bundle any referenced scripts.
|
||||
|
||||
- Added support for path aliases using `.luaurc` config files!
|
||||
|
||||
For full documentation and reference, check out the [official Luau RFC](https://rfcs.luau-lang.org/require-by-string-aliases.html), but here's a quick example:
|
||||
|
||||
```jsonc
|
||||
// .luaurc
|
||||
{
|
||||
"aliases": {
|
||||
"modules": "./some/long/path/to/modules"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```lua
|
||||
-- ./some/long/path/to/modules/foo.luau
|
||||
return { World = "World!" }
|
||||
|
||||
-- ./anywhere/you/want/my_script.luau
|
||||
local mod = require("@modules/foo")
|
||||
print("Hello, " .. mod.World)
|
||||
```
|
||||
|
||||
- Added support for multiple values for a single query, and multiple values for a single header, in `net.request`. This is a part of the HTTP specification that is not widely used but that may be useful in certain cases. To clarify:
|
||||
|
||||
- Single values remain unchanged and will work exactly the same as before. <br/>
|
||||
|
||||
```lua
|
||||
-- https://example.com/?foo=bar&baz=qux
|
||||
local net = require("@lune/net")
|
||||
net.request({
|
||||
url = "example.com",
|
||||
query = {
|
||||
foo = "bar",
|
||||
baz = "qux",
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- Multiple values _on a single query / header_ are represented as an ordered array of strings. <br/>
|
||||
|
||||
```lua
|
||||
-- https://example.com/?foo=first&foo=second&foo=third&bar=baz
|
||||
local net = require("@lune/net")
|
||||
net.request({
|
||||
url = "example.com",
|
||||
query = {
|
||||
foo = { "first", "second", "third" },
|
||||
bar = "baz",
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
[#140]: https://github.com/lune-org/lune/pull/140
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Luau version `0.606`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the `print` and `warn` global functions yielding the thread, preventing them from being used in places such as the callback to `table.sort`.
|
||||
- Fixed the `overwrite` option for `fs.move` not correctly removing existing files / directories. ([#133])
|
||||
|
||||
[#133]: https://github.com/lune-org/lune/pull/133
|
||||
|
||||
## `0.7.11` - October 29th, 2023
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Luau version `0.601`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `roblox.getAuthCookie` not being compatible with the latest cookie format by upgrading rbx_cookie.
|
||||
|
||||
## `0.7.10` - October 25th, 2023
|
||||
|
||||
### Added
|
||||
|
||||
- Added the `GetDebugId` instance method to the `roblox` built-in. This will return the internal id used by the instance, and as the name implies, it should be primarily used for _debugging_ purposes and cases where you need a globally unique identifier for an instance. It is guaranteed to be a 32-digit hexadecimal string.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issues with `SecurityCapabilities` on instances in the `roblox` built-in by upgrading rbx-dom.
|
||||
|
||||
## `0.7.9` - October 21st, 2023
|
||||
|
||||
### Added
|
||||
|
||||
- Added `implementProperty` and `implementMethod` to the `roblox` built-in library to fill in missing functionality that Lune does not aim to implement itself.
|
||||
|
||||
Example usage:
|
||||
|
||||
```lua
|
||||
local roblox = require("@lune/roblox")
|
||||
|
||||
local part = roblox.Instance.new("Part")
|
||||
|
||||
roblox.implementMethod("BasePart", "TestMethod", function(_, ...)
|
||||
print("Tried to call TestMethod with", ...)
|
||||
end)
|
||||
|
||||
part:TestMethod("Hello", "world!")
|
||||
```
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Luau version `0.599`.
|
||||
- Stdio options when using `process.spawn` can now be set with more granularity, allowing stderr & stdout to be disabled individually and completely to improve memory usage when they are not being used.
|
||||
|
||||
## `0.7.8` - October 5th, 2023
|
||||
|
||||
### Added
|
||||
|
@ -74,13 +484,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Fixed various functions for `CFrame` in the `roblox` built-in library being incorrect, specifically row-column ordering and some flipped signs. ([#103])
|
||||
- Fixed cross-service Instance references disappearing when using the `roblox` built-in library ([#117])
|
||||
|
||||
[#85]: https://github.com/filiptibell/lune/pull/85
|
||||
[#93]: https://github.com/filiptibell/lune/pull/93
|
||||
[#94]: https://github.com/filiptibell/lune/pull/94
|
||||
[#102]: https://github.com/filiptibell/lune/pull/102
|
||||
[#103]: https://github.com/filiptibell/lune/pull/103
|
||||
[#106]: https://github.com/filiptibell/lune/pull/106
|
||||
[#117]: https://github.com/filiptibell/lune/pull/117
|
||||
[#85]: https://github.com/lune-org/lune/pull/85
|
||||
[#93]: https://github.com/lune-org/lune/pull/93
|
||||
[#94]: https://github.com/lune-org/lune/pull/94
|
||||
[#102]: https://github.com/lune-org/lune/pull/102
|
||||
[#103]: https://github.com/lune-org/lune/pull/103
|
||||
[#106]: https://github.com/lune-org/lune/pull/106
|
||||
[#117]: https://github.com/lune-org/lune/pull/117
|
||||
|
||||
## `0.7.7` - August 23rd, 2023
|
||||
|
||||
|
@ -160,9 +570,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Fixed spurious panics and error messages such as `Tried to resume next queued future but none are queued`.
|
||||
- Fixed not being able to catch non-string errors properly, errors were accidentally being wrapped in an opaque `userdata` type.
|
||||
|
||||
[#82]: https://github.com/filiptibell/lune/pull/82
|
||||
[#83]: https://github.com/filiptibell/lune/pull/83
|
||||
[#86]: https://github.com/filiptibell/lune/pull/86
|
||||
[#82]: https://github.com/lune-org/lune/pull/82
|
||||
[#83]: https://github.com/lune-org/lune/pull/83
|
||||
[#86]: https://github.com/lune-org/lune/pull/86
|
||||
|
||||
## `0.7.6` - August 9th, 2023
|
||||
|
||||
|
@ -257,9 +667,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Fixed not being able to read & write to WebSocket objects at the same time. ([#68])
|
||||
- Fixed tab character at the start of a script causing it not to parse correctly. ([#72])
|
||||
|
||||
[#62]: https://github.com/filiptibell/lune/pull/62
|
||||
[#68]: https://github.com/filiptibell/lune/pull/66
|
||||
[#72]: https://github.com/filiptibell/lune/pull/72
|
||||
[#62]: https://github.com/lune-org/lune/pull/62
|
||||
[#68]: https://github.com/lune-org/lune/pull/66
|
||||
[#72]: https://github.com/lune-org/lune/pull/72
|
||||
|
||||
## `0.7.4` - July 7th, 2023
|
||||
|
||||
|
@ -277,7 +687,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- When using `roblox.serializeModel`, Lune will no longer keep internal unique ids. <br/>
|
||||
This is consistent with what Roblox does and prevents Lune from always generating a new and unique file. <br/>
|
||||
This previously caused unnecessary diffs when using git or other kinds of source control. ([Relevant issue](https://github.com/filiptibell/lune/issues/61))
|
||||
This previously caused unnecessary diffs when using git or other kinds of source control. ([Relevant issue](https://github.com/lune-org/lune/issues/61))
|
||||
|
||||
## `0.7.2` - June 28th, 2023
|
||||
|
||||
|
@ -302,7 +712,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Fixed `closeCode` erroring when being accessed on websockets. ([#57])
|
||||
- Fixed issues with `UniqueId` when using the `roblox` builtin by downgrading `rbx-dom`.
|
||||
|
||||
[#57]: https://github.com/filiptibell/lune/pull/57
|
||||
[#57]: https://github.com/lune-org/lune/pull/57
|
||||
|
||||
## `0.7.0` - June 12th, 2023
|
||||
|
||||
|
@ -371,7 +781,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Fixed `stdio.write` and `stdio.ewrite` not being flushed and causing output to be interleaved. ([#47])
|
||||
- Fixed `typeof` returning `userdata` for roblox types such as `Instance`, `Vector3`, ...
|
||||
|
||||
[#47]: https://github.com/filiptibell/lune/pull/47
|
||||
[#47]: https://github.com/lune-org/lune/pull/47
|
||||
|
||||
## `0.6.7` - May 14th, 2023
|
||||
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
### Reporting a Bug
|
||||
|
||||
- Make sure the bug has not already been reported by searching on GitHub under [Issues](https://github.com/filiptibell/lune/issues).
|
||||
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/filiptibell/lune/issues/new). Be sure to include a **title and description**, as much relevant information as possible, and if applicable, a **code sample** or a **test case** demonstrating the expected behavior.
|
||||
- Make sure the bug has not already been reported by searching on GitHub under [Issues](https://github.com/lune-org/lune/issues).
|
||||
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/lune-org/lune/issues/new). Be sure to include a **title and description**, as much relevant information as possible, and if applicable, a **code sample** or a **test case** demonstrating the expected behavior.
|
||||
|
||||
---
|
||||
|
||||
### Contributing - Bug Fixes
|
||||
|
||||
1. Make sure an [issue](https://github.com/filiptibell/lune/issues) has been created for the bug first, so that it can be tracked and searched for in the repository history. This is not mandatory for small fixes.
|
||||
1. Make sure an [issue](https://github.com/lune-org/lune/issues) has been created for the bug first, so that it can be tracked and searched for in the repository history. This is not mandatory for small fixes.
|
||||
2. Open a new GitHub pull request for it. A pull request for a bug fix must include:
|
||||
- A clear and concise description of the bug it is fixing.
|
||||
- A new test file ensuring there are no regressions after the bug has been fixed.
|
||||
|
@ -22,11 +22,11 @@
|
|||
|
||||
### Contributing - Features
|
||||
|
||||
1. Make sure an [issue](https://github.com/filiptibell/lune/issues) has been created for the feature first, so that it can be tracked and searched for in the repository history. If you are making changes to an existing feature, and no issue exists, one should be created for the proposed changes.
|
||||
1. Make sure an [issue](https://github.com/lune-org/lune/issues) has been created for the feature first, so that it can be tracked and searched for in the repository history. If you are making changes to an existing feature, and no issue exists, one should be created for the proposed changes.
|
||||
2. Any API design or considerations should first be brought up and discussed in the relevant issue, to prevent long review times on pull requests and unnecessary work for maintainers.
|
||||
3. Familiarize yourself with the codebase and the tools you will be using. Some important parts include:
|
||||
- The [mlua](https://crates.io/crates/mlua) library, which we use to interface with Luau.
|
||||
- Any [built-in libraries](https://github.com/filiptibell/lune/tree/main/src/lune/builtins) that are relevant for your new feature. If you are making a new built-in library, refer to existing ones for structure and implementation details.
|
||||
- Any [built-in libraries](https://github.com/lune-org/lune/tree/main/src/lune/builtins) that are relevant for your new feature. If you are making a new built-in library, refer to existing ones for structure and implementation details.
|
||||
- Our toolchain, notably [StyLua](https://github.com/JohnnyMorganz/StyLua), [rustfmt](https://github.com/rust-lang/rustfmt), and [clippy](https://github.com/rust-lang/rust-clippy). If you do not use these tools there is a decent chance CI will fail on your pull request, blocking it from getting approved.
|
||||
4. Write some code!
|
||||
5. Open a new GitHub pull request. A pull request for a feature must include:
|
||||
|
@ -48,7 +48,7 @@ Check out the [docs](https://github.com/lune-org/docs) repository and its contri
|
|||
|
||||
If type definitions for built-in libraries need improvements:
|
||||
|
||||
1. Check out the [types](https://github.com/filiptibell/lune/tree/main/types) directory at the root of the repository.
|
||||
1. Check out the [types](https://github.com/lune-org/lune/tree/main/types) directory at the root of the repository.
|
||||
2. Make the desired changes, and verify that they have the desired outcome.
|
||||
3. Open a new GitHub pull request for your changes.
|
||||
|
||||
|
@ -61,7 +61,7 @@ The Lune release process is semi-automated, and takes care of most things for yo
|
|||
1. Make sure the changelog is up to date and contains all of the changes since the last release.
|
||||
2. Add the release date in the changelog + set a new version number in `Cargo.toml`.
|
||||
3. Commit and push changes from step 2 to GitHub. This will automatically publish the Lune library to [crates.io](https://crates.io) when the version number changes.
|
||||
4. Trigger the [release](https://github.com/filiptibell/lune/actions/workflows/release.yaml) workflow on GitHub manually, and wait for it to finish. Find the new pending release in the [Releases](https://github.com/filiptibell/lune/releases) section.
|
||||
4. Trigger the [release](https://github.com/lune-org/lune/actions/workflows/release.yaml) workflow on GitHub manually, and wait for it to finish. Find the new pending release in the [Releases](https://github.com/lune-org/lune/releases) section.
|
||||
5. Add in changes from the changelog for the new pending release into the description, hit "accept" on creating a new version tag, and publish 🚀
|
||||
|
||||
---
|
||||
|
|
2524
Cargo.lock
generated
2524
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
161
Cargo.toml
161
Cargo.toml
|
@ -1,42 +1,22 @@
|
|||
[package]
|
||||
name = "lune"
|
||||
version = "0.7.8"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/filiptibell/lune"
|
||||
description = "A standalone Luau runtime"
|
||||
readme = "README.md"
|
||||
keywords = ["cli", "lua", "luau", "runtime"]
|
||||
categories = ["command-line-interface"]
|
||||
|
||||
[[bin]]
|
||||
name = "lune"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "lune"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["cli", "roblox"]
|
||||
cli = [
|
||||
"dep:anyhow",
|
||||
"dep:env_logger",
|
||||
"dep:itertools",
|
||||
"dep:clap",
|
||||
"dep:include_dir",
|
||||
"dep:regex",
|
||||
"dep:rustyline",
|
||||
]
|
||||
roblox = [
|
||||
"dep:glam",
|
||||
"dep:rand",
|
||||
"dep:rbx_cookie",
|
||||
"dep:rbx_binary",
|
||||
"dep:rbx_dom_weak",
|
||||
"dep:rbx_reflection",
|
||||
"dep:rbx_reflection_database",
|
||||
"dep:rbx_xml",
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
default-members = ["crates/lune"]
|
||||
members = [
|
||||
"crates/lune",
|
||||
"crates/lune-roblox",
|
||||
"crates/lune-std",
|
||||
"crates/lune-std-datetime",
|
||||
"crates/lune-std-fs",
|
||||
"crates/lune-std-luau",
|
||||
"crates/lune-std-net",
|
||||
"crates/lune-std-process",
|
||||
"crates/lune-std-regex",
|
||||
"crates/lune-std-roblox",
|
||||
"crates/lune-std-serde",
|
||||
"crates/lune-std-stdio",
|
||||
"crates/lune-std-task",
|
||||
"crates/lune-utils",
|
||||
"crates/mlua-luau-scheduler",
|
||||
]
|
||||
|
||||
# Profile for building the release binary, with the following options set:
|
||||
|
@ -54,86 +34,31 @@ opt-level = "z"
|
|||
strip = true
|
||||
lto = true
|
||||
|
||||
# All of the dependencies for Lune.
|
||||
# Lints for all crates in the workspace
|
||||
#
|
||||
# Dependencies are categorized as following:
|
||||
#
|
||||
# 1. General dependencies with no specific features set
|
||||
# 2. Large / core dependencies that have many different crates and / or features set
|
||||
# 3. Dependencies for specific features of Lune, eg. the CLI or massive Roblox builtin library
|
||||
#
|
||||
[dependencies]
|
||||
console = "0.15"
|
||||
directories = "5.0"
|
||||
futures-util = "0.3"
|
||||
once_cell = "1.17"
|
||||
thiserror = "1.0"
|
||||
async-trait = "0.1"
|
||||
dialoguer = "0.11"
|
||||
dunce = "1.0"
|
||||
lz4_flex = "0.11"
|
||||
path-clean = "1.0"
|
||||
pin-project = "1.0"
|
||||
os_str_bytes = "6.4"
|
||||
urlencoding = "2.1"
|
||||
# 1. Error on all lints by default, then make cargo + clippy pedantic lints just warn
|
||||
# 2. Selectively allow some lints that are _too_ pedantic, such as:
|
||||
# - Casts between number types
|
||||
# - Module naming conventions
|
||||
# - Imports and multiple dependency versions
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "deny", priority = -3 }
|
||||
cargo = { level = "warn", priority = -2 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
||||
### RUNTIME
|
||||
cast_lossless = { level = "allow", priority = 1 }
|
||||
cast_possible_truncation = { level = "allow", priority = 1 }
|
||||
cast_possible_wrap = { level = "allow", priority = 1 }
|
||||
cast_precision_loss = { level = "allow", priority = 1 }
|
||||
cast_sign_loss = { level = "allow", priority = 1 }
|
||||
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
mlua = { version = "0.9.1", features = ["luau", "luau-jit", "serialize"] }
|
||||
tokio = { version = "1.24", features = ["full", "tracing"] }
|
||||
similar_names = { level = "allow", priority = 1 }
|
||||
unnecessary_wraps = { level = "allow", priority = 1 }
|
||||
unnested_or_patterns = { level = "allow", priority = 1 }
|
||||
unreadable_literal = { level = "allow", priority = 1 }
|
||||
|
||||
### SERDE
|
||||
|
||||
async-compression = { version = "0.4", features = [
|
||||
"tokio",
|
||||
"brotli",
|
||||
"deflate",
|
||||
"gzip",
|
||||
"zlib",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
serde_yaml = "0.9"
|
||||
toml = { version = "0.8", features = ["preserve_order"] }
|
||||
|
||||
### NET
|
||||
|
||||
hyper = { version = "0.14", features = ["full"] }
|
||||
hyper-tungstenite = { version = "0.11" }
|
||||
reqwest = { version = "0.11", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
] }
|
||||
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
|
||||
|
||||
### DATETIME
|
||||
chrono = "0.4"
|
||||
chrono_lc = "0.1"
|
||||
num-traits = "0.2"
|
||||
|
||||
### CLI
|
||||
|
||||
anyhow = { optional = true, version = "1.0" }
|
||||
env_logger = { optional = true, version = "0.10" }
|
||||
itertools = { optional = true, version = "0.11" }
|
||||
clap = { optional = true, version = "4.1", features = ["derive"] }
|
||||
include_dir = { optional = true, version = "0.7", features = ["glob"] }
|
||||
regex = { optional = true, version = "1.7", default-features = false, features = [
|
||||
"std",
|
||||
"unicode-perl",
|
||||
] }
|
||||
rustyline = { optional = true, version = "12.0" }
|
||||
|
||||
### ROBLOX
|
||||
|
||||
glam = { optional = true, version = "0.24" }
|
||||
rand = { optional = true, version = "0.8" }
|
||||
|
||||
rbx_cookie = { optional = true, version = "0.1.3", default-features = false }
|
||||
|
||||
rbx_binary = { optional = true, version = "0.7.2" }
|
||||
rbx_dom_weak = { optional = true, version = "2.6.0" }
|
||||
rbx_reflection = { optional = true, version = "4.4.0" }
|
||||
rbx_reflection_database = { optional = true, version = "0.2.9" }
|
||||
rbx_xml = { optional = true, version = "0.13.2" }
|
||||
multiple_crate_versions = { level = "allow", priority = 1 }
|
||||
module_inception = { level = "allow", priority = 1 }
|
||||
module_name_repetitions = { level = "allow", priority = 1 }
|
||||
needless_pass_by_value = { level = "allow", priority = 1 }
|
||||
wildcard_imports = { level = "allow", priority = 1 }
|
||||
|
|
14
README.md
14
README.md
|
@ -10,14 +10,14 @@
|
|||
<a href="https://crates.io/crates/lune">
|
||||
<img src="https://img.shields.io/crates/v/lune.svg?label=Version" alt="Current Lune library version" />
|
||||
</a>
|
||||
<a href="https://github.com/filiptibell/lune/actions">
|
||||
<img src="https://shields.io/endpoint?url=https://badges.readysetplay.io/workflow/filiptibell/lune/ci.yaml" alt="CI status" />
|
||||
<a href="https://github.com/lune-org/lune/actions">
|
||||
<img src="https://shields.io/endpoint?url=https://badges.readysetplay.io/workflow/lune-org/lune/ci.yaml" alt="CI status" />
|
||||
</a>
|
||||
<a href="https://github.com/filiptibell/lune/actions">
|
||||
<img src="https://shields.io/endpoint?url=https://badges.readysetplay.io/workflow/filiptibell/lune/release.yaml" alt="Release status" />
|
||||
<a href="https://github.com/lune-org/lune/actions">
|
||||
<img src="https://shields.io/endpoint?url=https://badges.readysetplay.io/workflow/lune-org/lune/release.yaml" alt="Release status" />
|
||||
</a>
|
||||
<a href="https://github.com/filiptibell/lune/blob/main/LICENSE.txt">
|
||||
<img src="https://img.shields.io/github/license/filiptibell/lune.svg?label=License&color=informational" alt="Lune license" />
|
||||
<a href="https://github.com/lune-org/lune/blob/main/LICENSE.txt">
|
||||
<img src="https://img.shields.io/github/license/lune-org/lune.svg?label=License&color=informational" alt="Lune license" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@ Lune provides fully asynchronous APIs wherever possible, and is built in Rust
|
|||
## Features
|
||||
|
||||
- 🌙 Strictly minimal but powerful interface that is easy to read and remember, just like Luau itself
|
||||
- 🧰 Fully featured APIs for the filesystem, networking, stdio, all included in the small (~5mb) executable
|
||||
- 🧰 Fully featured APIs for the filesystem, networking, stdio, all included in the small (~5mb zipped) executable
|
||||
- 📚 World-class documentation, on the web _or_ directly in your editor, no network connection necessary
|
||||
- 🏡 Familiar runtime environment for Roblox developers, with an included 1-to-1 task scheduler port
|
||||
- ✏️ Optional built-in library for manipulating Roblox place & model files, and their instances
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
[tools]
|
||||
luau-lsp = "JohnnyMorganz/luau-lsp@1.24.1"
|
||||
selene = "Kampfkarren/selene@0.25.0"
|
||||
stylua = "JohnnyMorganz/StyLua@0.18.2"
|
29
crates/lune-roblox/Cargo.toml
Normal file
29
crates/lune-roblox/Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "lune-roblox"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Roblox library for Lune"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau"] }
|
||||
|
||||
glam = "0.27"
|
||||
rand = "0.8"
|
||||
thiserror = "1.0"
|
||||
once_cell = "1.17"
|
||||
|
||||
rbx_binary = "1.0.0"
|
||||
rbx_dom_weak = "3.0.0"
|
||||
rbx_reflection = "5.0.0"
|
||||
rbx_reflection_database = "1.0.0"
|
||||
rbx_xml = "1.0.0"
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
|
@ -4,6 +4,15 @@ use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
|
|||
|
||||
use super::extension::DomValueExt;
|
||||
|
||||
/**
|
||||
Checks if the given name is a valid attribute name.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the name starts with the prefix "RBX".
|
||||
- If the name contains any characters other than alphanumeric characters and underscore.
|
||||
- If the name is longer than 100 characters.
|
||||
*/
|
||||
pub fn ensure_valid_attribute_name(name: impl AsRef<str>) -> LuaResult<()> {
|
||||
let name = name.as_ref();
|
||||
if name.to_ascii_uppercase().starts_with("RBX") {
|
||||
|
@ -23,6 +32,13 @@ pub fn ensure_valid_attribute_name(name: impl AsRef<str>) -> LuaResult<()> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if the given value is a valid attribute value.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the value is not a valid attribute type.
|
||||
*/
|
||||
pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
|
||||
let is_valid = matches!(
|
||||
value.ty(),
|
||||
|
@ -31,6 +47,7 @@ pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
|
|||
| DomType::CFrame
|
||||
| DomType::Color3
|
||||
| DomType::ColorSequence
|
||||
| DomType::EnumItem
|
||||
| DomType::Float32
|
||||
| DomType::Float64
|
||||
| DomType::Font
|
|
@ -2,7 +2,7 @@ use mlua::prelude::*;
|
|||
|
||||
use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
|
||||
|
||||
use crate::roblox::{datatypes::extension::DomValueExt, instance::Instance};
|
||||
use crate::{datatypes::extension::DomValueExt, instance::Instance};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -51,7 +51,7 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
|
|||
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
|
||||
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
|
||||
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
|
||||
DomValue::Content(s) => Ok(LuaValue::String(
|
||||
DomValue::ContentId(s) => Ok(LuaValue::String(
|
||||
lua.create_string(AsRef::<str>::as_ref(s))?,
|
||||
)),
|
||||
|
||||
|
@ -65,8 +65,10 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
|
|||
|
||||
// NOTE: Some values are either optional or default and we should handle
|
||||
// that properly here since the userdata conversion above will always fail
|
||||
DomValue::OptionalCFrame(None) => Ok(LuaValue::Nil),
|
||||
DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => Ok(LuaValue::Nil),
|
||||
DomValue::OptionalCFrame(None)
|
||||
| DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => {
|
||||
Ok(LuaValue::Nil)
|
||||
}
|
||||
|
||||
_ => Err(e),
|
||||
},
|
||||
|
@ -102,8 +104,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
|
|||
(LuaValue::String(s), DomType::BinaryString) => {
|
||||
Ok(DomValue::BinaryString(s.as_ref().into()))
|
||||
}
|
||||
(LuaValue::String(s), DomType::Content) => {
|
||||
Ok(DomValue::Content(s.to_str()?.to_string().into()))
|
||||
(LuaValue::String(s), DomType::ContentId) => {
|
||||
Ok(DomValue::ContentId(s.to_str()?.to_string().into()))
|
||||
}
|
||||
|
||||
// NOTE: Some values are either optional or default and we
|
||||
|
@ -198,6 +200,8 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
|
|||
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
|
||||
DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3),
|
||||
DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
|
||||
DomValue::Content(value) => dom_to_userdata!(lua, value => Content),
|
||||
DomValue::EnumItem(value) => dom_to_userdata!(lua, value => EnumItem),
|
||||
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
|
||||
DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
|
||||
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
|
||||
|
@ -254,7 +258,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
|
|||
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
|
||||
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
|
||||
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
|
||||
DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum),
|
||||
DomType::Content => userdata_to_dom!(self as Content => dom::Content),
|
||||
DomType::EnumItem => userdata_to_dom!(self as EnumItem => dom::EnumItem),
|
||||
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
|
||||
DomType::Font => userdata_to_dom!(self as Font => dom::Font),
|
||||
DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange),
|
||||
|
@ -312,7 +317,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
|
|||
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame),
|
||||
value if value.is::<Color3>() => userdata_to_dom!(value as Color3 => dom::Color3),
|
||||
value if value.is::<ColorSequence>() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence),
|
||||
value if value.is::<Enum>() => userdata_to_dom!(value as EnumItem => dom::Enum),
|
||||
value if value.is::<EnumItem>() => userdata_to_dom!(value as EnumItem => dom::EnumItem),
|
||||
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces),
|
||||
value if value.is::<Font>() => userdata_to_dom!(value as Font => dom::Font),
|
||||
value if value.is::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),
|
|
@ -6,6 +6,7 @@ pub(crate) trait DomValueExt {
|
|||
|
||||
impl DomValueExt for DomType {
|
||||
fn variant_name(&self) -> Option<&'static str> {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use DomType::*;
|
||||
Some(match self {
|
||||
Attributes => "Attributes",
|
||||
|
@ -18,7 +19,9 @@ impl DomValueExt for DomType {
|
|||
Color3uint8 => "Color3uint8",
|
||||
ColorSequence => "ColorSequence",
|
||||
Content => "Content",
|
||||
ContentId => "ContentId",
|
||||
Enum => "Enum",
|
||||
EnumItem => "EnumItem",
|
||||
Faces => "Faces",
|
||||
Float32 => "Float32",
|
||||
Float64 => "Float64",
|
|
@ -10,4 +10,4 @@ mod util;
|
|||
|
||||
use result::*;
|
||||
|
||||
pub use crate::roblox::shared::userdata::*;
|
||||
pub use crate::shared::userdata::*;
|
|
@ -3,7 +3,9 @@ use core::fmt;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Axes as DomAxes;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
|
@ -52,8 +54,7 @@ impl LuaExportsTable<'_> for Axes {
|
|||
check(&e);
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected argument #{} to be an EnumItem, got userdata",
|
||||
index
|
||||
"Expected argument #{index} to be an EnumItem, got userdata",
|
||||
)));
|
||||
}
|
||||
} else {
|
|
@ -4,14 +4,16 @@ use mlua::prelude::*;
|
|||
use rand::seq::SliceRandom;
|
||||
use rbx_dom_weak::types::BrickColor as DomBrickColor;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Color3};
|
||||
|
||||
/**
|
||||
An implementation of the [BrickColor](https://create.roblox.com/docs/reference/engine/datatypes/BrickColor) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the BrickColor class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `BrickColor` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct BrickColor {
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::items_after_statements)]
|
||||
|
||||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
|
@ -5,7 +7,9 @@ use glam::{EulerRot, Mat3, Mat4, Quat, Vec3};
|
|||
use mlua::{prelude::*, Variadic};
|
||||
use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3};
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector3};
|
||||
|
||||
|
@ -14,7 +18,7 @@ use super::{super::*, Vector3};
|
|||
Roblox datatype, backed by [`glam::Mat4`].
|
||||
|
||||
This implements all documented properties, methods &
|
||||
constructors of the CFrame class as of March 2023.
|
||||
constructors of the `CFrame` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CFrame(pub Mat4);
|
||||
|
@ -42,6 +46,7 @@ impl CFrame {
|
|||
impl LuaExportsTable<'_> for CFrame {
|
||||
const EXPORT_NAME: &'static str = "CFrame";
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let cframe_angles = |_, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
|
||||
|
@ -68,8 +73,7 @@ impl LuaExportsTable<'_> for CFrame {
|
|||
Ok(CFrame(Mat4::from_cols(
|
||||
rx.0.extend(0.0),
|
||||
ry.0.extend(0.0),
|
||||
rz.map(|r| r.0)
|
||||
.unwrap_or_else(|| rx.0.cross(ry.0).normalize())
|
||||
rz.map_or_else(|| rx.0.cross(ry.0).normalize(), |r| r.0)
|
||||
.extend(0.0),
|
||||
pos.0.extend(1.0),
|
||||
)))
|
||||
|
@ -195,6 +199,7 @@ impl LuaUserData for CFrame {
|
|||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("Inverse", |_, this, ()| Ok(this.inverse()));
|
||||
|
@ -226,34 +231,49 @@ impl LuaUserData for CFrame {
|
|||
methods.add_method(
|
||||
"ToWorldSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
|
||||
Ok(Variadic::from_iter(rhs.into_iter().map(|cf| *this * *cf)))
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|cf| *this * *cf)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"ToObjectSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
|
||||
let inverse = this.inverse();
|
||||
Ok(Variadic::from_iter(rhs.into_iter().map(|cf| inverse * *cf)))
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|cf| inverse * *cf)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"PointToWorldSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
Ok(Variadic::from_iter(rhs.into_iter().map(|v3| *this * *v3)))
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| *this * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"PointToObjectSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
let inverse = this.inverse();
|
||||
Ok(Variadic::from_iter(rhs.into_iter().map(|v3| inverse * *v3)))
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| inverse * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"VectorToWorldSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
let result = *this - Vector3(this.position());
|
||||
Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3)))
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| result * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
|
@ -261,8 +281,10 @@ impl LuaUserData for CFrame {
|
|||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
let inverse = this.inverse();
|
||||
let result = inverse - Vector3(inverse.position());
|
||||
|
||||
Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3)))
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| result * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
|
@ -445,7 +467,7 @@ mod cframe_test {
|
|||
Vec3::new(1.0, 2.0, 3.0).extend(1.0),
|
||||
));
|
||||
|
||||
assert_eq!(CFrame::from(dom_cframe), cframe)
|
||||
assert_eq!(CFrame::from(dom_cframe), cframe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -466,6 +488,6 @@ mod cframe_test {
|
|||
),
|
||||
);
|
||||
|
||||
assert_eq!(DomCFrame::from(cframe), dom_cframe)
|
||||
assert_eq!(DomCFrame::from(cframe), dom_cframe);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::many_single_char_names)]
|
||||
|
||||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
|
@ -5,7 +7,9 @@ use glam::Vec3;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{Color3 as DomColor3, Color3uint8 as DomColor3uint8};
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
|
@ -85,8 +89,7 @@ impl LuaExportsTable<'_> for Color3 {
|
|||
b: (b as f32) / 255f32,
|
||||
}),
|
||||
_ => Err(LuaError::RuntimeError(format!(
|
||||
"Hex color string '{}' contains invalid character",
|
||||
trimmed
|
||||
"Hex color string '{trimmed}' contains invalid character",
|
||||
))),
|
||||
}
|
||||
};
|
||||
|
@ -151,6 +154,7 @@ impl LuaUserData for Color3 {
|
|||
let max = r.max(g).max(b);
|
||||
let diff = max - min;
|
||||
|
||||
#[allow(clippy::float_cmp)]
|
||||
let hue = (match max {
|
||||
max if max == min => 0.0,
|
||||
max if max == r => (g - b) / diff + (if g < b { 6.0 } else { 0.0 }),
|
|
@ -5,14 +5,16 @@ use rbx_dom_weak::types::{
|
|||
ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint,
|
||||
};
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Color3, ColorSequenceKeypoint};
|
||||
|
||||
/**
|
||||
An implementation of the [ColorSequence](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequence) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the ColorSequence class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `ColorSequence` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ColorSequence {
|
||||
|
@ -87,9 +89,9 @@ impl fmt::Display for ColorSequence {
|
|||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (index, keypoint) in self.keypoints.iter().enumerate() {
|
||||
if index < self.keypoints.len() - 1 {
|
||||
write!(f, "{}, ", keypoint)?;
|
||||
write!(f, "{keypoint}, ")?;
|
||||
} else {
|
||||
write!(f, "{}", keypoint)?;
|
||||
write!(f, "{keypoint}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -102,7 +104,7 @@ impl From<DomColorSequence> for ColorSequence {
|
|||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.cloned()
|
||||
.copied()
|
||||
.map(ColorSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
|
@ -115,7 +117,7 @@ impl From<ColorSequence> for DomColorSequence {
|
|||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.cloned()
|
||||
.copied()
|
||||
.map(DomColorSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
|
@ -3,14 +3,16 @@ use core::fmt;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::ColorSequenceKeypoint as DomColorSequenceKeypoint;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Color3};
|
||||
|
||||
/**
|
||||
An implementation of the [ColorSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequenceKeypoint) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the ColorSequenceKeypoint class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `ColorSequenceKeypoint` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ColorSequenceKeypoint {
|
120
crates/lune-roblox/src/datatypes/types/content.rs
Normal file
120
crates/lune-roblox/src/datatypes/types/content.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{Content as DomContent, ContentType};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::{exports::LuaExportsTable, instance::Instance};
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the Content type as of April 2025.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Content(ContentType);
|
||||
|
||||
impl LuaExportsTable<'_> for Content {
|
||||
const EXPORT_NAME: &'static str = "Content";
|
||||
|
||||
fn create_exports_table(lua: &'_ Lua) -> LuaResult<LuaTable<'_>> {
|
||||
let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri)));
|
||||
|
||||
let from_object = |_, obj: LuaUserDataRef<Instance>| {
|
||||
let database = rbx_reflection_database::get();
|
||||
let instance_descriptor = database
|
||||
.classes
|
||||
.get("Instance")
|
||||
.expect("the reflection database should always have Instance in it");
|
||||
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
|
||||
"you should not be able to construct an Instance that is not known to Lune",
|
||||
);
|
||||
if database.has_superclass(param_descriptor, instance_descriptor) {
|
||||
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
|
||||
} else {
|
||||
Ok(Content(ContentType::Object(obj.dom_ref)))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("none", Content(ContentType::None))?
|
||||
.with_function("fromUri", from_uri)?
|
||||
.with_function("fromObject", from_object)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Content {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("SourceType", |_, this| {
|
||||
let variant_name = match &this.0 {
|
||||
ContentType::None => "None",
|
||||
ContentType::Uri(_) => "Uri",
|
||||
ContentType::Object(_) => "Object",
|
||||
other => {
|
||||
return Err(LuaError::runtime(format!(
|
||||
"cannot get SourceType: unknown ContentType variant '{other:?}'"
|
||||
)))
|
||||
}
|
||||
};
|
||||
Ok(EnumItem::from_enum_name_and_name(
|
||||
"ContentSourceType",
|
||||
variant_name,
|
||||
))
|
||||
});
|
||||
fields.add_field_method_get("Uri", |_, this| {
|
||||
if let ContentType::Uri(uri) = &this.0 {
|
||||
Ok(Some(uri.to_owned()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
});
|
||||
fields.add_field_method_get("Object", |_, this| {
|
||||
if let ContentType::Object(referent) = &this.0 {
|
||||
Ok(Instance::new_opt(*referent))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Content {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Regardless of the actual content of the Content, Roblox just emits
|
||||
// `Content` when casting it to a string. We do not do that.
|
||||
write!(f, "Content(")?;
|
||||
match &self.0 {
|
||||
ContentType::None => write!(f, "None")?,
|
||||
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
|
||||
ContentType::Object(_) => write!(f, "Object")?,
|
||||
other => write!(f, "UnknownType({other:?})")?,
|
||||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomContent> for Content {
|
||||
fn from(value: DomContent) -> Self {
|
||||
Self(value.value().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Content> for DomContent {
|
||||
fn from(value: Content) -> Self {
|
||||
match value.0 {
|
||||
ContentType::None => Self::none(),
|
||||
ContentType::Uri(uri) => Self::from_uri(uri),
|
||||
ContentType::Object(referent) => Self::from_referent(referent),
|
||||
other => unimplemented!("unknown variant of ContentType: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Enum as DomEnum;
|
||||
use rbx_dom_weak::types::EnumItem as DomEnumItem;
|
||||
|
||||
use super::{super::*, Enum};
|
||||
|
||||
/**
|
||||
An implementation of the [EnumItem](https://create.roblox.com/docs/reference/engine/datatypes/EnumItem) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the EnumItem class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `EnumItem` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumItem {
|
||||
|
@ -100,8 +100,18 @@ impl PartialEq for EnumItem {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<EnumItem> for DomEnum {
|
||||
impl From<EnumItem> for DomEnumItem {
|
||||
fn from(v: EnumItem) -> Self {
|
||||
DomEnum::from_u32(v.value)
|
||||
DomEnumItem {
|
||||
ty: v.parent.desc.name.to_string(),
|
||||
value: v.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomEnumItem> for EnumItem {
|
||||
fn from(value: DomEnumItem) -> Self {
|
||||
EnumItem::from_enum_name_and_value(value.ty, value.value)
|
||||
.expect("cannot convert rbx_type::EnumItem with unknown type into EnumItem")
|
||||
}
|
||||
}
|
|
@ -24,8 +24,7 @@ impl LuaUserData for Enums {
|
|||
|_, _, name: String| match Enum::from_name(&name) {
|
||||
Some(e) => Ok(e),
|
||||
None => Err(LuaError::RuntimeError(format!(
|
||||
"The enum '{}' does not exist",
|
||||
name
|
||||
"The enum '{name}' does not exist",
|
||||
))),
|
||||
},
|
||||
);
|
|
@ -1,9 +1,13 @@
|
|||
#![allow(clippy::struct_excessive_bools)]
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Faces as DomFaces;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
|
@ -54,8 +58,7 @@ impl LuaExportsTable<'_> for Faces {
|
|||
check(&e);
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected argument #{} to be an EnumItem, got userdata",
|
||||
index
|
||||
"Expected argument #{index} to be an EnumItem, got userdata",
|
||||
)));
|
||||
}
|
||||
} else {
|
|
@ -6,7 +6,9 @@ use rbx_dom_weak::types::{
|
|||
Font as DomFont, FontStyle as DomFontStyle, FontWeight as DomFontWeight,
|
||||
};
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
|
@ -62,7 +64,7 @@ impl LuaExportsTable<'_> for Font {
|
|||
let font_from_name =
|
||||
|_, (file, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
|
||||
Ok(Font {
|
||||
family: format!("rbxasset://fonts/families/{}.json", file),
|
||||
family: format!("rbxasset://fonts/families/{file}.json"),
|
||||
weight: weight.unwrap_or_default(),
|
||||
style: style.unwrap_or_default(),
|
||||
cached_id: None,
|
||||
|
@ -72,7 +74,7 @@ impl LuaExportsTable<'_> for Font {
|
|||
let font_from_id =
|
||||
|_, (id, weight, style): (i32, Option<FontWeight>, Option<FontStyle>)| {
|
||||
Ok(Font {
|
||||
family: format!("rbxassetid://{}", id),
|
||||
family: format!("rbxassetid://{id}"),
|
||||
weight: weight.unwrap_or_default(),
|
||||
style: style.unwrap_or_default(),
|
||||
cached_id: None,
|
||||
|
@ -206,7 +208,7 @@ pub(crate) enum FontWeight {
|
|||
}
|
||||
|
||||
impl FontWeight {
|
||||
pub(crate) fn as_u16(&self) -> u16 {
|
||||
pub(crate) fn as_u16(self) -> u16 {
|
||||
match self {
|
||||
Self::Thin => 100,
|
||||
Self::ExtraLight => 200,
|
||||
|
@ -288,12 +290,11 @@ impl<'lua> FromLua<'lua> for FontWeight {
|
|||
if value.parent.desc.name == "FontWeight" {
|
||||
if let Ok(value) = FontWeight::from_str(&value.name) {
|
||||
return Ok(value);
|
||||
} else {
|
||||
message = Some(format!(
|
||||
"Found unknown Enum.FontWeight value '{}'",
|
||||
value.name
|
||||
));
|
||||
}
|
||||
message = Some(format!(
|
||||
"Found unknown Enum.FontWeight value '{}'",
|
||||
value.name
|
||||
));
|
||||
} else {
|
||||
message = Some(format!(
|
||||
"Expected Enum.FontWeight, got Enum.{}",
|
||||
|
@ -316,7 +317,7 @@ impl<'lua> IntoLua<'lua> for FontWeight {
|
|||
None => Err(LuaError::ToLuaConversionError {
|
||||
from: "FontWeight",
|
||||
to: "EnumItem",
|
||||
message: Some(format!("Found unknown Enum.FontWeight value '{}'", self)),
|
||||
message: Some(format!("Found unknown Enum.FontWeight value '{self}'")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -329,7 +330,7 @@ pub(crate) enum FontStyle {
|
|||
}
|
||||
|
||||
impl FontStyle {
|
||||
pub(crate) fn as_u8(&self) -> u8 {
|
||||
pub(crate) fn as_u8(self) -> u8 {
|
||||
match self {
|
||||
Self::Normal => 0,
|
||||
Self::Italic => 1,
|
||||
|
@ -383,12 +384,11 @@ impl<'lua> FromLua<'lua> for FontStyle {
|
|||
if value.parent.desc.name == "FontStyle" {
|
||||
if let Ok(value) = FontStyle::from_str(&value.name) {
|
||||
return Ok(value);
|
||||
} else {
|
||||
message = Some(format!(
|
||||
"Found unknown Enum.FontStyle value '{}'",
|
||||
value.name
|
||||
));
|
||||
}
|
||||
message = Some(format!(
|
||||
"Found unknown Enum.FontStyle value '{}'",
|
||||
value.name
|
||||
));
|
||||
} else {
|
||||
message = Some(format!(
|
||||
"Expected Enum.FontStyle, got Enum.{}",
|
||||
|
@ -411,7 +411,7 @@ impl<'lua> IntoLua<'lua> for FontStyle {
|
|||
None => Err(LuaError::ToLuaConversionError {
|
||||
from: "FontStyle",
|
||||
to: "EnumItem",
|
||||
message: Some(format!("Found unknown Enum.FontStyle value '{}'", self)),
|
||||
message: Some(format!("Found unknown Enum.FontStyle value '{self}'")),
|
||||
}),
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ mod cframe;
|
|||
mod color3;
|
||||
mod color_sequence;
|
||||
mod color_sequence_keypoint;
|
||||
mod content;
|
||||
mod r#enum;
|
||||
mod r#enum_item;
|
||||
mod r#enums;
|
||||
|
@ -30,6 +31,7 @@ pub use cframe::CFrame;
|
|||
pub use color3::Color3;
|
||||
pub use color_sequence::ColorSequence;
|
||||
pub use color_sequence_keypoint::ColorSequenceKeypoint;
|
||||
pub use content::Content;
|
||||
pub use faces::Faces;
|
||||
pub use font::Font;
|
||||
pub use number_range::NumberRange;
|
|
@ -3,14 +3,16 @@ use core::fmt;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::NumberRange as DomNumberRange;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [NumberRange](https://create.roblox.com/docs/reference/engine/datatypes/NumberRange) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the NumberRange class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `NumberRange` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct NumberRange {
|
|
@ -5,14 +5,16 @@ use rbx_dom_weak::types::{
|
|||
NumberSequence as DomNumberSequence, NumberSequenceKeypoint as DomNumberSequenceKeypoint,
|
||||
};
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, NumberSequenceKeypoint};
|
||||
|
||||
/**
|
||||
An implementation of the [NumberSequence](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequence) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the NumberSequence class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `NumberSequence` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NumberSequence {
|
||||
|
@ -91,9 +93,9 @@ impl fmt::Display for NumberSequence {
|
|||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (index, keypoint) in self.keypoints.iter().enumerate() {
|
||||
if index < self.keypoints.len() - 1 {
|
||||
write!(f, "{}, ", keypoint)?;
|
||||
write!(f, "{keypoint}, ")?;
|
||||
} else {
|
||||
write!(f, "{}", keypoint)?;
|
||||
write!(f, "{keypoint}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -106,7 +108,7 @@ impl From<DomNumberSequence> for NumberSequence {
|
|||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.cloned()
|
||||
.copied()
|
||||
.map(NumberSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
|
@ -119,7 +121,7 @@ impl From<NumberSequence> for DomNumberSequence {
|
|||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.cloned()
|
||||
.copied()
|
||||
.map(DomNumberSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
|
@ -3,14 +3,16 @@ use core::fmt;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::NumberSequenceKeypoint as DomNumberSequenceKeypoint;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [NumberSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequenceKeypoint) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the NumberSequenceKeypoint class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `NumberSequenceKeypoint` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct NumberSequenceKeypoint {
|
|
@ -3,14 +3,16 @@ use core::fmt;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::CustomPhysicalProperties as DomCustomPhysicalProperties;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [PhysicalProperties](https://create.roblox.com/docs/reference/engine/datatypes/PhysicalProperties) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the PhysicalProperties class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `PhysicalProperties` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct PhysicalProperties {
|
|
@ -4,7 +4,9 @@ use glam::Vec3;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Ray as DomRay;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector3};
|
||||
|
|
@ -5,7 +5,9 @@ use glam::Vec2;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Rect as DomRect;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector2};
|
||||
|
|
@ -4,7 +4,9 @@ use glam::{Mat4, Vec3};
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Region3 as DomRegion3;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, CFrame, Vector3};
|
||||
|
|
@ -4,7 +4,9 @@ use glam::IVec3;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Region3int16 as DomRegion3int16;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector3int16};
|
||||
|
|
@ -4,14 +4,16 @@ use std::ops;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::UDim as DomUDim;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [UDim](https://create.roblox.com/docs/reference/engine/datatypes/UDim) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the UDim class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `UDim` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct UDim {
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::items_after_statements)]
|
||||
|
||||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
|
@ -5,14 +7,16 @@ use glam::Vec2;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::UDim2 as DomUDim2;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, UDim};
|
||||
|
||||
/**
|
||||
An implementation of the [UDim2](https://create.roblox.com/docs/reference/engine/datatypes/UDim2) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the UDim2 class as of March 2023.
|
||||
This implements all documented properties, methods & constructors of the `UDim2` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct UDim2 {
|
|
@ -5,7 +5,9 @@ use glam::{Vec2, Vec3};
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Vector2 as DomVector2;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
|
@ -50,6 +52,9 @@ impl LuaUserData for Vector2 {
|
|||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("Angle", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
Ok(this.0.angle_between(rhs.0))
|
||||
});
|
||||
methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
let this_v3 = Vec3::new(this.0.x, this.0.y, 0f32);
|
||||
let rhs_v3 = Vec3::new(rhs.0.x, rhs.0.y, 0f32);
|
||||
|
@ -58,6 +63,14 @@ impl LuaUserData for Vector2 {
|
|||
methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
Ok(this.0.dot(rhs.0))
|
||||
});
|
||||
methods.add_method(
|
||||
"FuzzyEq",
|
||||
|_, this, (rhs, epsilon): (LuaUserDataRef<Vector2>, f32)| {
|
||||
let eq_x = (rhs.0.x - this.0.x).abs() <= epsilon;
|
||||
let eq_y = (rhs.0.y - this.0.y).abs() <= epsilon;
|
||||
Ok(eq_x && eq_y)
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"Lerp",
|
||||
|_, this, (rhs, alpha): (LuaUserDataRef<Vector2>, f32)| {
|
||||
|
@ -70,6 +83,10 @@ impl LuaUserData for Vector2 {
|
|||
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
Ok(Vector2(this.0.min(rhs.0)))
|
||||
});
|
||||
methods.add_method("Abs", |_, this, ()| Ok(Vector2(this.0.abs())));
|
||||
methods.add_method("Ceil", |_, this, ()| Ok(Vector2(this.0.ceil())));
|
||||
methods.add_method("Floor", |_, this, ()| Ok(Vector2(this.0.floor())));
|
||||
methods.add_method("Sign", |_, this, ()| Ok(Vector2(this.0.signum())));
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
|
@ -78,6 +95,7 @@ impl LuaUserData for Vector2 {
|
|||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::IDiv, userdata_impl_idiv_f32);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,6 +154,20 @@ impl ops::Div<f32> for Vector2 {
|
|||
}
|
||||
}
|
||||
|
||||
impl IDiv for Vector2 {
|
||||
type Output = Vector2;
|
||||
fn idiv(self, rhs: Self) -> Self::Output {
|
||||
Self((self.0 / rhs.0).floor())
|
||||
}
|
||||
}
|
||||
|
||||
impl IDiv<f32> for Vector2 {
|
||||
type Output = Vector2;
|
||||
fn idiv(self, rhs: f32) -> Self::Output {
|
||||
Self((self.0 / rhs).floor())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomVector2> for Vector2 {
|
||||
fn from(v: DomVector2) -> Self {
|
||||
Vector2(Vec2 { x: v.x, y: v.y })
|
|
@ -5,7 +5,9 @@ use glam::IVec2;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Vector2int16 as DomVector2int16;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
|
@ -5,10 +5,9 @@ use glam::Vec3;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Vector3 as DomVector3;
|
||||
|
||||
use crate::{
|
||||
lune::util::TableBuilder,
|
||||
roblox::{datatypes::util::round_float_decimal, exports::LuaExportsTable},
|
||||
};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::{datatypes::util::round_float_decimal, exports::LuaExportsTable};
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
|
@ -37,8 +36,7 @@ impl LuaExportsTable<'_> for Vector3 {
|
|||
"Z" => Vector3(Vec3::Z),
|
||||
name => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Axis '{}' is not known",
|
||||
name
|
||||
"Axis '{name}' is not known",
|
||||
)))
|
||||
}
|
||||
})
|
||||
|
@ -61,8 +59,7 @@ impl LuaExportsTable<'_> for Vector3 {
|
|||
"Back" => Vector3(Vec3::Z),
|
||||
name => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"NormalId '{}' is not known",
|
||||
name
|
||||
"NormalId '{name}' is not known",
|
||||
)))
|
||||
}
|
||||
})
|
||||
|
@ -136,6 +133,10 @@ impl LuaUserData for Vector3 {
|
|||
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
||||
Ok(Vector3(this.0.min(rhs.0)))
|
||||
});
|
||||
methods.add_method("Abs", |_, this, ()| Ok(Vector3(this.0.abs())));
|
||||
methods.add_method("Ceil", |_, this, ()| Ok(Vector3(this.0.ceil())));
|
||||
methods.add_method("Floor", |_, this, ()| Ok(Vector3(this.0.floor())));
|
||||
methods.add_method("Sign", |_, this, ()| Ok(Vector3(this.0.signum())));
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
|
@ -144,6 +145,7 @@ impl LuaUserData for Vector3 {
|
|||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::IDiv, userdata_impl_idiv_f32);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,6 +204,20 @@ impl ops::Div<f32> for Vector3 {
|
|||
}
|
||||
}
|
||||
|
||||
impl IDiv for Vector3 {
|
||||
type Output = Vector3;
|
||||
fn idiv(self, rhs: Self) -> Self::Output {
|
||||
Self((self.0 / rhs.0).floor())
|
||||
}
|
||||
}
|
||||
|
||||
impl IDiv<f32> for Vector3 {
|
||||
type Output = Vector3;
|
||||
fn idiv(self, rhs: f32) -> Self::Output {
|
||||
Self((self.0 / rhs).floor())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomVector3> for Vector3 {
|
||||
fn from(v: DomVector3) -> Self {
|
||||
Vector3(Vec3 {
|
|
@ -5,7 +5,9 @@ use glam::IVec3;
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Vector3int16 as DomVector3int16;
|
||||
|
||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
|
@ -2,7 +2,7 @@ use std::path::Path;
|
|||
|
||||
use rbx_dom_weak::WeakDom;
|
||||
|
||||
use crate::roblox::shared::instance::class_is_a_service;
|
||||
use crate::shared::instance::class_is_a_service;
|
||||
|
||||
/**
|
||||
A document kind specifier.
|
||||
|
@ -58,13 +58,14 @@ impl DocumentKind {
|
|||
|
||||
Returns `None` if the given dom is empty and as such can not have its kind inferred.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn from_weak_dom(dom: &WeakDom) -> Option<Self> {
|
||||
let mut has_top_level_child = false;
|
||||
let mut has_top_level_service = false;
|
||||
for child_ref in dom.root().children() {
|
||||
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
|
||||
has_top_level_child = true;
|
||||
if class_is_a_service(&child_inst.class).unwrap_or(false) {
|
||||
if class_is_a_service(child_inst.class).unwrap_or(false) {
|
||||
has_top_level_service = true;
|
||||
break;
|
||||
}
|
|
@ -15,7 +15,7 @@ pub use kind::*;
|
|||
|
||||
use postprocessing::*;
|
||||
|
||||
use crate::roblox::instance::{data_model, Instance};
|
||||
use crate::instance::{data_model, Instance};
|
||||
|
||||
pub type DocumentResult<T> = Result<T, DocumentError>;
|
||||
|
||||
|
@ -78,6 +78,7 @@ impl Document {
|
|||
| Model | Binary | `rbxm` |
|
||||
| Model | Xml | `rbxmx` |
|
||||
*/
|
||||
#[must_use]
|
||||
#[rustfmt::skip]
|
||||
pub fn canonical_extension(kind: DocumentKind, format: DocumentFormat) -> &'static str {
|
||||
match (kind, format) {
|
||||
|
@ -113,6 +114,10 @@ impl Document {
|
|||
Note that detection of model vs place file is heavily dependent on the structure
|
||||
of the file, and a model file with services in it will detect as a place file, so
|
||||
if possible using [`Document::from_bytes`] with an explicit kind should be preferred.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the given bytes are not a valid roblox file.
|
||||
*/
|
||||
pub fn from_bytes_auto(bytes: impl AsRef<[u8]>) -> DocumentResult<Self> {
|
||||
let (format, dom) = Self::from_bytes_inner(bytes)?;
|
||||
|
@ -125,6 +130,10 @@ impl Document {
|
|||
|
||||
This will automatically handle and detect if the document
|
||||
should be decoded using a roblox binary or roblox xml format.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the given bytes are not a valid roblox file or not of the given kind.
|
||||
*/
|
||||
pub fn from_bytes(bytes: impl AsRef<[u8]>, kind: DocumentKind) -> DocumentResult<Self> {
|
||||
let (format, dom) = Self::from_bytes_inner(bytes)?;
|
||||
|
@ -138,6 +147,10 @@ impl Document {
|
|||
This will use the same format that the document was created
|
||||
with, meaning if the document is a binary document the output
|
||||
will be binary, and vice versa for xml and other future formats.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the document can not be encoded.
|
||||
*/
|
||||
pub fn to_bytes(&self) -> DocumentResult<Vec<u8>> {
|
||||
self.to_bytes_with_format(self.format)
|
||||
|
@ -146,6 +159,10 @@ impl Document {
|
|||
/**
|
||||
Encodes the document as a vector of bytes, to
|
||||
be written to a file or sent over the network.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the document can not be encoded.
|
||||
*/
|
||||
pub fn to_bytes_with_format(&self, format: DocumentFormat) -> DocumentResult<Vec<u8>> {
|
||||
let mut bytes = Vec::new();
|
||||
|
@ -172,6 +189,7 @@ impl Document {
|
|||
/**
|
||||
Gets the kind this document was created with.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn kind(&self) -> DocumentKind {
|
||||
self.kind
|
||||
}
|
||||
|
@ -179,6 +197,7 @@ impl Document {
|
|||
/**
|
||||
Gets the format this document was created with.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn format(&self) -> DocumentFormat {
|
||||
self.format
|
||||
}
|
||||
|
@ -186,14 +205,17 @@ impl Document {
|
|||
/**
|
||||
Gets the file extension for this document.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn extension(&self) -> &'static str {
|
||||
Self::canonical_extension(self.kind, self.format)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a DataModel instance out of this place document.
|
||||
Creates a `DataModel` instance out of this place document.
|
||||
|
||||
Will error if the document is not a place.
|
||||
# Errors
|
||||
|
||||
Errors if the document is not a place.
|
||||
*/
|
||||
pub fn into_data_model_instance(mut self) -> DocumentResult<Instance> {
|
||||
if self.kind != DocumentKind::Place {
|
||||
|
@ -219,7 +241,9 @@ impl Document {
|
|||
/**
|
||||
Creates an array of instances out of this model document.
|
||||
|
||||
Will error if the document is not a model.
|
||||
# Errors
|
||||
|
||||
Errors if the document is not a model.
|
||||
*/
|
||||
pub fn into_instance_array(mut self) -> DocumentResult<Vec<Instance>> {
|
||||
if self.kind != DocumentKind::Model {
|
||||
|
@ -237,9 +261,11 @@ impl Document {
|
|||
}
|
||||
|
||||
/**
|
||||
Creates a place document out of a DataModel instance.
|
||||
Creates a place document out of a `DataModel` instance.
|
||||
|
||||
Will error if the instance is not a DataModel.
|
||||
# Errors
|
||||
|
||||
Errors if the instance is not a `DataModel`.
|
||||
*/
|
||||
pub fn from_data_model_instance(i: Instance) -> DocumentResult<Self> {
|
||||
if i.get_class_name() != data_model::CLASS_NAME {
|
||||
|
@ -266,7 +292,9 @@ impl Document {
|
|||
/**
|
||||
Creates a model document out of an array of instances.
|
||||
|
||||
Will error if any of the instances is a DataModel.
|
||||
# Errors
|
||||
|
||||
Errors if any of the instances is a `DataModel`.
|
||||
*/
|
||||
pub fn from_instance_array(v: Vec<Instance>) -> DocumentResult<Self> {
|
||||
for i in &v {
|
|
@ -1,9 +1,9 @@
|
|||
use rbx_dom_weak::{
|
||||
types::{Ref as DomRef, VariantType as DomType},
|
||||
Instance as DomInstance, WeakDom,
|
||||
ustr, Instance as DomInstance, WeakDom,
|
||||
};
|
||||
|
||||
use crate::roblox::shared::instance::class_is_a;
|
||||
use crate::shared::instance::class_is_a;
|
||||
|
||||
pub fn postprocess_dom_for_place(_dom: &mut WeakDom) {
|
||||
// Nothing here yet
|
||||
|
@ -18,8 +18,8 @@ pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
|
|||
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
|
||||
// Similar story with ScriptGuid - this is used
|
||||
// in the studio-only cloud script drafts feature
|
||||
if class_is_a(&inst.class, "LuaSourceContainer").unwrap_or(false) {
|
||||
inst.properties.remove("ScriptGuid");
|
||||
if class_is_a(inst.class, "LuaSourceContainer").unwrap_or(false) {
|
||||
inst.properties.remove(&ustr("ScriptGuid"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -41,7 +41,8 @@ where
|
|||
}
|
||||
|
||||
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
|
||||
if inst.properties.get(name).map_or(false, |u| u.ty() == ty) {
|
||||
let name = &ustr(name);
|
||||
if inst.properties.get(name).is_some_and(|u| u.ty() == ty) {
|
||||
inst.properties.remove(name);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::items_after_statements)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_dom_weak::{
|
||||
|
@ -5,7 +7,7 @@ use rbx_dom_weak::{
|
|||
Instance as DomInstance,
|
||||
};
|
||||
|
||||
use crate::roblox::{
|
||||
use crate::{
|
||||
datatypes::{
|
||||
attributes::{ensure_valid_attribute_name, ensure_valid_attribute_value},
|
||||
conversion::{DomValueToLua, LuaToDomValue},
|
||||
|
@ -15,8 +17,9 @@ use crate::roblox::{
|
|||
shared::instance::{class_is_a, find_property_info},
|
||||
};
|
||||
|
||||
use super::{data_model, Instance};
|
||||
use super::{data_model, registry::InstanceRegistry, Instance};
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||
m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| {
|
||||
ensure_not_destroyed(this)?;
|
||||
|
@ -49,6 +52,9 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|||
ensure_not_destroyed(this)?;
|
||||
this.get_full_name().into_lua(lua)
|
||||
});
|
||||
m.add_method("GetDebugId", |lua, this, ()| {
|
||||
this.dom_ref.to_string().into_lua(lua)
|
||||
});
|
||||
m.add_method("FindFirstAncestor", |lua, this, name: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.find_ancestor(|child| child.name == name).into_lua(lua)
|
||||
|
@ -65,7 +71,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|||
"FindFirstAncestorWhichIsA",
|
||||
|lua, this, class_name: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.find_ancestor(|child| class_is_a(&child.class, &class_name).unwrap_or(false))
|
||||
this.find_ancestor(|child| class_is_a(child.class, &class_name).unwrap_or(false))
|
||||
.into_lua(lua)
|
||||
},
|
||||
);
|
||||
|
@ -98,7 +104,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|||
|lua, this, (class_name, recursive): (String, Option<bool>)| {
|
||||
ensure_not_destroyed(this)?;
|
||||
let predicate =
|
||||
|child: &DomInstance| class_is_a(&child.class, &class_name).unwrap_or(false);
|
||||
|child: &DomInstance| class_is_a(child.class, &class_name).unwrap_or(false);
|
||||
if matches!(recursive, Some(true)) {
|
||||
this.find_descendant(predicate).into_lua(lua)
|
||||
} else {
|
||||
|
@ -107,8 +113,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|||
},
|
||||
);
|
||||
m.add_method("IsA", |_, this, class_name: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
Ok(class_is_a(&this.class_name, class_name).unwrap_or(false))
|
||||
Ok(class_is_a(this.class_name, class_name).unwrap_or(false))
|
||||
});
|
||||
m.add_method(
|
||||
"IsAncestorOf",
|
||||
|
@ -139,7 +144,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|||
ensure_not_destroyed(this)?;
|
||||
let attributes = this.get_attributes();
|
||||
let tab = lua.create_table_with_capacity(0, attributes.len())?;
|
||||
for (key, value) in attributes.into_iter() {
|
||||
for (key, value) in attributes {
|
||||
tab.set(key, LuaValue::dom_value_to_lua(lua, &value)?)?;
|
||||
}
|
||||
Ok(tab)
|
||||
|
@ -149,13 +154,18 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|||
|lua, this, (attribute_name, lua_value): (String, LuaValue)| {
|
||||
ensure_not_destroyed(this)?;
|
||||
ensure_valid_attribute_name(&attribute_name)?;
|
||||
match lua_value.lua_to_dom_value(lua, None) {
|
||||
Ok(dom_value) => {
|
||||
ensure_valid_attribute_value(&dom_value)?;
|
||||
this.set_attribute(attribute_name, dom_value);
|
||||
Ok(())
|
||||
if lua_value.is_nil() || lua_value.is_null() {
|
||||
this.remove_attribute(attribute_name);
|
||||
Ok(())
|
||||
} else {
|
||||
match lua_value.lua_to_dom_value(lua, None) {
|
||||
Ok(dom_value) => {
|
||||
ensure_valid_attribute_value(&dom_value)?;
|
||||
this.set_attribute(attribute_name, dom_value);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -206,26 +216,26 @@ fn instance_property_get<'lua>(
|
|||
this: &Instance,
|
||||
prop_name: String,
|
||||
) -> LuaResult<LuaValue<'lua>> {
|
||||
ensure_not_destroyed(this)?;
|
||||
|
||||
match prop_name.as_str() {
|
||||
"ClassName" => return this.get_class_name().into_lua(lua),
|
||||
"Name" => {
|
||||
return this.get_name().into_lua(lua);
|
||||
}
|
||||
"Parent" => {
|
||||
return this.get_parent().into_lua(lua);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
|
||||
ensure_not_destroyed(this)?;
|
||||
|
||||
if prop_name.as_str() == "Name" {
|
||||
return this.get_name().into_lua(lua);
|
||||
}
|
||||
|
||||
if let Some(info) = find_property_info(this.class_name, &prop_name) {
|
||||
if let Some(prop) = this.get_property(&prop_name) {
|
||||
if let DomValue::Enum(enum_value) = prop {
|
||||
let enum_name = info.enum_name.ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{}' - encountered unknown enum",
|
||||
prop_name
|
||||
"Failed to get property '{prop_name}' - encountered unknown enum",
|
||||
))
|
||||
})?;
|
||||
EnumItem::from_enum_name_and_value(&enum_name, enum_value.to_u32())
|
||||
|
@ -243,8 +253,7 @@ fn instance_property_get<'lua>(
|
|||
EnumItem::from_enum_name_and_value(&enum_name, enum_value)
|
||||
.ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{}' - Enum.{} does not contain numeric value {}",
|
||||
prop_name, enum_name, enum_value
|
||||
"Failed to get property '{prop_name}' - Enum.{enum_name} does not contain numeric value {enum_value}",
|
||||
))
|
||||
})?
|
||||
.into_lua(lua)
|
||||
|
@ -255,22 +264,23 @@ fn instance_property_get<'lua>(
|
|||
Ok(LuaValue::Nil)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{}' - missing default value",
|
||||
prop_name
|
||||
"Failed to get property '{prop_name}' - missing default value",
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{}' - malformed property info",
|
||||
prop_name
|
||||
"Failed to get property '{prop_name}' - malformed property info",
|
||||
)))
|
||||
}
|
||||
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
|
||||
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
|
||||
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
|
||||
getter.call(*this)
|
||||
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
|
||||
Ok(LuaValue::Function(method))
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{} is not a valid member of {}",
|
||||
prop_name, this
|
||||
"{prop_name} is not a valid member of {this}",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
@ -311,46 +321,43 @@ fn instance_property_set<'lua>(
|
|||
}
|
||||
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
|
||||
let parent = Parent::from_lua(prop_value, lua)?;
|
||||
this.set_parent(parent.map(|p| p.clone()));
|
||||
this.set_parent(parent.map(|p| *p));
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let info = match find_property_info(&this.class_name, &prop_name) {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"{} is not a valid member of {}",
|
||||
prop_name, this
|
||||
if let Some(info) = find_property_info(this.class_name, &prop_name) {
|
||||
if let Some(enum_name) = info.enum_name {
|
||||
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
|
||||
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
|
||||
this.set_property(prop_name, DomValue::EnumItem((*given_enum).clone().into()));
|
||||
Ok(())
|
||||
}
|
||||
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
|
||||
"Failed to set property '{}' - expected Enum.{}, got Enum.{}",
|
||||
prop_name, enum_name, given_enum.parent.desc.name
|
||||
))),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else if let Some(dom_type) = info.value_type {
|
||||
match prop_value.lua_to_dom_value(lua, Some(dom_type)) {
|
||||
Ok(dom_value) => {
|
||||
this.set_property(prop_name, dom_value);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to set property '{prop_name}' - malformed property info",
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(enum_name) = info.enum_name {
|
||||
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
|
||||
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
|
||||
this.set_property(prop_name, DomValue::Enum((*given_enum).clone().into()));
|
||||
Ok(())
|
||||
}
|
||||
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
|
||||
"Failed to set property '{}' - expected Enum.{}, got Enum.{}",
|
||||
prop_name, enum_name, given_enum.parent.desc.name
|
||||
))),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else if let Some(dom_type) = info.value_type {
|
||||
match prop_value.lua_to_dom_value(lua, Some(dom_type)) {
|
||||
Ok(dom_value) => {
|
||||
this.set_property(prop_name, dom_value);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
|
||||
setter.call((*this, prop_value))
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to set property '{}' - malformed property info",
|
||||
prop_name
|
||||
"{prop_name} is not a valid member of {this}",
|
||||
)))
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use crate::roblox::shared::{
|
||||
use crate::shared::{
|
||||
classes::{
|
||||
add_class_restricted_getter, add_class_restricted_method,
|
||||
get_or_create_property_ref_instance,
|
||||
|
@ -12,8 +12,8 @@ use super::Instance;
|
|||
|
||||
pub const CLASS_NAME: &str = "DataModel";
|
||||
|
||||
pub fn add_fields<'lua, M: LuaUserDataFields<'lua, Instance>>(m: &mut M) {
|
||||
add_class_restricted_getter(m, CLASS_NAME, "Workspace", data_model_get_workspace);
|
||||
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
|
||||
add_class_restricted_getter(f, CLASS_NAME, "Workspace", data_model_get_workspace);
|
||||
}
|
||||
|
||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||
|
@ -26,40 +26,39 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|||
|
||||
### See Also
|
||||
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
|
||||
get_or_create_property_ref_instance(this, "Workspace", "Workspace")
|
||||
}
|
||||
|
||||
/**
|
||||
Gets or creates a service for this DataModel.
|
||||
Gets or creates a service for this `DataModel`.
|
||||
|
||||
### See Also
|
||||
* [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> LuaResult<Instance> {
|
||||
if matches!(class_is_a_service(&service_name), None | Some(false)) {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"'{}' is not a valid service name",
|
||||
service_name
|
||||
"'{service_name}' is not a valid service name",
|
||||
)))
|
||||
} else if let Some(service) = this.find_child(|child| child.class == service_name) {
|
||||
Ok(service)
|
||||
} else {
|
||||
let service = Instance::new_orphaned(service_name);
|
||||
service.set_parent(Some(this.clone()));
|
||||
service.set_parent(Some(*this));
|
||||
Ok(service)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Gets a service for this DataModel, if it exists.
|
||||
Gets a service for this `DataModel`, if it exists.
|
||||
|
||||
### See Also
|
||||
* [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn data_model_find_service(
|
||||
_: &Lua,
|
||||
|
@ -68,8 +67,7 @@ fn data_model_find_service(
|
|||
) -> LuaResult<Option<Instance>> {
|
||||
if matches!(class_is_a_service(&service_name), None | Some(false)) {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"'{}' is not a valid service name",
|
||||
service_name
|
||||
"'{service_name}' is not a valid service name",
|
||||
)))
|
||||
} else if let Some(service) = this.find_child(|child| child.class == service_name) {
|
||||
Ok(Some(service))
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::missing_panics_doc)]
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
fmt,
|
||||
|
@ -9,13 +11,14 @@ use mlua::prelude::*;
|
|||
use once_cell::sync::Lazy;
|
||||
use rbx_dom_weak::{
|
||||
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
|
||||
Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom,
|
||||
ustr, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, Ustr, WeakDom,
|
||||
};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::{
|
||||
lune::util::TableBuilder,
|
||||
roblox::exports::LuaExportsTable,
|
||||
roblox::shared::instance::{class_exists, class_is_a},
|
||||
exports::LuaExportsTable,
|
||||
shared::instance::{class_exists, class_is_a},
|
||||
};
|
||||
|
||||
pub(crate) mod base;
|
||||
|
@ -23,16 +26,18 @@ pub(crate) mod data_model;
|
|||
pub(crate) mod terrain;
|
||||
pub(crate) mod workspace;
|
||||
|
||||
pub mod registry;
|
||||
|
||||
const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes";
|
||||
const PROPERTY_NAME_TAGS: &str = "Tags";
|
||||
|
||||
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
|
||||
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Instance {
|
||||
pub(crate) dom_ref: DomRef,
|
||||
pub(crate) class_name: String,
|
||||
pub(crate) class_name: Ustr,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
|
@ -40,47 +45,37 @@ impl Instance {
|
|||
Creates a new `Instance` from an existing dom object ref.
|
||||
|
||||
Panics if the instance does not exist in the internal dom,
|
||||
or if the given dom object ref points to the dom root.
|
||||
or if the given dom object ref points to the internal dom root.
|
||||
|
||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
||||
any existing lock must first be released to prevent any deadlocking.
|
||||
*/
|
||||
pub(crate) fn new(dom_ref: DomRef) -> Self {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let instance = dom
|
||||
.get_by_ref(dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
|
||||
if instance.referent() == dom.root_ref() {
|
||||
panic!("Instances can not be created from dom roots")
|
||||
}
|
||||
|
||||
Self {
|
||||
dom_ref,
|
||||
class_name: instance.class.clone(),
|
||||
}
|
||||
#[must_use]
|
||||
pub fn new(dom_ref: DomRef) -> Self {
|
||||
Self::new_opt(dom_ref).expect("Failed to find instance in document")
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new `Instance` from a dom object ref, if the instance exists.
|
||||
|
||||
Panics if the given dom object ref points to the dom root.
|
||||
Panics if the given dom object ref points to the internal dom root.
|
||||
|
||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
||||
any existing lock must first be released to prevent any deadlocking.
|
||||
*/
|
||||
pub(crate) fn new_opt(dom_ref: DomRef) -> Option<Self> {
|
||||
#[must_use]
|
||||
pub fn new_opt(dom_ref: DomRef) -> Option<Self> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
if let Some(instance) = dom.get_by_ref(dom_ref) {
|
||||
if instance.referent() == dom.root_ref() {
|
||||
panic!("Instances can not be created from dom roots")
|
||||
}
|
||||
assert!(
|
||||
!(instance.referent() == dom.root_ref()),
|
||||
"Instances can not be created from dom roots"
|
||||
);
|
||||
|
||||
Some(Self {
|
||||
dom_ref,
|
||||
class_name: instance.class.clone(),
|
||||
class_name: instance.class,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
@ -90,24 +85,25 @@ impl Instance {
|
|||
/**
|
||||
Creates a new orphaned `Instance` with a given class name.
|
||||
|
||||
An orphaned instance is an instance at the root of a weak dom.
|
||||
An orphaned instance is an instance at the root of Lune's internal weak dom.
|
||||
|
||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
||||
any existing lock must first be released to prevent any deadlocking.
|
||||
*/
|
||||
pub(crate) fn new_orphaned(class_name: impl AsRef<str>) -> Self {
|
||||
#[must_use]
|
||||
pub fn new_orphaned(class_name: impl AsRef<str>) -> Self {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let class_name = class_name.as_ref();
|
||||
|
||||
let instance = DomInstanceBuilder::new(class_name.to_string());
|
||||
let instance = DomInstanceBuilder::new(class_name);
|
||||
|
||||
let dom_root = dom.root_ref();
|
||||
let dom_ref = dom.insert(dom_root, instance);
|
||||
|
||||
Self {
|
||||
dom_ref,
|
||||
class_name: class_name.to_string(),
|
||||
class_name: ustr(class_name),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,10 +111,11 @@ impl Instance {
|
|||
Creates a new orphaned `Instance` by transferring
|
||||
it from an external weak dom to the internal one.
|
||||
|
||||
An orphaned instance is an instance at the root of a weak dom.
|
||||
An orphaned instance is an instance at the root of Lune's internal weak dom.
|
||||
|
||||
Panics if the given dom ref is the root dom ref of the external weak dom.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn from_external_dom(external_dom: &mut WeakDom, external_dom_ref: DomRef) -> Self {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let dom_root = dom.root_ref();
|
||||
|
@ -144,6 +141,12 @@ impl Instance {
|
|||
cloned
|
||||
}
|
||||
|
||||
/**
|
||||
Clones multiple instances to an external weak dom.
|
||||
|
||||
This will place the instances as children of the
|
||||
root of the weak dom, and return their referents.
|
||||
*/
|
||||
pub fn clone_multiple_into_external_dom(
|
||||
referents: &[DomRef],
|
||||
external_dom: &mut WeakDom,
|
||||
|
@ -152,7 +155,7 @@ impl Instance {
|
|||
|
||||
let cloned = dom.clone_multiple_into_external(referents, external_dom);
|
||||
|
||||
for referent in cloned.iter() {
|
||||
for referent in &cloned {
|
||||
external_dom.transfer_within(*referent, external_dom.root_ref());
|
||||
}
|
||||
|
||||
|
@ -167,9 +170,10 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn clone_instance(&self) -> Instance {
|
||||
#[must_use]
|
||||
pub fn clone_instance(&self) -> Self {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let new_ref = dom.clone_within(self.dom_ref);
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
|
@ -190,7 +194,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`Destroy`](https://create.roblox.com/docs/reference/engine/classes/Instance#Destroy)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn destroy(&mut self) -> bool {
|
||||
if self.is_destroyed() {
|
||||
|
@ -217,7 +221,7 @@ impl Instance {
|
|||
### See Also
|
||||
* [`Instance::Destroy`] for more info about what happens when an instance gets destroyed
|
||||
* [`ClearAllChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClearAllChildren)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn clear_all_children(&mut self) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
@ -237,10 +241,10 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
|
||||
class_is_a(&self.class_name, class_name).unwrap_or(false)
|
||||
class_is_a(self.class_name, class_name).unwrap_or(false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -250,8 +254,9 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_class_name(&self) -> &str {
|
||||
self.class_name.as_str()
|
||||
}
|
||||
|
@ -261,7 +266,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_name(&self) -> String {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
@ -277,14 +282,14 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn set_name(&self, name: impl Into<String>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
dom.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.name = name.into()
|
||||
.name = name.into();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -292,15 +297,12 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_parent(&self) -> Option<Instance> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let parent_ref = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.parent();
|
||||
let parent_ref = dom.get_by_ref(self.dom_ref)?.parent();
|
||||
|
||||
if parent_ref == dom.root_ref() {
|
||||
None
|
||||
|
@ -315,18 +317,16 @@ impl Instance {
|
|||
|
||||
If the provided parent is [`None`] the instance will become orphaned.
|
||||
|
||||
An orphaned instance is an instance at the root of a weak dom.
|
||||
An orphaned instance is an instance at the root of Lune's internal weak dom.
|
||||
|
||||
### See Also
|
||||
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn set_parent(&self, parent: Option<Instance>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let parent_ref = parent
|
||||
.map(|parent| parent.dom_ref)
|
||||
.unwrap_or_else(|| dom.root_ref());
|
||||
let parent_ref = parent.map_or_else(|| dom.root_ref(), |parent| parent.dom_ref);
|
||||
|
||||
dom.transfer_within(self.dom_ref, parent_ref);
|
||||
}
|
||||
|
@ -341,7 +341,7 @@ impl Instance {
|
|||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.properties
|
||||
.get(name.as_ref())
|
||||
.get(&ustr(name.as_ref()))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
|
@ -358,7 +358,7 @@ impl Instance {
|
|||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.properties
|
||||
.insert(name.as_ref().to_string(), value);
|
||||
.insert(ustr(name.as_ref()), value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -366,7 +366,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_attribute(&self, name: impl AsRef<str>) -> Option<DomValue> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
@ -374,7 +374,7 @@ impl Instance {
|
|||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Attributes(attributes)) =
|
||||
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
|
||||
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||
{
|
||||
attributes.get(name.as_ref()).cloned()
|
||||
} else {
|
||||
|
@ -387,7 +387,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_attributes(&self) -> BTreeMap<String, DomValue> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
@ -395,7 +395,7 @@ impl Instance {
|
|||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Attributes(attributes)) =
|
||||
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
|
||||
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||
{
|
||||
attributes.clone().into_iter().collect()
|
||||
} else {
|
||||
|
@ -408,7 +408,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn set_attribute(&self, name: impl AsRef<str>, value: DomValue) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
@ -422,36 +422,59 @@ impl Instance {
|
|||
value => value,
|
||||
};
|
||||
if let Some(DomValue::Attributes(attributes)) =
|
||||
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
|
||||
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||
{
|
||||
attributes.insert(name.as_ref().to_string(), value);
|
||||
} else {
|
||||
let mut attributes = DomAttributes::new();
|
||||
attributes.insert(name.as_ref().to_string(), value);
|
||||
inst.properties.insert(
|
||||
PROPERTY_NAME_ATTRIBUTES.to_string(),
|
||||
ustr(PROPERTY_NAME_ATTRIBUTES),
|
||||
DomValue::Attributes(attributes),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes an attribute from the instance.
|
||||
|
||||
Note that this does not have an equivalent in the Roblox engine API,
|
||||
but separating this from `set_attribute` lets `set_attribute` be more
|
||||
ergonomic and not require an `Option<DomValue>` for the value argument.
|
||||
The equivalent in the Roblox engine API would be `instance:SetAttribute(name, nil)`.
|
||||
*/
|
||||
pub fn remove_attribute(&self, name: impl AsRef<str>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Attributes(attributes)) =
|
||||
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||
{
|
||||
attributes.remove(name.as_ref());
|
||||
if attributes.is_empty() {
|
||||
inst.properties.remove(&ustr(PROPERTY_NAME_ATTRIBUTES));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Adds a tag to the instance.
|
||||
|
||||
### See Also
|
||||
* [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn add_tag(&self, name: impl AsRef<str>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
|
||||
tags.push(name.as_ref());
|
||||
} else {
|
||||
inst.properties.insert(
|
||||
PROPERTY_NAME_TAGS.to_string(),
|
||||
ustr(PROPERTY_NAME_TAGS),
|
||||
DomValue::Tags(vec![name.as_ref().to_string()].into()),
|
||||
);
|
||||
}
|
||||
|
@ -462,14 +485,14 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_tags(&self) -> Vec<String> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
|
||||
tags.iter().map(ToString::to_string).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
|
@ -481,14 +504,14 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn has_tag(&self, name: impl AsRef<str>) -> bool {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
|
||||
let name = name.as_ref();
|
||||
tags.iter().any(|tag| tag == name)
|
||||
} else {
|
||||
|
@ -501,21 +524,19 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn remove_tag(&self, name: impl AsRef<str>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
|
||||
let name = name.as_ref();
|
||||
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
|
||||
new_tags.retain(|tag| tag != name);
|
||||
inst.properties.insert(
|
||||
PROPERTY_NAME_TAGS.to_string(),
|
||||
DomValue::Tags(new_tags.into()),
|
||||
);
|
||||
inst.properties
|
||||
.insert(ustr(PROPERTY_NAME_TAGS), DomValue::Tags(new_tags.into()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -527,7 +548,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_children(&self) -> Vec<Instance> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
@ -550,7 +571,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_descendants(&self) -> Vec<Instance> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
@ -584,7 +605,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_full_name(&self) -> String {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
@ -661,9 +682,8 @@ impl Instance {
|
|||
if predicate(ancestor) {
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
return Some(Self::new(ancestor_ref));
|
||||
} else {
|
||||
ancestor_ref = ancestor.parent();
|
||||
}
|
||||
ancestor_ref = ancestor.parent();
|
||||
}
|
||||
|
||||
None
|
||||
|
@ -675,7 +695,7 @@ impl Instance {
|
|||
|
||||
### See Also
|
||||
* [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn find_descendant<F>(&self, predicate: F) -> Option<Instance>
|
||||
where
|
||||
|
@ -697,9 +717,8 @@ impl Instance {
|
|||
let queue_ref = queue_item.referent();
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
return Some(Self::new(queue_ref));
|
||||
} else {
|
||||
queue.extend(queue_item.children())
|
||||
}
|
||||
queue.extend(queue_item.children());
|
||||
}
|
||||
|
||||
None
|
||||
|
@ -715,8 +734,7 @@ impl LuaExportsTable<'_> for Instance {
|
|||
Instance::new_orphaned(class_name).into_lua(lua)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to create Instance - '{}' is not a valid class name",
|
||||
class_name
|
||||
"Failed to create Instance - '{class_name}' is not a valid class name",
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
@ -735,6 +753,9 @@ impl LuaExportsTable<'_> for Instance {
|
|||
and methods we support here - we should only implement methods that
|
||||
are necessary for modifying the dom and / or having ergonomic access
|
||||
to the dom, not try to replicate Roblox engine behavior of instances
|
||||
|
||||
If a user wants to replicate Roblox engine behavior, they can use the
|
||||
instance registry, and register properties + methods from the lua side
|
||||
*/
|
||||
impl LuaUserData for Instance {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
|
@ -751,7 +772,7 @@ impl LuaUserData for Instance {
|
|||
|
||||
impl Hash for Instance {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.dom_ref.hash(state)
|
||||
self.dom_ref.hash(state);
|
||||
}
|
||||
}
|
||||
|
274
crates/lune-roblox/src/instance/registry.rs
Normal file
274
crates/lune-roblox/src/instance/registry.rs
Normal file
|
@ -0,0 +1,274 @@
|
|||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use mlua::{prelude::*, AppDataRef};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::Instance;
|
||||
|
||||
type InstanceRegistryMap = HashMap<String, HashMap<String, LuaRegistryKey>>;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum InstanceRegistryError {
|
||||
#[error("class name '{0}' is not valid")]
|
||||
InvalidClassName(String),
|
||||
#[error("class '{class_name}' already registered method '{method_name}'")]
|
||||
MethodAlreadyExists {
|
||||
class_name: String,
|
||||
method_name: String,
|
||||
},
|
||||
#[error("class '{class_name}' already registered property '{property_name}'")]
|
||||
PropertyAlreadyExists {
|
||||
class_name: String,
|
||||
property_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InstanceRegistry {
|
||||
getters: Arc<Mutex<InstanceRegistryMap>>,
|
||||
setters: Arc<Mutex<InstanceRegistryMap>>,
|
||||
methods: Arc<Mutex<InstanceRegistryMap>>,
|
||||
}
|
||||
|
||||
impl InstanceRegistry {
|
||||
// NOTE: We lazily create the instance registry instead
|
||||
// of always creating it together with the roblox builtin
|
||||
// since it is less commonly used and it simplifies some app
|
||||
// data borrowing relationship problems we'd otherwise have
|
||||
fn get_or_create(lua: &Lua) -> AppDataRef<'_, Self> {
|
||||
if lua.app_data_ref::<Self>().is_none() {
|
||||
lua.set_app_data(Self {
|
||||
getters: Arc::new(Mutex::new(HashMap::new())),
|
||||
setters: Arc::new(Mutex::new(HashMap::new())),
|
||||
methods: Arc::new(Mutex::new(HashMap::new())),
|
||||
});
|
||||
}
|
||||
lua.app_data_ref::<Self>()
|
||||
.expect("Missing InstanceRegistry in app data")
|
||||
}
|
||||
|
||||
/**
|
||||
Inserts a method into the instance registry.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the method already exists in the registry.
|
||||
*/
|
||||
pub fn insert_method<'lua>(
|
||||
lua: &'lua Lua,
|
||||
class_name: &str,
|
||||
method_name: &str,
|
||||
method: LuaFunction<'lua>,
|
||||
) -> Result<(), InstanceRegistryError> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
|
||||
let mut methods = registry
|
||||
.methods
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry methods");
|
||||
|
||||
let class_methods = methods.entry(class_name.to_string()).or_default();
|
||||
if class_methods.contains_key(method_name) {
|
||||
return Err(InstanceRegistryError::MethodAlreadyExists {
|
||||
class_name: class_name.to_string(),
|
||||
method_name: method_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let key = lua
|
||||
.create_registry_value(method)
|
||||
.expect("Failed to store method in lua registry");
|
||||
class_methods.insert(method_name.to_string(), key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
Inserts a property getter into the instance registry.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the property already exists in the registry.
|
||||
*/
|
||||
pub fn insert_property_getter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
class_name: &str,
|
||||
property_name: &str,
|
||||
property_getter: LuaFunction<'lua>,
|
||||
) -> Result<(), InstanceRegistryError> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
|
||||
let mut getters = registry
|
||||
.getters
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry getters");
|
||||
|
||||
let class_getters = getters.entry(class_name.to_string()).or_default();
|
||||
if class_getters.contains_key(property_name) {
|
||||
return Err(InstanceRegistryError::PropertyAlreadyExists {
|
||||
class_name: class_name.to_string(),
|
||||
property_name: property_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let key = lua
|
||||
.create_registry_value(property_getter)
|
||||
.expect("Failed to store getter in lua registry");
|
||||
class_getters.insert(property_name.to_string(), key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
Inserts a property setter into the instance registry.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the property already exists in the registry.
|
||||
*/
|
||||
pub fn insert_property_setter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
class_name: &str,
|
||||
property_name: &str,
|
||||
property_setter: LuaFunction<'lua>,
|
||||
) -> Result<(), InstanceRegistryError> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
|
||||
let mut setters = registry
|
||||
.setters
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry getters");
|
||||
|
||||
let class_setters = setters.entry(class_name.to_string()).or_default();
|
||||
if class_setters.contains_key(property_name) {
|
||||
return Err(InstanceRegistryError::PropertyAlreadyExists {
|
||||
class_name: class_name.to_string(),
|
||||
property_name: property_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let key = lua
|
||||
.create_registry_value(property_setter)
|
||||
.expect("Failed to store getter in lua registry");
|
||||
class_setters.insert(property_name.to_string(), key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a method in the instance registry.
|
||||
|
||||
Returns `None` if the method is not found.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn find_method<'lua>(
|
||||
lua: &'lua Lua,
|
||||
instance: &Instance,
|
||||
method_name: &str,
|
||||
) -> Option<LuaFunction<'lua>> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
let methods = registry
|
||||
.methods
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry methods");
|
||||
|
||||
class_name_chain(&instance.class_name)
|
||||
.iter()
|
||||
.find_map(|&class_name| {
|
||||
methods
|
||||
.get(class_name)
|
||||
.and_then(|class_methods| class_methods.get(method_name))
|
||||
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a property getter in the instance registry.
|
||||
|
||||
Returns `None` if the property getter is not found.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn find_property_getter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
instance: &Instance,
|
||||
property_name: &str,
|
||||
) -> Option<LuaFunction<'lua>> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
let getters = registry
|
||||
.getters
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry getters");
|
||||
|
||||
class_name_chain(&instance.class_name)
|
||||
.iter()
|
||||
.find_map(|&class_name| {
|
||||
getters
|
||||
.get(class_name)
|
||||
.and_then(|class_getters| class_getters.get(property_name))
|
||||
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a property setter in the instance registry.
|
||||
|
||||
Returns `None` if the property setter is not found.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn find_property_setter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
instance: &Instance,
|
||||
property_name: &str,
|
||||
) -> Option<LuaFunction<'lua>> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
let setters = registry
|
||||
.setters
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry setters");
|
||||
|
||||
class_name_chain(&instance.class_name)
|
||||
.iter()
|
||||
.find_map(|&class_name| {
|
||||
setters
|
||||
.get(class_name)
|
||||
.and_then(|class_setters| class_setters.get(property_name))
|
||||
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the class name chain for a given class name.
|
||||
|
||||
The chain starts with the given class name and ends with the root class.
|
||||
|
||||
# Panics
|
||||
|
||||
Panics if the class name is not valid.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn class_name_chain(class_name: &str) -> Vec<&str> {
|
||||
let db = rbx_reflection_database::get();
|
||||
|
||||
let mut list = vec![class_name];
|
||||
let mut current_name = class_name;
|
||||
|
||||
loop {
|
||||
let class_descriptor = db
|
||||
.classes
|
||||
.get(current_name)
|
||||
.expect("Got invalid class name");
|
||||
if let Some(sup) = &class_descriptor.superclass {
|
||||
current_name = sup.borrow();
|
||||
list.push(current_name);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
list
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{MaterialColors, TerrainMaterials, Variant};
|
||||
|
||||
use crate::roblox::{
|
||||
use crate::{
|
||||
datatypes::types::{Color3, EnumItem},
|
||||
shared::classes::{add_class_restricted_method, add_class_restricted_method_mut},
|
||||
};
|
||||
|
@ -23,13 +23,12 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M)
|
|||
CLASS_NAME,
|
||||
"SetMaterialColor",
|
||||
terrain_set_material_color,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
|
||||
if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors")
|
||||
{
|
||||
material_colors
|
||||
if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") {
|
||||
inner
|
||||
} else {
|
||||
MaterialColors::default()
|
||||
}
|
||||
|
@ -40,7 +39,7 @@ fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
|
|||
|
||||
### See Also
|
||||
* [`GetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#GetMaterialColor)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn terrain_get_material_color(_: &Lua, this: &Instance, material: EnumItem) -> LuaResult<Color3> {
|
||||
let material_colors = get_or_create_material_colors(this);
|
||||
|
@ -65,7 +64,7 @@ fn terrain_get_material_color(_: &Lua, this: &Instance, material: EnumItem) -> L
|
|||
|
||||
### See Also
|
||||
* [`SetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#SetMaterialColor)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn terrain_set_material_color(
|
||||
_: &Lua,
|
|
@ -1,16 +1,14 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use crate::roblox::shared::classes::{
|
||||
add_class_restricted_getter, get_or_create_property_ref_instance,
|
||||
};
|
||||
use crate::shared::classes::{add_class_restricted_getter, get_or_create_property_ref_instance};
|
||||
|
||||
use super::Instance;
|
||||
|
||||
pub const CLASS_NAME: &str = "Workspace";
|
||||
|
||||
pub fn add_fields<'lua, M: LuaUserDataFields<'lua, Instance>>(m: &mut M) {
|
||||
add_class_restricted_getter(m, CLASS_NAME, "Terrain", workspace_get_terrain);
|
||||
add_class_restricted_getter(m, CLASS_NAME, "CurrentCamera", workspace_get_camera);
|
||||
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
|
||||
add_class_restricted_getter(f, CLASS_NAME, "Terrain", workspace_get_terrain);
|
||||
add_class_restricted_getter(f, CLASS_NAME, "CurrentCamera", workspace_get_camera);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,7 +16,7 @@ pub fn add_fields<'lua, M: LuaUserDataFields<'lua, Instance>>(m: &mut M) {
|
|||
|
||||
### See Also
|
||||
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn workspace_get_terrain(_: &Lua, this: &Instance) -> LuaResult<Instance> {
|
||||
get_or_create_property_ref_instance(this, "Terrain", "Terrain")
|
||||
|
@ -29,7 +27,7 @@ fn workspace_get_terrain(_: &Lua, this: &Instance) -> LuaResult<Instance> {
|
|||
|
||||
### See Also
|
||||
* [`CurrentCamera`](https://create.roblox.com/docs/reference/engine/classes/Workspace#CurrentCamera)
|
||||
on the Roblox Developer Hub
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn workspace_get_camera(_: &Lua, this: &Instance) -> LuaResult<Instance> {
|
||||
get_or_create_property_ref_instance(this, "CurrentCamera", "Camera")
|
|
@ -1,6 +1,8 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use crate::lune::util::TableBuilder;
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
pub mod datatypes;
|
||||
pub mod document;
|
||||
|
@ -23,6 +25,7 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
|
|||
export::<Color3>(lua)?,
|
||||
export::<ColorSequence>(lua)?,
|
||||
export::<ColorSequenceKeypoint>(lua)?,
|
||||
export::<Content>(lua)?,
|
||||
export::<Faces>(lua)?,
|
||||
export::<Font>(lua)?,
|
||||
export::<NumberRange>(lua)?,
|
||||
|
@ -46,6 +49,16 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
|
|||
])
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a table containing all the Roblox datatypes, classes, and singletons.
|
||||
|
||||
Note that this is not guaranteed to contain any value unless indexed directly,
|
||||
it may be optimized to use lazy initialization in the future.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory or when a value cannot be created.
|
||||
*/
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
// FUTURE: We can probably create these lazily as users
|
||||
// index the main exports (this return value) table and
|
|
@ -7,7 +7,7 @@ use rbx_dom_weak::types::Variant as DomVariant;
|
|||
use rbx_reflection::{ClassDescriptor, DataType};
|
||||
|
||||
use super::{property::DatabaseProperty, utils::*};
|
||||
use crate::roblox::datatypes::{
|
||||
use crate::datatypes::{
|
||||
conversion::DomValueToLua, types::EnumItem, userdata_impl_eq, userdata_impl_to_string,
|
||||
};
|
||||
|
||||
|
@ -28,6 +28,7 @@ impl DatabaseClass {
|
|||
/**
|
||||
Get the name of this class.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_name(&self) -> String {
|
||||
self.0.name.to_string()
|
||||
}
|
||||
|
@ -37,6 +38,7 @@ impl DatabaseClass {
|
|||
|
||||
May be `None` if no parent class exists.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_superclass(&self) -> Option<String> {
|
||||
let sup = self.0.superclass.as_ref()?;
|
||||
Some(sup.to_string())
|
||||
|
@ -45,6 +47,7 @@ impl DatabaseClass {
|
|||
/**
|
||||
Get all known properties for this class.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_properties(&self) -> HashMap<String, DatabaseProperty> {
|
||||
self.0
|
||||
.properties
|
||||
|
@ -56,6 +59,7 @@ impl DatabaseClass {
|
|||
/**
|
||||
Get all default values for properties of this class.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_defaults(&self) -> HashMap<String, DomVariant> {
|
||||
self.0
|
||||
.default_properties
|
||||
|
@ -71,7 +75,12 @@ impl DatabaseClass {
|
|||
to players at runtime, and top-level class categories.
|
||||
*/
|
||||
pub fn get_tags_str(&self) -> Vec<&'static str> {
|
||||
self.0.tags.iter().map(class_tag_to_str).collect::<Vec<_>>()
|
||||
self.0
|
||||
.tags
|
||||
.iter()
|
||||
.copied()
|
||||
.map(class_tag_to_str)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,14 +144,12 @@ fn make_enum_value(inner: DbClass, name: impl AsRef<str>, value: u32) -> LuaResu
|
|||
let name = name.as_ref();
|
||||
let enum_name = find_enum_name(inner, name).ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get default property '{}' - No enum descriptor was found",
|
||||
name
|
||||
"Failed to get default property '{name}' - No enum descriptor was found",
|
||||
))
|
||||
})?;
|
||||
EnumItem::from_enum_name_and_value(&enum_name, value).ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get default property '{}' - Enum.{} does not contain numeric value {}",
|
||||
name, enum_name, value
|
||||
"Failed to get default property '{name}' - Enum.{enum_name} does not contain numeric value {value}",
|
||||
))
|
||||
})
|
||||
}
|
|
@ -4,7 +4,7 @@ use mlua::prelude::*;
|
|||
|
||||
use rbx_reflection::EnumDescriptor;
|
||||
|
||||
use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string};
|
||||
use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string};
|
||||
|
||||
type DbEnum = &'static EnumDescriptor<'static>;
|
||||
|
||||
|
@ -23,6 +23,7 @@ impl DatabaseEnum {
|
|||
/**
|
||||
Get the name of this enum.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_name(&self) -> String {
|
||||
self.0.name.to_string()
|
||||
}
|
||||
|
@ -31,8 +32,9 @@ impl DatabaseEnum {
|
|||
Get all known members of this enum.
|
||||
|
||||
Note that this is a direct map of name -> enum values,
|
||||
and does not actually use the EnumItem datatype itself.
|
||||
and does not actually use the `EnumItem` datatype itself.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_items(&self) -> HashMap<String, u32> {
|
||||
self.0
|
||||
.items
|
|
@ -4,7 +4,7 @@ use mlua::prelude::*;
|
|||
|
||||
use rbx_reflection::ReflectionDatabase;
|
||||
|
||||
use crate::roblox::datatypes::userdata_impl_eq;
|
||||
use crate::datatypes::userdata_impl_eq;
|
||||
|
||||
mod class;
|
||||
mod enums;
|
||||
|
@ -30,6 +30,7 @@ impl Database {
|
|||
/**
|
||||
Creates a new database struct, referencing the bundled reflection database.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
@ -40,6 +41,7 @@ impl Database {
|
|||
This will follow the format `x.y.z.w`, which most
|
||||
commonly looks something like `0.567.0.123456789`.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_version(&self) -> String {
|
||||
let [x, y, z, w] = self.0.version;
|
||||
format!("{x}.{y}.{z}.{w}")
|
||||
|
@ -48,15 +50,17 @@ impl Database {
|
|||
/**
|
||||
Retrieves a list of all currently known enum names.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_enum_names(&self) -> Vec<String> {
|
||||
self.0.enums.keys().map(|e| e.to_string()).collect()
|
||||
self.0.enums.keys().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves a list of all currently known class names.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_class_names(&self) -> Vec<String> {
|
||||
self.0.classes.keys().map(|e| e.to_string()).collect()
|
||||
self.0.classes.keys().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -108,14 +112,17 @@ impl Database {
|
|||
|
||||
impl LuaUserData for Database {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Version", |_, this| Ok(this.get_version()))
|
||||
fields.add_field_method_get("Version", |_, this| Ok(this.get_version()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_method("GetEnumNames", |_, this, _: ()| Ok(this.get_enum_names()));
|
||||
methods.add_method("GetClassNames", |_, this, _: ()| Ok(this.get_class_names()));
|
||||
methods.add_method("GetEnumNames", |_, this, (): ()| Ok(this.get_enum_names()));
|
||||
methods.add_method(
|
||||
"GetClassNames",
|
||||
|_, this, (): ()| Ok(this.get_class_names()),
|
||||
);
|
||||
methods.add_method("GetEnum", |_, this, name: String| Ok(this.get_enum(name)));
|
||||
methods.add_method("GetClass", |_, this, name: String| Ok(this.get_class(name)));
|
||||
methods.add_method("FindEnum", |_, this, name: String| Ok(this.find_enum(name)));
|
|
@ -5,7 +5,7 @@ use mlua::prelude::*;
|
|||
use rbx_reflection::{ClassDescriptor, PropertyDescriptor};
|
||||
|
||||
use super::utils::*;
|
||||
use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string};
|
||||
use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string};
|
||||
|
||||
type DbClass = &'static ClassDescriptor<'static>;
|
||||
type DbProp = &'static PropertyDescriptor<'static>;
|
||||
|
@ -25,6 +25,7 @@ impl DatabaseProperty {
|
|||
/**
|
||||
Get the name of this property.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_name(&self) -> String {
|
||||
self.1.name.to_string()
|
||||
}
|
||||
|
@ -36,6 +37,7 @@ impl DatabaseProperty {
|
|||
|
||||
For enums this will be a string formatted as `Enum.EnumName`.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_datatype_name(&self) -> String {
|
||||
data_type_to_str(self.1.data_type.clone())
|
||||
}
|
||||
|
@ -45,8 +47,9 @@ impl DatabaseProperty {
|
|||
|
||||
All properties are writable and readable in Lune even if scriptability is not.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_scriptability_str(&self) -> &'static str {
|
||||
scriptability_to_str(&self.1.scriptability)
|
||||
scriptability_to_str(self.1.scriptability)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,6 +62,7 @@ impl DatabaseProperty {
|
|||
self.1
|
||||
.tags
|
||||
.iter()
|
||||
.copied()
|
||||
.map(property_tag_to_str)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use rbx_reflection::{ClassTag, DataType, PropertyTag, Scriptability};
|
||||
|
||||
use crate::roblox::datatypes::extension::DomValueExt;
|
||||
use crate::datatypes::extension::DomValueExt;
|
||||
|
||||
pub fn data_type_to_str(data_type: DataType) -> String {
|
||||
match data_type {
|
||||
|
@ -17,7 +17,7 @@ pub fn data_type_to_str(data_type: DataType) -> String {
|
|||
NOTE: Remember to add any new strings here to typedefs too!
|
||||
*/
|
||||
|
||||
pub fn scriptability_to_str(scriptability: &Scriptability) -> &'static str {
|
||||
pub fn scriptability_to_str(scriptability: Scriptability) -> &'static str {
|
||||
match scriptability {
|
||||
Scriptability::None => "None",
|
||||
Scriptability::Custom => "Custom",
|
||||
|
@ -28,7 +28,7 @@ pub fn scriptability_to_str(scriptability: &Scriptability) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn property_tag_to_str(tag: &PropertyTag) -> &'static str {
|
||||
pub fn property_tag_to_str(tag: PropertyTag) -> &'static str {
|
||||
match tag {
|
||||
PropertyTag::Deprecated => "Deprecated",
|
||||
PropertyTag::Hidden => "Hidden",
|
||||
|
@ -41,7 +41,7 @@ pub fn property_tag_to_str(tag: &PropertyTag) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn class_tag_to_str(tag: &ClassTag) -> &'static str {
|
||||
pub fn class_tag_to_str(tag: ClassTag) -> &'static str {
|
||||
match tag {
|
||||
ClassTag::Deprecated => "Deprecated",
|
||||
ClassTag::NotBrowsable => "NotBrowsable",
|
|
@ -2,7 +2,7 @@ use mlua::prelude::*;
|
|||
|
||||
use rbx_dom_weak::types::Variant as DomValue;
|
||||
|
||||
use crate::roblox::instance::Instance;
|
||||
use crate::instance::Instance;
|
||||
|
||||
use super::instance::class_is_a;
|
||||
|
||||
|
@ -20,8 +20,7 @@ pub(crate) fn add_class_restricted_getter<'lua, F: LuaUserDataFields<'lua, Insta
|
|||
field_getter(lua, this)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{} is not a valid member of {}",
|
||||
field_name, class_name
|
||||
"{field_name} is not a valid member of {class_name}",
|
||||
)))
|
||||
}
|
||||
});
|
||||
|
@ -42,8 +41,7 @@ pub(crate) fn add_class_restricted_setter<'lua, F: LuaUserDataFields<'lua, Insta
|
|||
field_getter(lua, this, value)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{} is not a valid member of {}",
|
||||
field_name, class_name
|
||||
"{field_name} is not a valid member of {class_name}",
|
||||
)))
|
||||
}
|
||||
});
|
||||
|
@ -64,8 +62,7 @@ pub(crate) fn add_class_restricted_method<'lua, M: LuaUserDataMethods<'lua, Inst
|
|||
method(lua, this, args)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{} is not a valid member of {}",
|
||||
method_name, class_name
|
||||
"{method_name} is not a valid member of {class_name}",
|
||||
)))
|
||||
}
|
||||
});
|
||||
|
@ -92,8 +89,7 @@ pub(crate) fn add_class_restricted_method_mut<
|
|||
method(lua, this, args)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{} is not a valid member of {}",
|
||||
method_name, class_name
|
||||
"{method_name} is not a valid member of {class_name}",
|
||||
)))
|
||||
}
|
||||
});
|
||||
|
@ -102,7 +98,7 @@ pub(crate) fn add_class_restricted_method_mut<
|
|||
/**
|
||||
Gets or creates the instance child with the given reference prop name and class name.
|
||||
|
||||
Note that the class name here must be an exact match, it is not checked using IsA.
|
||||
Note that the class name here must be an exact match, it is not checked using `IsA`.
|
||||
|
||||
The instance may be in one of several states but this function will guarantee that the
|
||||
property reference is correct and that the instance exists after it has been called:
|
||||
|
@ -126,7 +122,7 @@ pub(crate) fn get_or_create_property_ref_instance(
|
|||
Ok(inst)
|
||||
} else {
|
||||
let inst = Instance::new_orphaned(class_name);
|
||||
inst.set_parent(Some(this.clone()));
|
||||
inst.set_parent(Some(*this));
|
||||
this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
|
||||
Ok(inst)
|
||||
}
|
|
@ -60,12 +60,12 @@ pub(crate) fn find_property_info(
|
|||
value_type: Some(*value_type),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Default::default(),
|
||||
_ => PropertyInfo::default(),
|
||||
});
|
||||
break;
|
||||
} else if let Some(sup) = &class.superclass {
|
||||
// No property found, we should look at the superclass
|
||||
class_name = Cow::Borrowed(sup)
|
||||
class_name = Cow::Borrowed(sup);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ pub(crate) fn find_property_info(
|
|||
break;
|
||||
} else if let Some(sup) = &class.superclass {
|
||||
// No default value found, we should look at the superclass
|
||||
class_name = Cow::Borrowed(sup)
|
||||
class_name = Cow::Borrowed(sup);
|
||||
} else {
|
||||
break;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::{any::type_name, cell::RefCell, fmt, ops};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
@ -5,21 +7,29 @@ use mlua::prelude::*;
|
|||
// Utility functions
|
||||
|
||||
type ListWriter = dyn Fn(&mut fmt::Formatter<'_>, bool, &str) -> fmt::Result;
|
||||
|
||||
#[must_use]
|
||||
pub fn make_list_writer() -> Box<ListWriter> {
|
||||
let first = RefCell::new(true);
|
||||
Box::new(move |f, flag, literal| {
|
||||
if flag {
|
||||
if first.take() {
|
||||
write!(f, "{}", literal)?;
|
||||
write!(f, "{literal}")?;
|
||||
} else {
|
||||
write!(f, ", {}", literal)?;
|
||||
write!(f, ", {literal}")?;
|
||||
}
|
||||
}
|
||||
Ok::<_, fmt::Error>(())
|
||||
})
|
||||
}
|
||||
|
||||
// Userdata metamethod implementations
|
||||
/*
|
||||
Userdata metamethod implementations
|
||||
|
||||
Note that many of these return [`LuaResult`] even though they don't
|
||||
return any errors - this is for consistency reasons and to make it
|
||||
easier to add these blanket implementations to [`LuaUserData`] impls.
|
||||
*/
|
||||
|
||||
pub fn userdata_impl_to_string<D>(_: &Lua, datatype: &D, _: ()) -> LuaResult<String>
|
||||
where
|
||||
|
@ -139,6 +149,37 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
pub trait IDiv<Rhs = Self> {
|
||||
type Output;
|
||||
#[must_use]
|
||||
fn idiv(self, rhs: Rhs) -> Self::Output;
|
||||
}
|
||||
|
||||
pub fn userdata_impl_idiv_f32<D>(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + IDiv<D, Output = D> + IDiv<f32, Output = D> + Copy + 'static,
|
||||
{
|
||||
match &rhs {
|
||||
LuaValue::Number(n) => return Ok(datatype.idiv(*n as f32)),
|
||||
LuaValue::Integer(i) => return Ok(datatype.idiv(*i as f32)),
|
||||
LuaValue::UserData(ud) => {
|
||||
if let Ok(vec) = ud.borrow::<D>() {
|
||||
return Ok(datatype.idiv(*vec));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: rhs.type_name(),
|
||||
to: type_name::<D>(),
|
||||
message: Some(format!(
|
||||
"Expected {} or number, got {}",
|
||||
type_name::<D>(),
|
||||
rhs.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn userdata_impl_div_i32<D>(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + ops::Div<D, Output = D> + ops::Div<i32, Output = D> + Copy + 'static,
|
22
crates/lune-std-datetime/Cargo.toml
Normal file
22
crates/lune-std-datetime/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "lune-std-datetime"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - DateTime"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau"] }
|
||||
|
||||
thiserror = "1.0"
|
||||
chrono = "0.4.38"
|
||||
chrono_lc = "0.1.6"
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
|
@ -6,31 +6,8 @@ use chrono::prelude::*;
|
|||
use chrono::DateTime as ChronoDateTime;
|
||||
use chrono_lc::LocaleDate;
|
||||
|
||||
use crate::lune::util::TableBuilder;
|
||||
|
||||
mod error;
|
||||
mod values;
|
||||
|
||||
use error::*;
|
||||
use values::*;
|
||||
|
||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("fromIsoDate", |_, iso_date: String| {
|
||||
Ok(DateTime::from_iso_date(iso_date)?)
|
||||
})?
|
||||
.with_function("fromLocalTime", |_, values| {
|
||||
Ok(DateTime::from_local_time(&values)?)
|
||||
})?
|
||||
.with_function("fromUniversalTime", |_, values| {
|
||||
Ok(DateTime::from_universal_time(&values)?)
|
||||
})?
|
||||
.with_function("fromUnixTimestamp", |_, timestamp| {
|
||||
Ok(DateTime::from_unix_timestamp_float(timestamp)?)
|
||||
})?
|
||||
.with_function("now", |_, ()| Ok(DateTime::now()))?
|
||||
.build_readonly()
|
||||
}
|
||||
use crate::result::{DateTimeError, DateTimeResult};
|
||||
use crate::values::DateTimeValues;
|
||||
|
||||
const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
|
||||
const DEFAULT_LOCALE: &str = "en";
|
||||
|
@ -49,6 +26,7 @@ impl DateTime {
|
|||
|
||||
See [`chrono::DateTime::now`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn now() -> Self {
|
||||
Self { inner: Utc::now() }
|
||||
}
|
||||
|
@ -66,6 +44,10 @@ impl DateTime {
|
|||
```
|
||||
|
||||
See [`chrono::DateTime::from_timestamp`] for additional details.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the input value is out of range.
|
||||
*/
|
||||
pub fn from_unix_timestamp_float(unix_timestamp: f64) -> DateTimeResult<Self> {
|
||||
let whole = unix_timestamp.trunc() as i64;
|
||||
|
@ -84,6 +66,10 @@ impl DateTime {
|
|||
|
||||
See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
|
||||
for additional details and cases where this constructor may return an error.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the date or time values are invalid.
|
||||
*/
|
||||
pub fn from_universal_time(values: &DateTimeValues) -> DateTimeResult<Self> {
|
||||
let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
|
||||
|
@ -108,6 +94,10 @@ impl DateTime {
|
|||
|
||||
See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
|
||||
for additional details and cases where this constructor may return an error.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the date or time values are invalid or ambiguous.
|
||||
*/
|
||||
pub fn from_local_time(values: &DateTimeValues) -> DateTimeResult<Self> {
|
||||
let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
|
||||
|
@ -138,6 +128,7 @@ impl DateTime {
|
|||
|
||||
See [`chrono_lc::DateTime::formatl`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn format_string_local(&self, format: Option<&str>, locale: Option<&str>) -> String {
|
||||
self.inner
|
||||
.with_timezone(&Local)
|
||||
|
@ -156,6 +147,7 @@ impl DateTime {
|
|||
|
||||
See [`chrono_lc::DateTime::formatl`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn format_string_universal(&self, format: Option<&str>, locale: Option<&str>) -> String {
|
||||
self.inner
|
||||
.with_timezone(&Utc)
|
||||
|
@ -171,6 +163,10 @@ impl DateTime {
|
|||
`1996-12-19T16:39:57-08:00`, into a new `DateTime` struct.
|
||||
|
||||
See [`chrono::DateTime::parse_from_rfc3339`] for additional details.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the input string is not a valid RFC 3339 date-time.
|
||||
*/
|
||||
pub fn from_iso_date(iso_date: impl AsRef<str>) -> DateTimeResult<Self> {
|
||||
let inner = ChronoDateTime::parse_from_rfc3339(iso_date.as_ref())?.with_timezone(&Utc);
|
||||
|
@ -181,6 +177,7 @@ impl DateTime {
|
|||
Extracts individual date & time values from this
|
||||
`DateTime`, using the current local time zone.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn to_local_time(self) -> DateTimeValues {
|
||||
DateTimeValues::from(self.inner.with_timezone(&Local))
|
||||
}
|
||||
|
@ -189,6 +186,7 @@ impl DateTime {
|
|||
Extracts individual date & time values from this
|
||||
`DateTime`, using the universal (UTC) time zone.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn to_universal_time(self) -> DateTimeValues {
|
||||
DateTimeValues::from(self.inner.with_timezone(&Utc))
|
||||
}
|
||||
|
@ -198,6 +196,7 @@ impl DateTime {
|
|||
|
||||
See [`chrono::DateTime::to_rfc3339`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn to_iso_date(self) -> String {
|
||||
self.inner.to_rfc3339()
|
||||
}
|
36
crates/lune-std-datetime/src/lib.rs
Normal file
36
crates/lune-std-datetime/src/lib.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
mod date_time;
|
||||
mod result;
|
||||
mod values;
|
||||
|
||||
pub use self::date_time::DateTime;
|
||||
|
||||
/**
|
||||
Creates the `datetime` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("fromIsoDate", |_, iso_date: String| {
|
||||
Ok(DateTime::from_iso_date(iso_date)?)
|
||||
})?
|
||||
.with_function("fromLocalTime", |_, values| {
|
||||
Ok(DateTime::from_local_time(&values)?)
|
||||
})?
|
||||
.with_function("fromUniversalTime", |_, values| {
|
||||
Ok(DateTime::from_universal_time(&values)?)
|
||||
})?
|
||||
.with_function("fromUnixTimestamp", |_, timestamp| {
|
||||
Ok(DateTime::from_unix_timestamp_float(timestamp)?)
|
||||
})?
|
||||
.with_function("now", |_, ()| Ok(DateTime::now()))?
|
||||
.build_readonly()
|
||||
}
|
|
@ -2,9 +2,9 @@ use mlua::prelude::*;
|
|||
|
||||
use chrono::prelude::*;
|
||||
|
||||
use crate::lune::util::TableBuilder;
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use super::error::{DateTimeError, DateTimeResult};
|
||||
use super::result::{DateTimeError, DateTimeResult};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DateTimeValues {
|
||||
|
@ -60,10 +60,10 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Conversion methods between DateTimeValues and plain lua tables
|
||||
/*
|
||||
Conversion methods between `DateTimeValues` and plain lua tables
|
||||
|
||||
Note that the IntoLua implementation here uses a read-only table,
|
||||
Note that the `IntoLua` implementation here uses a read-only table,
|
||||
since we generally want to convert into lua when we know we have
|
||||
a fixed point in time, and we guarantee that it doesn't change
|
||||
*/
|
||||
|
@ -117,9 +117,9 @@ impl IntoLua<'_> for DateTimeValues {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Conversion methods between chrono's timezone-aware DateTime to
|
||||
and from our non-timezone-aware DateTimeValues values struct
|
||||
/*
|
||||
Conversion methods between chrono's timezone-aware `DateTime` to
|
||||
and from our non-timezone-aware `DateTimeValues` values struct
|
||||
*/
|
||||
|
||||
impl<T: TimeZone> From<DateTime<T>> for DateTimeValues {
|
23
crates/lune-std-fs/Cargo.toml
Normal file
23
crates/lune-std-fs/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "lune-std-fs"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - FS"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau"] }
|
||||
|
||||
bstr = "1.9"
|
||||
|
||||
tokio = { version = "1", default-features = false, features = ["fs"] }
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
||||
lune-std-datetime = { version = "0.1.2", path = "../lune-std-datetime" }
|
|
@ -13,7 +13,7 @@ pub struct CopyContents {
|
|||
pub files: Vec<(usize, PathBuf)>,
|
||||
}
|
||||
|
||||
async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult<CopyContents> {
|
||||
async fn get_contents_at(root: PathBuf, _: FsWriteOptions) -> LuaResult<CopyContents> {
|
||||
let mut dirs = Vec::new();
|
||||
let mut files = Vec::new();
|
||||
|
||||
|
@ -53,11 +53,11 @@ async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult<C
|
|||
|
||||
// Ensure that all directory and file paths are relative to the root path
|
||||
// SAFETY: Since we only ever push dirs and files relative to the root, unwrap is safe
|
||||
for (_, dir) in dirs.iter_mut() {
|
||||
*dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf()
|
||||
for (_, dir) in &mut dirs {
|
||||
*dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf();
|
||||
}
|
||||
for (_, file) in files.iter_mut() {
|
||||
*file = file.strip_prefix(&normalized_root).unwrap().to_path_buf()
|
||||
for (_, file) in &mut files {
|
||||
*file = file.strip_prefix(&normalized_root).unwrap().to_path_buf();
|
||||
}
|
||||
|
||||
// FUTURE: Deduplicate paths such that these directories:
|
||||
|
@ -138,9 +138,20 @@ pub async fn copy(
|
|||
let contents = get_contents_at(source.to_path_buf(), options).await?;
|
||||
|
||||
if options.overwrite {
|
||||
fs::remove_dir_all(target).await?;
|
||||
let (is_dir, is_file) = match fs::metadata(&target).await {
|
||||
Ok(meta) => (meta.is_dir(), meta.is_file()),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => (false, false),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
if is_dir {
|
||||
fs::remove_dir_all(target).await?;
|
||||
} else if is_file {
|
||||
fs::remove_file(target).await?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(target).await?;
|
||||
|
||||
// FUTURE: Write dirs / files concurrently
|
||||
// to potentially speed these operations up
|
||||
for (_, dir) in &contents.dirs {
|
52
src/lune/builtins/fs/mod.rs → crates/lune-std-fs/src/lib.rs
Normal file → Executable file
52
src/lune/builtins/fs/mod.rs → crates/lune-std-fs/src/lib.rs
Normal file → Executable file
|
@ -1,20 +1,30 @@
|
|||
use std::io::ErrorKind as IoErrorKind;
|
||||
use std::path::{PathBuf, MAIN_SEPARATOR};
|
||||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use std::io::ErrorKind as IoErrorKind;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bstr::{BString, ByteSlice};
|
||||
use mlua::prelude::*;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::lune::util::TableBuilder;
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
mod copy;
|
||||
mod metadata;
|
||||
mod options;
|
||||
|
||||
use copy::copy;
|
||||
use metadata::FsMetadata;
|
||||
use options::FsWriteOptions;
|
||||
use self::copy::copy;
|
||||
use self::metadata::FsMetadata;
|
||||
use self::options::FsWriteOptions;
|
||||
|
||||
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
||||
/**
|
||||
Creates the `fs` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?
|
||||
.with_async_function("readFile", fs_read_file)?
|
||||
.with_async_function("readDir", fs_read_dir)?
|
||||
|
@ -32,6 +42,7 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
|||
|
||||
async fn fs_read_file(lua: &Lua, path: String) -> LuaResult<LuaString> {
|
||||
let bytes = fs::read(&path).await.into_lua_err()?;
|
||||
|
||||
lua.create_string(bytes)
|
||||
}
|
||||
|
||||
|
@ -39,33 +50,20 @@ async fn fs_read_dir(_: &Lua, path: String) -> LuaResult<Vec<String>> {
|
|||
let mut dir_strings = Vec::new();
|
||||
let mut dir = fs::read_dir(&path).await.into_lua_err()?;
|
||||
while let Some(dir_entry) = dir.next_entry().await.into_lua_err()? {
|
||||
if let Some(dir_path_str) = dir_entry.path().to_str() {
|
||||
dir_strings.push(dir_path_str.to_owned());
|
||||
if let Some(dir_name_str) = dir_entry.file_name().to_str() {
|
||||
dir_strings.push(dir_name_str.to_owned());
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"File path could not be converted into a string: '{}'",
|
||||
dir_entry.path().display()
|
||||
"File name could not be converted into a string: '{}'",
|
||||
dir_entry.file_name().to_string_lossy()
|
||||
)));
|
||||
}
|
||||
}
|
||||
let mut dir_string_prefix = path;
|
||||
if !dir_string_prefix.ends_with(MAIN_SEPARATOR) {
|
||||
dir_string_prefix.push(MAIN_SEPARATOR);
|
||||
}
|
||||
let dir_strings_no_prefix = dir_strings
|
||||
.iter()
|
||||
.map(|inner_path| {
|
||||
inner_path
|
||||
.trim()
|
||||
.trim_start_matches(&dir_string_prefix)
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(dir_strings_no_prefix)
|
||||
Ok(dir_strings)
|
||||
}
|
||||
|
||||
async fn fs_write_file(_: &Lua, (path, contents): (String, LuaString<'_>)) -> LuaResult<()> {
|
||||
fs::write(&path, &contents.as_bytes()).await.into_lua_err()
|
||||
async fn fs_write_file(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> {
|
||||
fs::write(&path, contents.as_bytes()).await.into_lua_err()
|
||||
}
|
||||
|
||||
async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> {
|
|
@ -8,6 +8,8 @@ use std::{
|
|||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_std_datetime::DateTime;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FsMetadataKind {
|
||||
None,
|
||||
|
@ -95,9 +97,9 @@ impl<'lua> IntoLua<'lua> for FsPermissions {
|
|||
pub struct FsMetadata {
|
||||
pub(crate) kind: FsMetadataKind,
|
||||
pub(crate) exists: bool,
|
||||
pub(crate) created_at: Option<f64>,
|
||||
pub(crate) modified_at: Option<f64>,
|
||||
pub(crate) accessed_at: Option<f64>,
|
||||
pub(crate) created_at: Option<DateTime>,
|
||||
pub(crate) modified_at: Option<DateTime>,
|
||||
pub(crate) accessed_at: Option<DateTime>,
|
||||
pub(crate) permissions: Option<FsPermissions>,
|
||||
}
|
||||
|
||||
|
@ -116,7 +118,7 @@ impl FsMetadata {
|
|||
|
||||
impl<'lua> IntoLua<'lua> for FsMetadata {
|
||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
||||
let tab = lua.create_table_with_capacity(0, 5)?;
|
||||
let tab = lua.create_table_with_capacity(0, 6)?;
|
||||
tab.set("kind", self.kind)?;
|
||||
tab.set("exists", self.exists)?;
|
||||
tab.set("createdAt", self.created_at)?;
|
||||
|
@ -133,7 +135,6 @@ impl From<StdMetadata> for FsMetadata {
|
|||
Self {
|
||||
kind: value.file_type().into(),
|
||||
exists: true,
|
||||
// FUTURE: Turn these into DateTime structs instead when that's implemented
|
||||
created_at: system_time_to_timestamp(value.created()),
|
||||
modified_at: system_time_to_timestamp(value.modified()),
|
||||
accessed_at: system_time_to_timestamp(value.accessed()),
|
||||
|
@ -142,10 +143,10 @@ impl From<StdMetadata> for FsMetadata {
|
|||
}
|
||||
}
|
||||
|
||||
fn system_time_to_timestamp(res: IoResult<SystemTime>) -> Option<f64> {
|
||||
fn system_time_to_timestamp(res: IoResult<SystemTime>) -> Option<DateTime> {
|
||||
match res {
|
||||
Ok(t) => match t.duration_since(SystemTime::UNIX_EPOCH) {
|
||||
Ok(d) => Some(d.as_secs_f64()),
|
||||
Ok(d) => DateTime::from_unix_timestamp_float(d.as_secs_f64()).ok(),
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
18
crates/lune-std-luau/Cargo.toml
Normal file
18
crates/lune-std-luau/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "lune-std-luau"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - Luau"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau", "luau-jit"] }
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
90
crates/lune-std-luau/src/lib.rs
Normal file
90
crates/lune-std-luau/src/lib.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::{jit::JitStatus, TableBuilder};
|
||||
|
||||
mod options;
|
||||
|
||||
use self::options::{LuauCompileOptions, LuauLoadOptions};
|
||||
|
||||
const BYTECODE_ERROR_BYTE: u8 = 0;
|
||||
|
||||
/**
|
||||
Creates the `luau` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("compile", compile_source)?
|
||||
.with_function("load", load_source)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
fn compile_source<'lua>(
|
||||
lua: &'lua Lua,
|
||||
(source, options): (LuaString<'lua>, LuauCompileOptions),
|
||||
) -> LuaResult<LuaString<'lua>> {
|
||||
let bytecode = options.into_compiler().compile(source);
|
||||
|
||||
match bytecode.first() {
|
||||
Some(&BYTECODE_ERROR_BYTE) => Err(LuaError::RuntimeError(
|
||||
String::from_utf8_lossy(&bytecode).into_owned(),
|
||||
)),
|
||||
Some(_) => lua.create_string(bytecode),
|
||||
None => panic!("Compiling resulted in empty bytecode"),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_source<'lua>(
|
||||
lua: &'lua Lua,
|
||||
(source, options): (LuaString<'lua>, LuauLoadOptions),
|
||||
) -> LuaResult<LuaFunction<'lua>> {
|
||||
let mut chunk = lua.load(source.as_bytes()).set_name(options.debug_name);
|
||||
let env_changed = options.environment.is_some();
|
||||
|
||||
if let Some(custom_environment) = options.environment {
|
||||
let environment = lua.create_table()?;
|
||||
|
||||
// Inject all globals into the environment
|
||||
if options.inject_globals {
|
||||
for pair in lua.globals().pairs() {
|
||||
let (key, value): (LuaValue, LuaValue) = pair?;
|
||||
environment.set(key, value)?;
|
||||
}
|
||||
|
||||
if let Some(global_metatable) = lua.globals().get_metatable() {
|
||||
environment.set_metatable(Some(global_metatable));
|
||||
}
|
||||
} else if let Some(custom_metatable) = custom_environment.get_metatable() {
|
||||
// Since we don't need to set the global metatable,
|
||||
// we can just set a custom metatable if it exists
|
||||
environment.set_metatable(Some(custom_metatable));
|
||||
}
|
||||
|
||||
// Inject the custom environment
|
||||
for pair in custom_environment.pairs() {
|
||||
let (key, value): (LuaValue, LuaValue) = pair?;
|
||||
environment.set(key, value)?;
|
||||
}
|
||||
|
||||
chunk = chunk.set_environment(environment);
|
||||
}
|
||||
|
||||
// Enable JIT if codegen is enabled and the environment hasn't
|
||||
// changed, otherwise disable JIT since it'll fall back anyways
|
||||
lua.enable_jit(options.codegen_enabled && !env_changed);
|
||||
let function = chunk.into_function()?;
|
||||
lua.enable_jit(
|
||||
lua.app_data_ref::<JitStatus>()
|
||||
.ok_or(LuaError::runtime(
|
||||
"Failed to get current JitStatus ref from AppData",
|
||||
))?
|
||||
.enabled(),
|
||||
);
|
||||
|
||||
Ok(function)
|
||||
}
|
|
@ -1,8 +1,14 @@
|
|||
#![allow(clippy::struct_field_names)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
use mlua::Compiler as LuaCompiler;
|
||||
|
||||
const DEFAULT_DEBUG_NAME: &str = "luau.load(...)";
|
||||
|
||||
/**
|
||||
Options for compiling Lua source code.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LuauCompileOptions {
|
||||
pub(crate) optimization_level: u8,
|
||||
pub(crate) coverage_level: u8,
|
||||
|
@ -76,6 +82,8 @@ impl<'lua> FromLua<'lua> for LuauCompileOptions {
|
|||
pub struct LuauLoadOptions<'lua> {
|
||||
pub(crate) debug_name: String,
|
||||
pub(crate) environment: Option<LuaTable<'lua>>,
|
||||
pub(crate) inject_globals: bool,
|
||||
pub(crate) codegen_enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for LuauLoadOptions<'_> {
|
||||
|
@ -83,6 +91,8 @@ impl Default for LuauLoadOptions<'_> {
|
|||
Self {
|
||||
debug_name: DEFAULT_DEBUG_NAME.to_string(),
|
||||
environment: None,
|
||||
inject_globals: true,
|
||||
codegen_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,11 +112,21 @@ impl<'lua> FromLua<'lua> for LuauLoadOptions<'lua> {
|
|||
options.environment = Some(environment);
|
||||
}
|
||||
|
||||
if let Some(inject_globals) = t.get("injectGlobals")? {
|
||||
options.inject_globals = inject_globals;
|
||||
}
|
||||
|
||||
if let Some(codegen_enabled) = t.get("codegenEnabled")? {
|
||||
options.codegen_enabled = codegen_enabled;
|
||||
}
|
||||
|
||||
options
|
||||
}
|
||||
LuaValue::String(s) => Self {
|
||||
debug_name: s.to_string_lossy().to_string(),
|
||||
environment: None,
|
||||
inject_globals: true,
|
||||
codegen_enabled: false,
|
||||
},
|
||||
_ => {
|
||||
return Err(LuaError::FromLuaConversionError {
|
39
crates/lune-std-net/Cargo.toml
Normal file
39
crates/lune-std-net/Cargo.toml
Normal file
|
@ -0,0 +1,39 @@
|
|||
[package]
|
||||
name = "lune-std-net"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - Net"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau"] }
|
||||
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
|
||||
|
||||
bstr = "1.9"
|
||||
futures-util = "0.3"
|
||||
hyper = { version = "1.1", features = ["full"] }
|
||||
hyper-util = { version = "0.1", features = ["full"] }
|
||||
http = "1.0"
|
||||
http-body-util = { version = "0.1" }
|
||||
hyper-tungstenite = { version = "0.13" }
|
||||
reqwest = { version = "0.11", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
] }
|
||||
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
|
||||
urlencoding = "2.1"
|
||||
|
||||
tokio = { version = "1", default-features = false, features = [
|
||||
"sync",
|
||||
"net",
|
||||
"macros",
|
||||
] }
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
||||
lune-std-serde = { version = "0.1.2", path = "../lune-std-serde" }
|
163
crates/lune-std-net/src/client.rs
Normal file
163
crates/lune-std-net/src/client.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_ENCODING};
|
||||
|
||||
use lune_std_serde::{decompress, CompressDecompressFormat};
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use super::{config::RequestConfig, util::header_map_to_table};
|
||||
|
||||
const REGISTRY_KEY: &str = "NetClient";
|
||||
|
||||
pub struct NetClientBuilder {
|
||||
builder: reqwest::ClientBuilder,
|
||||
}
|
||||
|
||||
impl NetClientBuilder {
|
||||
pub fn new() -> NetClientBuilder {
|
||||
Self {
|
||||
builder: reqwest::ClientBuilder::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headers<K, V>(mut self, headers: &[(K, V)]) -> LuaResult<Self>
|
||||
where
|
||||
K: AsRef<str>,
|
||||
V: AsRef<[u8]>,
|
||||
{
|
||||
let mut map = HeaderMap::new();
|
||||
for (key, val) in headers {
|
||||
let hkey = HeaderName::from_str(key.as_ref()).into_lua_err()?;
|
||||
let hval = HeaderValue::from_bytes(val.as_ref()).into_lua_err()?;
|
||||
map.insert(hkey, hval);
|
||||
}
|
||||
self.builder = self.builder.default_headers(map);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn build(self) -> LuaResult<NetClient> {
|
||||
let client = self.builder.build().into_lua_err()?;
|
||||
Ok(NetClient { inner: client })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetClient {
|
||||
inner: reqwest::Client,
|
||||
}
|
||||
|
||||
impl NetClient {
|
||||
pub fn from_registry(lua: &Lua) -> Self {
|
||||
lua.named_registry_value(REGISTRY_KEY)
|
||||
.expect("Failed to get NetClient from lua registry")
|
||||
}
|
||||
|
||||
pub fn into_registry(self, lua: &Lua) {
|
||||
lua.set_named_registry_value(REGISTRY_KEY, self)
|
||||
.expect("Failed to store NetClient in lua registry");
|
||||
}
|
||||
|
||||
pub async fn request(&self, config: RequestConfig) -> LuaResult<NetClientResponse> {
|
||||
// Create and send the request
|
||||
let mut request = self.inner.request(config.method, config.url);
|
||||
for (query, values) in config.query {
|
||||
request = request.query(
|
||||
&values
|
||||
.iter()
|
||||
.map(|v| (query.as_str(), v))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
for (header, values) in config.headers {
|
||||
for value in values {
|
||||
request = request.header(header.as_str(), value);
|
||||
}
|
||||
}
|
||||
let res = request
|
||||
.body(config.body.unwrap_or_default())
|
||||
.send()
|
||||
.await
|
||||
.into_lua_err()?;
|
||||
|
||||
// Extract status, headers
|
||||
let res_status = res.status().as_u16();
|
||||
let res_status_text = res.status().canonical_reason();
|
||||
let res_headers = res.headers().clone();
|
||||
|
||||
// Read response bytes
|
||||
let mut res_bytes = res.bytes().await.into_lua_err()?.to_vec();
|
||||
let mut res_decompressed = false;
|
||||
|
||||
// Check for extra options, decompression
|
||||
if config.options.decompress {
|
||||
let decompress_format = res_headers
|
||||
.iter()
|
||||
.find(|(name, _)| {
|
||||
name.as_str()
|
||||
.eq_ignore_ascii_case(CONTENT_ENCODING.as_str())
|
||||
})
|
||||
.and_then(|(_, value)| value.to_str().ok())
|
||||
.and_then(CompressDecompressFormat::detect_from_header_str);
|
||||
if let Some(format) = decompress_format {
|
||||
res_bytes = decompress(res_bytes, format).await?;
|
||||
res_decompressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NetClientResponse {
|
||||
ok: (200..300).contains(&res_status),
|
||||
status_code: res_status,
|
||||
status_message: res_status_text.unwrap_or_default().to_string(),
|
||||
headers: res_headers,
|
||||
body: res_bytes,
|
||||
body_decompressed: res_decompressed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for NetClient {}
|
||||
|
||||
impl FromLua<'_> for NetClient {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
if let LuaValue::UserData(ud) = value {
|
||||
if let Ok(ctx) = ud.borrow::<NetClient>() {
|
||||
return Ok(ctx.clone());
|
||||
}
|
||||
}
|
||||
unreachable!("NetClient should only be used from registry")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Lua> for NetClient {
|
||||
fn from(value: &Lua) -> Self {
|
||||
value
|
||||
.named_registry_value(REGISTRY_KEY)
|
||||
.expect("Missing require context in lua registry")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetClientResponse {
|
||||
ok: bool,
|
||||
status_code: u16,
|
||||
status_message: String,
|
||||
headers: HeaderMap,
|
||||
body: Vec<u8>,
|
||||
body_decompressed: bool,
|
||||
}
|
||||
|
||||
impl NetClientResponse {
|
||||
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("ok", self.ok)?
|
||||
.with_value("statusCode", self.status_code)?
|
||||
.with_value("statusMessage", self.status_message)?
|
||||
.with_value(
|
||||
"headers",
|
||||
header_map_to_table(lua, self.headers, self.body_decompressed)?,
|
||||
)?
|
||||
.with_value("body", lua.create_string(&self.body)?)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
231
crates/lune-std-net/src/config.rs
Normal file
231
crates/lune-std-net/src/config.rs
Normal file
|
@ -0,0 +1,231 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
};
|
||||
|
||||
use bstr::{BString, ByteSlice};
|
||||
use mlua::prelude::*;
|
||||
|
||||
use reqwest::Method;
|
||||
|
||||
use super::util::table_to_hash_map;
|
||||
|
||||
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
|
||||
return {
|
||||
status = 426,
|
||||
body = "Upgrade Required",
|
||||
headers = {
|
||||
Upgrade = "websocket",
|
||||
},
|
||||
}
|
||||
"#;
|
||||
|
||||
// Net request config
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequestConfigOptions {
|
||||
pub decompress: bool,
|
||||
}
|
||||
|
||||
impl Default for RequestConfigOptions {
|
||||
fn default() -> Self {
|
||||
Self { decompress: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for RequestConfigOptions {
|
||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
||||
if let LuaValue::Nil = value {
|
||||
// Nil means default options
|
||||
Ok(Self::default())
|
||||
} else if let LuaValue::Table(tab) = value {
|
||||
// Table means custom options
|
||||
let decompress = match tab.get::<_, Option<bool>>("decompress") {
|
||||
Ok(decomp) => Ok(decomp.unwrap_or(true)),
|
||||
Err(_) => Err(LuaError::RuntimeError(
|
||||
"Invalid option value for 'decompress' in request config options".to_string(),
|
||||
)),
|
||||
}?;
|
||||
Ok(Self { decompress })
|
||||
} else {
|
||||
// Anything else is invalid
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "RequestConfigOptions",
|
||||
message: Some(format!(
|
||||
"Invalid request config options - expected table or nil, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequestConfig {
|
||||
pub url: String,
|
||||
pub method: Method,
|
||||
pub query: HashMap<String, Vec<String>>,
|
||||
pub headers: HashMap<String, Vec<String>>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
pub options: RequestConfigOptions,
|
||||
}
|
||||
|
||||
impl FromLua<'_> for RequestConfig {
|
||||
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
|
||||
// If we just got a string we assume its a GET request to a given url
|
||||
if let LuaValue::String(s) = value {
|
||||
Ok(Self {
|
||||
url: s.to_string_lossy().to_string(),
|
||||
method: Method::GET,
|
||||
query: HashMap::new(),
|
||||
headers: HashMap::new(),
|
||||
body: None,
|
||||
options: RequestConfigOptions::default(),
|
||||
})
|
||||
} else if let LuaValue::Table(tab) = value {
|
||||
// If we got a table we are able to configure the entire request
|
||||
// Extract url
|
||||
let url = match tab.get::<_, LuaString>("url") {
|
||||
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
|
||||
Err(_) => Err(LuaError::runtime("Missing 'url' in request config")),
|
||||
}?;
|
||||
// Extract method
|
||||
let method = match tab.get::<_, LuaString>("method") {
|
||||
Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(),
|
||||
Err(_) => "GET".to_string(),
|
||||
};
|
||||
// Extract query
|
||||
let query = match tab.get::<_, LuaTable>("query") {
|
||||
Ok(tab) => table_to_hash_map(tab, "query")?,
|
||||
Err(_) => HashMap::new(),
|
||||
};
|
||||
// Extract headers
|
||||
let headers = match tab.get::<_, LuaTable>("headers") {
|
||||
Ok(tab) => table_to_hash_map(tab, "headers")?,
|
||||
Err(_) => HashMap::new(),
|
||||
};
|
||||
// Extract body
|
||||
let body = match tab.get::<_, BString>("body") {
|
||||
Ok(config_body) => Some(config_body.as_bytes().to_owned()),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
// Convert method string into proper enum
|
||||
let method = method.trim().to_ascii_uppercase();
|
||||
let method = match method.as_ref() {
|
||||
"GET" => Ok(Method::GET),
|
||||
"POST" => Ok(Method::POST),
|
||||
"PUT" => Ok(Method::PUT),
|
||||
"DELETE" => Ok(Method::DELETE),
|
||||
"HEAD" => Ok(Method::HEAD),
|
||||
"OPTIONS" => Ok(Method::OPTIONS),
|
||||
"PATCH" => Ok(Method::PATCH),
|
||||
_ => Err(LuaError::RuntimeError(format!(
|
||||
"Invalid request config method '{}'",
|
||||
&method
|
||||
))),
|
||||
}?;
|
||||
// Parse any extra options given
|
||||
let options = match tab.get::<_, LuaValue>("options") {
|
||||
Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?,
|
||||
Err(_) => RequestConfigOptions::default(),
|
||||
};
|
||||
// All good, validated and we got what we need
|
||||
Ok(Self {
|
||||
url,
|
||||
method,
|
||||
query,
|
||||
headers,
|
||||
body,
|
||||
options,
|
||||
})
|
||||
} else {
|
||||
// Anything else is invalid
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "RequestConfig",
|
||||
message: Some(format!(
|
||||
"Invalid request config - expected string or table, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Net serve config
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServeConfig<'a> {
|
||||
pub address: IpAddr,
|
||||
pub handle_request: LuaFunction<'a>,
|
||||
pub handle_web_socket: Option<LuaFunction<'a>>,
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for ServeConfig<'lua> {
|
||||
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
|
||||
if let LuaValue::Function(f) = &value {
|
||||
// Single function = request handler, rest is default
|
||||
Ok(ServeConfig {
|
||||
handle_request: f.clone(),
|
||||
handle_web_socket: None,
|
||||
address: DEFAULT_IP_ADDRESS,
|
||||
})
|
||||
} else if let LuaValue::Table(t) = &value {
|
||||
// Table means custom options
|
||||
let address: Option<LuaString> = t.get("address")?;
|
||||
let handle_request: Option<LuaFunction> = t.get("handleRequest")?;
|
||||
let handle_web_socket: Option<LuaFunction> = t.get("handleWebSocket")?;
|
||||
if handle_request.is_some() || handle_web_socket.is_some() {
|
||||
let address: IpAddr = match &address {
|
||||
Some(addr) => {
|
||||
let addr_str = addr.to_str()?;
|
||||
|
||||
addr_str
|
||||
.trim_start_matches("http://")
|
||||
.trim_start_matches("https://")
|
||||
.parse()
|
||||
.map_err(|_e| LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "ServeConfig",
|
||||
message: Some(format!(
|
||||
"IP address format is incorrect - \
|
||||
expected an IP in the form 'http://0.0.0.0' or '0.0.0.0', \
|
||||
got '{addr_str}'"
|
||||
)),
|
||||
})?
|
||||
}
|
||||
None => DEFAULT_IP_ADDRESS,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
address,
|
||||
handle_request: handle_request.unwrap_or_else(|| {
|
||||
lua.load(WEB_SOCKET_UPDGRADE_REQUEST_HANDLER)
|
||||
.into_function()
|
||||
.expect("Failed to create default http responder function")
|
||||
}),
|
||||
handle_web_socket,
|
||||
})
|
||||
} else {
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "ServeConfig",
|
||||
message: Some(String::from(
|
||||
"Invalid serve config - expected table with 'handleRequest' or 'handleWebSocket' function",
|
||||
)),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Anything else is invalid
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "ServeConfig",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
102
crates/lune-std-net/src/lib.rs
Normal file
102
crates/lune-std-net/src/lib.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use bstr::BString;
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::LuaSpawnExt;
|
||||
|
||||
mod client;
|
||||
mod config;
|
||||
mod server;
|
||||
mod util;
|
||||
mod websocket;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use self::{
|
||||
client::{NetClient, NetClientBuilder},
|
||||
config::{RequestConfig, ServeConfig},
|
||||
server::serve,
|
||||
util::create_user_agent_header,
|
||||
websocket::NetWebSocket,
|
||||
};
|
||||
|
||||
use lune_std_serde::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
|
||||
|
||||
/**
|
||||
Creates the `net` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
NetClientBuilder::new()
|
||||
.headers(&[("User-Agent", create_user_agent_header(lua)?)])?
|
||||
.build()?
|
||||
.into_registry(lua);
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("jsonEncode", net_json_encode)?
|
||||
.with_function("jsonDecode", net_json_decode)?
|
||||
.with_async_function("request", net_request)?
|
||||
.with_async_function("socket", net_socket)?
|
||||
.with_async_function("serve", net_serve)?
|
||||
.with_function("urlEncode", net_url_encode)?
|
||||
.with_function("urlDecode", net_url_decode)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
fn net_json_encode<'lua>(
|
||||
lua: &'lua Lua,
|
||||
(val, pretty): (LuaValue<'lua>, Option<bool>),
|
||||
) -> LuaResult<LuaString<'lua>> {
|
||||
let config = EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()));
|
||||
encode(val, lua, config)
|
||||
}
|
||||
|
||||
fn net_json_decode(lua: &Lua, json: BString) -> LuaResult<LuaValue> {
|
||||
let config = EncodeDecodeConfig::from(EncodeDecodeFormat::Json);
|
||||
decode(json, lua, config)
|
||||
}
|
||||
|
||||
async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
|
||||
let client = NetClient::from_registry(lua);
|
||||
// NOTE: We spawn the request as a background task to free up resources in lua
|
||||
let res = lua.spawn(async move { client.request(config).await });
|
||||
res.await?.into_lua_table(lua)
|
||||
}
|
||||
|
||||
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaValue> {
|
||||
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
|
||||
NetWebSocket::new(ws).into_lua(lua)
|
||||
}
|
||||
|
||||
async fn net_serve<'lua>(
|
||||
lua: &'lua Lua,
|
||||
(port, config): (u16, ServeConfig<'lua>),
|
||||
) -> LuaResult<LuaTable<'lua>> {
|
||||
serve(lua, port, config).await
|
||||
}
|
||||
|
||||
fn net_url_encode<'lua>(
|
||||
lua: &'lua Lua,
|
||||
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
|
||||
) -> LuaResult<LuaValue<'lua>> {
|
||||
if matches!(as_binary, Some(true)) {
|
||||
urlencoding::encode_binary(lua_string.as_bytes()).into_lua(lua)
|
||||
} else {
|
||||
urlencoding::encode(lua_string.to_str()?).into_lua(lua)
|
||||
}
|
||||
}
|
||||
|
||||
fn net_url_decode<'lua>(
|
||||
lua: &'lua Lua,
|
||||
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
|
||||
) -> LuaResult<LuaValue<'lua>> {
|
||||
if matches!(as_binary, Some(true)) {
|
||||
urlencoding::decode_binary(lua_string.as_bytes()).into_lua(lua)
|
||||
} else {
|
||||
urlencoding::decode(lua_string.to_str()?)
|
||||
.map_err(|e| LuaError::RuntimeError(format!("Encountered invalid encoding - {e}")))?
|
||||
.into_lua(lua)
|
||||
}
|
||||
}
|
61
crates/lune-std-net/src/server/keys.rs
Normal file
61
crates/lune-std-net/src/server/keys.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) struct SvcKeys {
|
||||
key_request: &'static str,
|
||||
key_websocket: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl SvcKeys {
|
||||
pub(super) fn new<'lua>(
|
||||
lua: &'lua Lua,
|
||||
handle_request: LuaFunction<'lua>,
|
||||
handle_websocket: Option<LuaFunction<'lua>>,
|
||||
) -> LuaResult<Self> {
|
||||
static SERVE_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
let count = SERVE_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
// NOTE: We leak strings here, but this is an acceptable tradeoff since programs
|
||||
// generally only start one or a couple of servers and they are usually never dropped.
|
||||
// Leaking here lets us keep this struct Copy and access the request handler callbacks
|
||||
// very performantly, significantly reducing the per-request overhead of the server.
|
||||
let key_request: &'static str =
|
||||
Box::leak(format!("__net_serve_request_{count}").into_boxed_str());
|
||||
let key_websocket: Option<&'static str> = if handle_websocket.is_some() {
|
||||
Some(Box::leak(
|
||||
format!("__net_serve_websocket_{count}").into_boxed_str(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
lua.set_named_registry_value(key_request, handle_request)?;
|
||||
if let Some(key) = key_websocket {
|
||||
lua.set_named_registry_value(key, handle_websocket.unwrap())?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
key_request,
|
||||
key_websocket,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn has_websocket_handler(&self) -> bool {
|
||||
self.key_websocket.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn request_handler<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaFunction<'lua>> {
|
||||
lua.named_registry_value(self.key_request)
|
||||
}
|
||||
|
||||
pub(super) fn websocket_handler<'lua>(
|
||||
&self,
|
||||
lua: &'lua Lua,
|
||||
) -> LuaResult<Option<LuaFunction<'lua>>> {
|
||||
self.key_websocket
|
||||
.map(|key| lua.named_registry_value(key))
|
||||
.transpose()
|
||||
}
|
||||
}
|
105
crates/lune-std-net/src/server/mod.rs
Normal file
105
crates/lune-std-net/src/server/mod.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::{
|
||||
net::SocketAddr,
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
|
||||
use hyper::server::conn::http1;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::{net::TcpListener, pin};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::LuaSpawnExt;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use super::config::ServeConfig;
|
||||
|
||||
mod keys;
|
||||
mod request;
|
||||
mod response;
|
||||
mod service;
|
||||
|
||||
use keys::SvcKeys;
|
||||
use service::Svc;
|
||||
|
||||
pub async fn serve<'lua>(
|
||||
lua: &'lua Lua,
|
||||
port: u16,
|
||||
config: ServeConfig<'lua>,
|
||||
) -> LuaResult<LuaTable<'lua>> {
|
||||
let addr: SocketAddr = (config.address, port).into();
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
|
||||
let (lua_svc, lua_inner) = {
|
||||
let rc = lua
|
||||
.app_data_ref::<Weak<Lua>>()
|
||||
.expect("Missing weak lua ref")
|
||||
.upgrade()
|
||||
.expect("Lua was dropped unexpectedly");
|
||||
(Rc::clone(&rc), rc)
|
||||
};
|
||||
|
||||
let keys = SvcKeys::new(lua, config.handle_request, config.handle_web_socket)?;
|
||||
let svc = Svc {
|
||||
lua: lua_svc,
|
||||
addr,
|
||||
keys,
|
||||
};
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
|
||||
lua.spawn_local(async move {
|
||||
let mut shutdown_rx_outer = shutdown_rx.clone();
|
||||
loop {
|
||||
// Create futures for accepting new connections and shutting down
|
||||
let fut_shutdown = shutdown_rx_outer.changed();
|
||||
let fut_accept = async {
|
||||
let stream = match listener.accept().await {
|
||||
Err(_) => return,
|
||||
Ok((s, _)) => s,
|
||||
};
|
||||
|
||||
let io = TokioIo::new(stream);
|
||||
let svc = svc.clone();
|
||||
let mut shutdown_rx_inner = shutdown_rx.clone();
|
||||
|
||||
lua_inner.spawn_local(async move {
|
||||
let conn = http1::Builder::new()
|
||||
.keep_alive(true) // Web sockets need this
|
||||
.serve_connection(io, svc)
|
||||
.with_upgrades();
|
||||
// NOTE: Because we need to use keep_alive for websockets, we need to
|
||||
// also manually poll this future and handle the shutdown signal here
|
||||
pin!(conn);
|
||||
tokio::select! {
|
||||
_ = conn.as_mut() => {}
|
||||
_ = shutdown_rx_inner.changed() => {
|
||||
conn.as_mut().graceful_shutdown();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Wait for either a new connection or a shutdown signal
|
||||
tokio::select! {
|
||||
() = fut_accept => {}
|
||||
res = fut_shutdown => {
|
||||
// NOTE: We will only get a RecvError here if the serve handle is dropped,
|
||||
// this means lua has garbage collected it and the user does not want
|
||||
// to manually stop the server using the serve handle. Run forever.
|
||||
if res.is_ok() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("ip", addr.ip().to_string())?
|
||||
.with_value("port", addr.port())?
|
||||
.with_function("stop", move |_, (): ()| match shutdown_tx.send(true) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => Err(LuaError::runtime("Server already stopped")),
|
||||
})?
|
||||
.build_readonly()
|
||||
}
|
56
crates/lune-std-net/src/server/request.rs
Normal file
56
crates/lune-std-net/src/server/request.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use std::{collections::HashMap, net::SocketAddr};
|
||||
|
||||
use http::request::Parts;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
pub(super) struct LuaRequest {
|
||||
pub(super) _remote_addr: SocketAddr,
|
||||
pub(super) head: Parts,
|
||||
pub(super) body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl LuaRequest {
|
||||
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let method = self.head.method.as_str().to_string();
|
||||
let path = self.head.uri.path().to_string();
|
||||
let body = lua.create_string(&self.body)?;
|
||||
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let query: HashMap<LuaString, LuaString> = self
|
||||
.head
|
||||
.uri
|
||||
.query()
|
||||
.unwrap_or_default()
|
||||
.split('&')
|
||||
.filter_map(|q| q.split_once('='))
|
||||
.map(|(k, v)| {
|
||||
let k = lua.create_string(k)?;
|
||||
let v = lua.create_string(v)?;
|
||||
Ok((k, v))
|
||||
})
|
||||
.collect::<LuaResult<_>>()?;
|
||||
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let headers: HashMap<LuaString, LuaString> = self
|
||||
.head
|
||||
.headers
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let k = lua.create_string(k.as_str())?;
|
||||
let v = lua.create_string(v.as_bytes())?;
|
||||
Ok((k, v))
|
||||
})
|
||||
.collect::<LuaResult<_>>()?;
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("method", method)?
|
||||
.with_value("path", path)?
|
||||
.with_value("query", query)?
|
||||
.with_value("headers", headers)?
|
||||
.with_value("body", body)?
|
||||
.build()
|
||||
}
|
||||
}
|
|
@ -1,72 +1,78 @@
|
|||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bstr::{BString, ByteSlice};
|
||||
use http_body_util::Full;
|
||||
use hyper::{
|
||||
body::Bytes,
|
||||
header::{HeaderName, HeaderValue},
|
||||
HeaderMap, Response,
|
||||
};
|
||||
|
||||
use hyper::{Body, Response};
|
||||
use mlua::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum NetServeResponseKind {
|
||||
pub(super) enum LuaResponseKind {
|
||||
PlainText,
|
||||
Table,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NetServeResponse {
|
||||
kind: NetServeResponseKind,
|
||||
status: u16,
|
||||
headers: HashMap<String, Vec<u8>>,
|
||||
body: Option<Vec<u8>>,
|
||||
pub(super) struct LuaResponse {
|
||||
pub(super) kind: LuaResponseKind,
|
||||
pub(super) status: u16,
|
||||
pub(super) headers: HeaderMap,
|
||||
pub(super) body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl NetServeResponse {
|
||||
pub fn into_response(self) -> LuaResult<Response<Body>> {
|
||||
impl LuaResponse {
|
||||
pub(super) fn into_response(self) -> LuaResult<Response<Full<Bytes>>> {
|
||||
Ok(match self.kind {
|
||||
NetServeResponseKind::PlainText => Response::builder()
|
||||
LuaResponseKind::PlainText => Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(Body::from(self.body.unwrap()))
|
||||
.body(Full::new(Bytes::from(self.body.unwrap())))
|
||||
.into_lua_err()?,
|
||||
NetServeResponseKind::Table => {
|
||||
let mut response = Response::builder();
|
||||
for (key, value) in self.headers {
|
||||
response = response.header(&key, value);
|
||||
}
|
||||
response
|
||||
LuaResponseKind::Table => {
|
||||
let mut response = Response::builder()
|
||||
.status(self.status)
|
||||
.body(Body::from(self.body.unwrap_or_default()))
|
||||
.into_lua_err()?
|
||||
.body(Full::new(Bytes::from(self.body.unwrap_or_default())))
|
||||
.into_lua_err()?;
|
||||
response.headers_mut().extend(self.headers);
|
||||
response
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for NetServeResponse {
|
||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
||||
impl FromLua<'_> for LuaResponse {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
match value {
|
||||
// Plain strings from the handler are plaintext responses
|
||||
LuaValue::String(s) => Ok(Self {
|
||||
kind: NetServeResponseKind::PlainText,
|
||||
kind: LuaResponseKind::PlainText,
|
||||
status: 200,
|
||||
headers: HashMap::new(),
|
||||
headers: HeaderMap::new(),
|
||||
body: Some(s.as_bytes().to_vec()),
|
||||
}),
|
||||
// Tables are more detailed responses with potential status, headers, body
|
||||
LuaValue::Table(t) => {
|
||||
let status: Option<u16> = t.get("status")?;
|
||||
let headers: Option<LuaTable> = t.get("headers")?;
|
||||
let body: Option<LuaString> = t.get("body")?;
|
||||
let body: Option<BString> = t.get("body")?;
|
||||
|
||||
let mut headers_map = HashMap::new();
|
||||
let mut headers_map = HeaderMap::new();
|
||||
if let Some(headers) = headers {
|
||||
for pair in headers.pairs::<String, LuaString>() {
|
||||
let (h, v) = pair?;
|
||||
headers_map.insert(h, v.as_bytes().to_vec());
|
||||
let name = HeaderName::from_str(&h).into_lua_err()?;
|
||||
let value = HeaderValue::from_bytes(v.as_bytes()).into_lua_err()?;
|
||||
headers_map.insert(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
let body_bytes = body.map(|s| s.as_bytes().to_vec());
|
||||
|
||||
Ok(Self {
|
||||
kind: NetServeResponseKind::Table,
|
||||
kind: LuaResponseKind::Table,
|
||||
status: status.unwrap_or(200),
|
||||
headers: headers_map,
|
||||
body: body_bytes,
|
82
crates/lune-std-net/src/server/service.rs
Normal file
82
crates/lune-std-net/src/server/service.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use std::{future::Future, net::SocketAddr, pin::Pin, rc::Rc};
|
||||
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::{
|
||||
body::{Bytes, Incoming},
|
||||
service::Service,
|
||||
Request, Response,
|
||||
};
|
||||
use hyper_tungstenite::{is_upgrade_request, upgrade};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt};
|
||||
|
||||
use super::{
|
||||
super::websocket::NetWebSocket, keys::SvcKeys, request::LuaRequest, response::LuaResponse,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Svc {
|
||||
pub(super) lua: Rc<Lua>,
|
||||
pub(super) addr: SocketAddr,
|
||||
pub(super) keys: SvcKeys,
|
||||
}
|
||||
|
||||
impl Service<Request<Incoming>> for Svc {
|
||||
type Response = Response<Full<Bytes>>;
|
||||
type Error = LuaError;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
|
||||
fn call(&self, req: Request<Incoming>) -> Self::Future {
|
||||
let lua = self.lua.clone();
|
||||
let addr = self.addr;
|
||||
let keys = self.keys;
|
||||
|
||||
if keys.has_websocket_handler() && is_upgrade_request(&req) {
|
||||
Box::pin(async move {
|
||||
let (res, sock) = upgrade(req, None).into_lua_err()?;
|
||||
|
||||
let lua_inner = lua.clone();
|
||||
lua.spawn_local(async move {
|
||||
let sock = sock.await.unwrap();
|
||||
let lua_sock = NetWebSocket::new(sock);
|
||||
let lua_val = lua_sock.into_lua(&lua_inner).unwrap();
|
||||
|
||||
let handler_websocket: LuaFunction =
|
||||
keys.websocket_handler(&lua_inner).unwrap().unwrap();
|
||||
|
||||
lua_inner
|
||||
.push_thread_back(handler_websocket, lua_val)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
})
|
||||
} else {
|
||||
let (head, body) = req.into_parts();
|
||||
|
||||
Box::pin(async move {
|
||||
let handler_request: LuaFunction = keys.request_handler(&lua).unwrap();
|
||||
|
||||
let body = body.collect().await.into_lua_err()?;
|
||||
let body = body.to_bytes().to_vec();
|
||||
|
||||
let lua_req = LuaRequest {
|
||||
_remote_addr: addr,
|
||||
head,
|
||||
body,
|
||||
};
|
||||
let lua_req_table = lua_req.into_lua_table(&lua)?;
|
||||
|
||||
let thread_id = lua.push_thread_back(handler_request, lua_req_table)?;
|
||||
lua.track_thread(thread_id);
|
||||
lua.wait_for_thread(thread_id).await;
|
||||
let thread_res = lua
|
||||
.get_thread_result(thread_id)
|
||||
.expect("Missing handler thread result")?;
|
||||
|
||||
LuaResponse::from_lua_multi(thread_res, &lua)?.into_response()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
94
crates/lune-std-net/src/util.rs
Normal file
94
crates/lune-std-net/src/util.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
pub fn create_user_agent_header(lua: &Lua) -> LuaResult<String> {
|
||||
let version_global = lua
|
||||
.globals()
|
||||
.get::<_, LuaString>("_VERSION")
|
||||
.expect("Missing _VERSION global");
|
||||
|
||||
let version_global_str = version_global
|
||||
.to_str()
|
||||
.context("Invalid utf8 found in _VERSION global")?;
|
||||
|
||||
let (package_name, full_version) = version_global_str.split_once(' ').unwrap();
|
||||
|
||||
Ok(format!("{}/{}", package_name.to_lowercase(), full_version))
|
||||
}
|
||||
|
||||
pub fn header_map_to_table(
|
||||
lua: &Lua,
|
||||
headers: HeaderMap,
|
||||
remove_content_headers: bool,
|
||||
) -> LuaResult<LuaTable> {
|
||||
let mut res_headers: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for (name, value) in &headers {
|
||||
let name = name.as_str();
|
||||
let value = value.to_str().unwrap().to_owned();
|
||||
if let Some(existing) = res_headers.get_mut(name) {
|
||||
existing.push(value);
|
||||
} else {
|
||||
res_headers.insert(name.to_owned(), vec![value]);
|
||||
}
|
||||
}
|
||||
|
||||
if remove_content_headers {
|
||||
let content_encoding_header_str = CONTENT_ENCODING.as_str();
|
||||
let content_length_header_str = CONTENT_LENGTH.as_str();
|
||||
res_headers.retain(|name, _| {
|
||||
name != content_encoding_header_str && name != content_length_header_str
|
||||
});
|
||||
}
|
||||
|
||||
let mut builder = TableBuilder::new(lua)?;
|
||||
for (name, mut values) in res_headers {
|
||||
if values.len() == 1 {
|
||||
let value = values.pop().unwrap().into_lua(lua)?;
|
||||
builder = builder.with_value(name, value)?;
|
||||
} else {
|
||||
let values = TableBuilder::new(lua)?
|
||||
.with_sequential_values(values)?
|
||||
.build_readonly()?
|
||||
.into_lua(lua)?;
|
||||
builder = builder.with_value(name, values)?;
|
||||
}
|
||||
}
|
||||
|
||||
builder.build_readonly()
|
||||
}
|
||||
|
||||
pub fn table_to_hash_map(
|
||||
tab: LuaTable,
|
||||
tab_origin_key: &'static str,
|
||||
) -> LuaResult<HashMap<String, Vec<String>>> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for pair in tab.pairs::<String, LuaValue>() {
|
||||
let (key, value) = pair?;
|
||||
match value {
|
||||
LuaValue::String(s) => {
|
||||
map.insert(key, vec![s.to_str()?.to_owned()]);
|
||||
}
|
||||
LuaValue::Table(t) => {
|
||||
let mut values = Vec::new();
|
||||
for value in t.sequence_values::<LuaString>() {
|
||||
values.push(value?.to_str()?.to_owned());
|
||||
}
|
||||
map.insert(key, values);
|
||||
}
|
||||
_ => {
|
||||
return Err(LuaError::runtime(format!(
|
||||
"Value for '{tab_origin_key}' must be a string or array of strings",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
149
crates/lune-std-net/src/websocket.rs
Normal file
149
crates/lune-std-net/src/websocket.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicU16, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use bstr::{BString, ByteSlice};
|
||||
use mlua::prelude::*;
|
||||
|
||||
use futures_util::{
|
||||
stream::{SplitSink, SplitStream},
|
||||
SinkExt, StreamExt,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
sync::Mutex as AsyncMutex,
|
||||
};
|
||||
|
||||
use hyper_tungstenite::{
|
||||
tungstenite::{
|
||||
protocol::{frame::coding::CloseCode as WsCloseCode, CloseFrame as WsCloseFrame},
|
||||
Message as WsMessage,
|
||||
},
|
||||
WebSocketStream,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NetWebSocket<T> {
|
||||
close_code_exists: Arc<AtomicBool>,
|
||||
close_code_value: Arc<AtomicU16>,
|
||||
read_stream: Arc<AsyncMutex<SplitStream<WebSocketStream<T>>>>,
|
||||
write_stream: Arc<AsyncMutex<SplitSink<WebSocketStream<T>, WsMessage>>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for NetWebSocket<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
close_code_exists: Arc::clone(&self.close_code_exists),
|
||||
close_code_value: Arc::clone(&self.close_code_value),
|
||||
read_stream: Arc::clone(&self.read_stream),
|
||||
write_stream: Arc::clone(&self.write_stream),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> NetWebSocket<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
pub fn new(value: WebSocketStream<T>) -> Self {
|
||||
let (write, read) = value.split();
|
||||
|
||||
Self {
|
||||
close_code_exists: Arc::new(AtomicBool::new(false)),
|
||||
close_code_value: Arc::new(AtomicU16::new(0)),
|
||||
read_stream: Arc::new(AsyncMutex::new(read)),
|
||||
write_stream: Arc::new(AsyncMutex::new(write)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_close_code(&self) -> Option<u16> {
|
||||
if self.close_code_exists.load(Ordering::Relaxed) {
|
||||
Some(self.close_code_value.load(Ordering::Relaxed))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_close_code(&self, code: u16) {
|
||||
self.close_code_exists.store(true, Ordering::Relaxed);
|
||||
self.close_code_value.store(code, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub async fn send(&self, msg: WsMessage) -> LuaResult<()> {
|
||||
let mut ws = self.write_stream.lock().await;
|
||||
ws.send(msg).await.into_lua_err()
|
||||
}
|
||||
|
||||
pub async fn next(&self) -> LuaResult<Option<WsMessage>> {
|
||||
let mut ws = self.read_stream.lock().await;
|
||||
ws.next().await.transpose().into_lua_err()
|
||||
}
|
||||
|
||||
pub async fn close(&self, code: Option<u16>) -> LuaResult<()> {
|
||||
if self.close_code_exists.load(Ordering::Relaxed) {
|
||||
return Err(LuaError::runtime("Socket has already been closed"));
|
||||
}
|
||||
|
||||
self.send(WsMessage::Close(Some(WsCloseFrame {
|
||||
code: match code {
|
||||
Some(code) if (1000..=4999).contains(&code) => WsCloseCode::from(code),
|
||||
Some(code) => {
|
||||
return Err(LuaError::runtime(format!(
|
||||
"Close code must be between 1000 and 4999, got {code}"
|
||||
)))
|
||||
}
|
||||
None => WsCloseCode::Normal,
|
||||
},
|
||||
reason: "".into(),
|
||||
})))
|
||||
.await?;
|
||||
|
||||
let mut ws = self.write_stream.lock().await;
|
||||
ws.close().await.into_lua_err()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> LuaUserData for NetWebSocket<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("closeCode", |_, this| Ok(this.get_close_code()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_async_method("close", |_, this, code: Option<u16>| async move {
|
||||
this.close(code).await
|
||||
});
|
||||
|
||||
methods.add_async_method(
|
||||
"send",
|
||||
|_, this, (string, as_binary): (BString, Option<bool>)| async move {
|
||||
this.send(if as_binary.unwrap_or_default() {
|
||||
WsMessage::Binary(string.as_bytes().to_vec())
|
||||
} else {
|
||||
let s = string.to_str().into_lua_err()?;
|
||||
WsMessage::Text(s.to_string())
|
||||
})
|
||||
.await
|
||||
},
|
||||
);
|
||||
|
||||
methods.add_async_method("next", |lua, this, (): ()| async move {
|
||||
let msg = this.next().await?;
|
||||
|
||||
if let Some(WsMessage::Close(Some(frame))) = msg.as_ref() {
|
||||
this.set_close_code(frame.code.into());
|
||||
}
|
||||
|
||||
Ok(match msg {
|
||||
Some(WsMessage::Binary(bin)) => LuaValue::String(lua.create_string(bin)?),
|
||||
Some(WsMessage::Text(txt)) => LuaValue::String(lua.create_string(txt)?),
|
||||
Some(WsMessage::Close(_)) | None => LuaValue::Nil,
|
||||
// Ignore ping/pong/frame messages, they are handled by tungstenite
|
||||
msg => unreachable!("Unhandled message: {:?}", msg),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
34
crates/lune-std-process/Cargo.toml
Normal file
34
crates/lune-std-process/Cargo.toml
Normal file
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "lune-std-process"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - Process"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau"] }
|
||||
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
|
||||
|
||||
directories = "5.0"
|
||||
pin-project = "1.0"
|
||||
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
||||
|
||||
bstr = "1.9"
|
||||
bytes = "1.6.0"
|
||||
|
||||
tokio = { version = "1", default-features = false, features = [
|
||||
"io-std",
|
||||
"io-util",
|
||||
"process",
|
||||
"rt",
|
||||
"sync",
|
||||
] }
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
289
crates/lune-std-process/src/lib.rs
Normal file
289
crates/lune-std-process/src/lib.rs
Normal file
|
@ -0,0 +1,289 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
env::{
|
||||
self,
|
||||
consts::{ARCH, OS},
|
||||
},
|
||||
path::MAIN_SEPARATOR,
|
||||
process::Stdio,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
|
||||
use options::ProcessSpawnOptionsStdio;
|
||||
use os_str_bytes::RawOsString;
|
||||
use stream::{ChildProcessReader, ChildProcessWriter};
|
||||
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
|
||||
|
||||
mod options;
|
||||
mod stream;
|
||||
mod tee_writer;
|
||||
mod wait_for_child;
|
||||
|
||||
use self::options::ProcessSpawnOptions;
|
||||
use self::wait_for_child::wait_for_child;
|
||||
|
||||
use lune_utils::path::get_current_dir;
|
||||
|
||||
/**
|
||||
Creates the `process` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let mut cwd_str = get_current_dir()
|
||||
.to_str()
|
||||
.expect("cwd should be valid UTF-8")
|
||||
.to_string();
|
||||
if !cwd_str.ends_with(MAIN_SEPARATOR) {
|
||||
cwd_str.push(MAIN_SEPARATOR);
|
||||
}
|
||||
// Create constants for OS & processor architecture
|
||||
let os = lua.create_string(OS.to_lowercase())?;
|
||||
let arch = lua.create_string(ARCH.to_lowercase())?;
|
||||
let endianness = lua.create_string(if cfg!(target_endian = "big") {
|
||||
"big"
|
||||
} else {
|
||||
"little"
|
||||
})?;
|
||||
// Create readonly args array
|
||||
let args_vec = lua
|
||||
.app_data_ref::<Vec<String>>()
|
||||
.ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))?
|
||||
.clone();
|
||||
let args_tab = TableBuilder::new(lua)?
|
||||
.with_sequential_values(args_vec)?
|
||||
.build_readonly()?;
|
||||
// Create proxied table for env that gets & sets real env vars
|
||||
let env_tab = TableBuilder::new(lua)?
|
||||
.with_metatable(
|
||||
TableBuilder::new(lua)?
|
||||
.with_function(LuaMetaMethod::Index.name(), process_env_get)?
|
||||
.with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
|
||||
.with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
|
||||
.build_readonly()?,
|
||||
)?
|
||||
.build_readonly()?;
|
||||
// Create our process exit function, the scheduler crate provides this
|
||||
let fns = Functions::new(lua)?;
|
||||
let process_exit = fns.exit;
|
||||
// Create the full process table
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("os", os)?
|
||||
.with_value("arch", arch)?
|
||||
.with_value("endianness", endianness)?
|
||||
.with_value("args", args_tab)?
|
||||
.with_value("cwd", cwd_str)?
|
||||
.with_value("env", env_tab)?
|
||||
.with_value("exit", process_exit)?
|
||||
.with_async_function("exec", process_exec)?
|
||||
.with_function("create", process_create)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
fn process_env_get<'lua>(
|
||||
lua: &'lua Lua,
|
||||
(_, key): (LuaValue<'lua>, String),
|
||||
) -> LuaResult<LuaValue<'lua>> {
|
||||
match env::var_os(key) {
|
||||
Some(value) => {
|
||||
let raw_value = RawOsString::new(value);
|
||||
Ok(LuaValue::String(
|
||||
lua.create_string(raw_value.to_raw_bytes())?,
|
||||
))
|
||||
}
|
||||
None => Ok(LuaValue::Nil),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_env_set<'lua>(
|
||||
_: &'lua Lua,
|
||||
(_, key, value): (LuaValue<'lua>, String, Option<String>),
|
||||
) -> LuaResult<()> {
|
||||
// Make sure key is valid, otherwise set_var will panic
|
||||
if key.is_empty() {
|
||||
Err(LuaError::RuntimeError("Key must not be empty".to_string()))
|
||||
} else if key.contains('=') {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Key must not contain the equals character '='".to_string(),
|
||||
))
|
||||
} else if key.contains('\0') {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Key must not contain the NUL character".to_string(),
|
||||
))
|
||||
} else if let Some(value) = value {
|
||||
// Make sure value is valid, otherwise set_var will panic
|
||||
if value.contains('\0') {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Value must not contain the NUL character".to_string(),
|
||||
))
|
||||
} else {
|
||||
env::set_var(&key, &value);
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
env::remove_var(&key);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn process_env_iter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
(_, ()): (LuaValue<'lua>, ()),
|
||||
) -> LuaResult<LuaFunction<'lua>> {
|
||||
let mut vars = env::vars_os().collect::<Vec<_>>().into_iter();
|
||||
lua.create_function_mut(move |lua, (): ()| match vars.next() {
|
||||
Some((key, value)) => {
|
||||
let raw_key = RawOsString::new(key);
|
||||
let raw_value = RawOsString::new(value);
|
||||
Ok((
|
||||
LuaValue::String(lua.create_string(raw_key.to_raw_bytes())?),
|
||||
LuaValue::String(lua.create_string(raw_value.to_raw_bytes())?),
|
||||
))
|
||||
}
|
||||
None => Ok((LuaValue::Nil, LuaValue::Nil)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_exec(
|
||||
lua: &Lua,
|
||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||
) -> LuaResult<LuaTable> {
|
||||
let res = lua
|
||||
.spawn(async move {
|
||||
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
|
||||
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
|
||||
})
|
||||
.await?;
|
||||
|
||||
/*
|
||||
NOTE: If an exit code was not given by the child process,
|
||||
we default to 1 if it yielded any error output, otherwise 0
|
||||
|
||||
An exit code may be missing if the process was terminated by
|
||||
some external signal, which is the only time we use this default
|
||||
*/
|
||||
let code = res
|
||||
.status
|
||||
.code()
|
||||
.unwrap_or(i32::from(!res.stderr.is_empty()));
|
||||
|
||||
// Construct and return a readonly lua table with results
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("ok", code == 0)?
|
||||
.with_value("code", code)?
|
||||
.with_value("stdout", lua.create_string(&res.stdout)?)?
|
||||
.with_value("stderr", lua.create_string(&res.stderr)?)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
#[allow(clippy::await_holding_refcell_ref)]
|
||||
fn process_create(
|
||||
lua: &Lua,
|
||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||
) -> LuaResult<LuaTable> {
|
||||
// We do not want the user to provide stdio options for process.create,
|
||||
// so we reset the options, regardless of what the user provides us
|
||||
let mut spawn_options = options.clone();
|
||||
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
|
||||
|
||||
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
|
||||
let code_rx_rc = Rc::new(RefCell::new(code_rx));
|
||||
|
||||
let child = spawn_command(program, args, spawn_options)?;
|
||||
|
||||
let child_arc = Arc::new(RwLock::new(child));
|
||||
|
||||
let child_arc_clone = Arc::clone(&child_arc);
|
||||
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
|
||||
|
||||
let stdin = child_lock.stdin.take().unwrap();
|
||||
let stdout = child_lock.stdout.take().unwrap();
|
||||
let stderr = child_lock.stderr.take().unwrap();
|
||||
|
||||
let child_arc_inner = Arc::clone(&child_arc);
|
||||
|
||||
// Spawn a background task to wait for the child to exit and send the exit code
|
||||
let status_handle = tokio::spawn(async move {
|
||||
let res = child_arc_inner.write().await.wait().await;
|
||||
|
||||
if let Ok(output) = res {
|
||||
let code = output.code().unwrap_or_default();
|
||||
|
||||
code_tx
|
||||
.send(code)
|
||||
.expect("ExitCode receiver was unexpectedly dropped");
|
||||
}
|
||||
});
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("stdout", ChildProcessReader(stdout))?
|
||||
.with_value("stderr", ChildProcessReader(stderr))?
|
||||
.with_value("stdin", ChildProcessWriter(stdin))?
|
||||
.with_async_function("kill", move |_, ()| {
|
||||
// First, stop the status task so the RwLock is dropped
|
||||
status_handle.abort();
|
||||
let child_arc_clone = Arc::clone(&child_arc);
|
||||
|
||||
// Then get another RwLock to write to the child process and kill it
|
||||
async move { Ok(child_arc_clone.write().await.kill().await?) }
|
||||
})?
|
||||
.with_async_function("status", move |lua, ()| {
|
||||
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
|
||||
async move {
|
||||
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
|
||||
// the receiver gets suddenly dropped
|
||||
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("code", code)?
|
||||
.with_value("ok", code == 0)?
|
||||
.build_readonly()
|
||||
}
|
||||
})?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
async fn spawn_command_with_stdin(
|
||||
program: String,
|
||||
args: Option<Vec<String>>,
|
||||
mut options: ProcessSpawnOptions,
|
||||
) -> LuaResult<Child> {
|
||||
let stdin = options.stdio.stdin.take();
|
||||
|
||||
let mut child = spawn_command(program, args, options)?;
|
||||
|
||||
if let Some(stdin) = stdin {
|
||||
let mut child_stdin = child.stdin.take().unwrap();
|
||||
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
||||
}
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
fn spawn_command(
|
||||
program: String,
|
||||
args: Option<Vec<String>>,
|
||||
options: ProcessSpawnOptions,
|
||||
) -> LuaResult<Child> {
|
||||
let stdout = options.stdio.stdout;
|
||||
let stderr = options.stdio.stderr;
|
||||
|
||||
let child = options
|
||||
.into_command(program, args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(stdout.as_stdio())
|
||||
.stderr(stderr.as_stdio())
|
||||
.spawn()?;
|
||||
|
||||
Ok(child)
|
||||
}
|
80
crates/lune-std-process/src/options/kind.rs
Normal file
80
crates/lune-std-process/src/options/kind.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use std::{fmt, process::Stdio, str::FromStr};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum ProcessSpawnOptionsStdioKind {
|
||||
// TODO: We need better more obvious names
|
||||
// for these, but that is a breaking change
|
||||
#[default]
|
||||
Default,
|
||||
Forward,
|
||||
Inherit,
|
||||
None,
|
||||
}
|
||||
|
||||
impl ProcessSpawnOptionsStdioKind {
|
||||
pub fn all() -> &'static [Self] {
|
||||
&[Self::Default, Self::Forward, Self::Inherit, Self::None]
|
||||
}
|
||||
|
||||
pub fn as_stdio(self) -> Stdio {
|
||||
match self {
|
||||
Self::None => Stdio::null(),
|
||||
Self::Forward => Stdio::inherit(),
|
||||
_ => Stdio::piped(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProcessSpawnOptionsStdioKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match *self {
|
||||
Self::Default => "default",
|
||||
Self::Forward => "forward",
|
||||
Self::Inherit => "inherit",
|
||||
Self::None => "none",
|
||||
};
|
||||
f.write_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ProcessSpawnOptionsStdioKind {
|
||||
type Err = LuaError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s.trim().to_ascii_lowercase().as_str() {
|
||||
"default" => Self::Default,
|
||||
"forward" => Self::Forward,
|
||||
"inherit" => Self::Inherit,
|
||||
"none" => Self::None,
|
||||
_ => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Invalid spawn options stdio kind - got '{}', expected one of {}",
|
||||
s,
|
||||
ProcessSpawnOptionsStdioKind::all()
|
||||
.iter()
|
||||
.map(|k| format!("'{k}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for ProcessSpawnOptionsStdioKind {
|
||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
||||
match value {
|
||||
LuaValue::Nil => Ok(Self::default()),
|
||||
LuaValue::String(s) => s.to_str()?.parse(),
|
||||
_ => Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "ProcessSpawnOptionsStdioKind",
|
||||
message: Some(format!(
|
||||
"Invalid spawn options stdio kind - expected string, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,13 +8,18 @@ use directories::UserDirs;
|
|||
use mlua::prelude::*;
|
||||
use tokio::process::Command;
|
||||
|
||||
mod kind;
|
||||
mod stdio;
|
||||
|
||||
pub(super) use kind::*;
|
||||
pub(super) use stdio::*;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProcessSpawnOptions {
|
||||
pub(crate) cwd: Option<PathBuf>,
|
||||
pub(crate) envs: HashMap<String, String>,
|
||||
pub(crate) shell: Option<String>,
|
||||
pub(crate) inherit_stdio: bool,
|
||||
pub(crate) stdin: Option<Vec<u8>>,
|
||||
pub(super) struct ProcessSpawnOptions {
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub envs: HashMap<String, String>,
|
||||
pub shell: Option<String>,
|
||||
pub stdio: ProcessSpawnOptionsStdio,
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
|
||||
|
@ -51,7 +56,7 @@ impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
|
|||
"Invalid value for option 'cwd' - failed to get home directory",
|
||||
)
|
||||
})?;
|
||||
cwd = user_dirs.home_dir().join(stripped)
|
||||
cwd = user_dirs.home_dir().join(stripped);
|
||||
}
|
||||
if !cwd.exists() {
|
||||
return Err(LuaError::runtime(
|
||||
|
@ -112,34 +117,14 @@ impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
|
|||
}
|
||||
|
||||
/*
|
||||
If we got options for stdio handling, make sure its one of the constant values
|
||||
*/
|
||||
match value.get("stdio")? {
|
||||
LuaValue::Nil => {}
|
||||
LuaValue::String(s) => match s.to_str()? {
|
||||
"inherit" => this.inherit_stdio = true,
|
||||
"default" => this.inherit_stdio = false,
|
||||
_ => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'",
|
||||
s.to_string_lossy()
|
||||
)))
|
||||
}
|
||||
},
|
||||
value => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Invalid type for option 'stdio' - expected 'string', got '{}'",
|
||||
value.type_name()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
If we have stdin contents, we need to pass those to the child process
|
||||
If we got options for stdio handling, parse those as well - note that
|
||||
we accept a separate "stdin" value here for compatibility with older
|
||||
scripts, but the user should preferrably pass it in the stdio table
|
||||
*/
|
||||
this.stdio = value.get("stdio")?;
|
||||
match value.get("stdin")? {
|
||||
LuaValue::Nil => {}
|
||||
LuaValue::String(s) => this.stdin = Some(s.as_bytes().to_vec()),
|
||||
LuaValue::String(s) => this.stdio.stdin = Some(s.as_bytes().to_vec()),
|
||||
value => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Invalid type for option 'stdin' - expected 'string', got '{}'",
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue