Compare commits
No commits in common. "main" and "v0.7.6" have entirely different histories.
7
.gitattributes
vendored
|
@ -1,8 +1,9 @@
|
|||
* 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
|
||||
|
||||
# Ensure all txt files within tests use LF
|
||||
tests/**/*.txt eol=lf
|
||||
|
|
93
.github/workflows/ci.yaml
vendored
|
@ -2,104 +2,33 @@ name: CI
|
|||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
name: Check formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: Install Tooling
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Check Formatting
|
||||
run: just fmt-check
|
||||
|
||||
analyze:
|
||||
needs: ["fmt"]
|
||||
name: Analyze and lint Luau files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Tooling
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Analyze
|
||||
run: just analyze
|
||||
|
||||
ci:
|
||||
needs: ["fmt"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Windows x86_64
|
||||
runner-os: windows-latest
|
||||
cargo-target: x86_64-pc-windows-msvc
|
||||
name: CI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
- name: Linux x86_64
|
||||
runner-os: ubuntu-latest
|
||||
cargo-target: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: macOS x86_64
|
||||
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:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
targets: ${{ matrix.cargo-target }}
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo binstall cargo-nextest
|
||||
- name: Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --workspace \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
run: cargo build --locked
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
cargo clippy --workspace \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
cargo nextest run --no-fail-fast \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
run: cargo test --lib
|
||||
|
|
24
.github/workflows/publish.yaml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- 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
|
135
.github/workflows/release.yaml
vendored
|
@ -6,9 +6,8 @@ on:
|
|||
permissions:
|
||||
contents: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
CARGO_TARGET_DIR: output
|
||||
|
||||
jobs:
|
||||
init:
|
||||
|
@ -18,37 +17,18 @@ jobs:
|
|||
version: ${{ steps.get_version.outputs.value }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get version from manifest
|
||||
uses: SebRollen/toml-action@v1.2.0
|
||||
uses: SebRollen/toml-action@9062fbef52816d61278d24ce53c8070440e1e8dd
|
||||
id: get_version
|
||||
with:
|
||||
file: crates/lune/Cargo.toml
|
||||
file: 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"] # , "dry-run"]
|
||||
needs: ["init"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Windows x86_64
|
||||
|
@ -80,17 +60,14 @@ jobs:
|
|||
runs-on: ${{ matrix.runner-os }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v2
|
||||
|
||||
- name: Install build tooling (aarch64-unknown-linux-gnu)
|
||||
- name: Install tooling (aarch64-unknown-linux-gnu)
|
||||
if: matrix.cargo-target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
|
@ -98,61 +75,79 @@ jobs:
|
|||
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
||||
|
||||
- name: Build binary
|
||||
run: just build --locked --release --target ${{ matrix.cargo-target }}
|
||||
shell: bash
|
||||
run: |
|
||||
cargo build \
|
||||
--locked --release --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Create release archive
|
||||
run: just zip-release ${{ matrix.cargo-target }}
|
||||
- name: Create binary archive
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p staging
|
||||
if [ "${{ matrix.runner-os }}" = "windows-latest" ]; then
|
||||
cp "output/${{ matrix.cargo-target }}/release/lune.exe" staging/
|
||||
cd staging
|
||||
7z a ../release.zip *
|
||||
else
|
||||
cp "output/${{ matrix.cargo-target }}/release/lune" staging/
|
||||
cd staging
|
||||
chmod +x lune
|
||||
zip ../release.zip *
|
||||
fi
|
||||
|
||||
- name: Upload release artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
path: release.zip
|
||||
|
||||
release-github:
|
||||
name: Release (GitHub)
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: ["init", "build"] # , "dry-run", "build"]
|
||||
needs: ["init", "build"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v2
|
||||
|
||||
- name: Download releases
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: ./releases
|
||||
path: ./binaries
|
||||
|
||||
- name: Unpack releases
|
||||
run: just unpack-releases "./releases"
|
||||
- name: Discover binaries
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./binaries
|
||||
echo ""
|
||||
echo "Binaries dir:"
|
||||
ls -lhrt
|
||||
echo ""
|
||||
echo "Searching for zipped releases..."
|
||||
for DIR in * ; do
|
||||
if [ -d "$DIR" ]; then
|
||||
cd "$DIR"
|
||||
for FILE in * ; do
|
||||
if [ ! -d "$FILE" ]; then
|
||||
if [ "$FILE" = "release.zip" ]; then
|
||||
echo "Found zipped release '$DIR'"
|
||||
mv "$FILE" "../$DIR.zip"
|
||||
rm -rf "../$DIR/"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "Binaries dir:"
|
||||
ls -lhrt
|
||||
cd ..
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
name: ${{ needs.init.outputs.version }}
|
||||
tag_name: v${{ needs.init.outputs.version }}
|
||||
fail_on_unmatched_files: true
|
||||
files: ./releases/*.zip
|
||||
files: ./binaries/*.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 }}
|
||||
|
|
12
.gitignore
vendored
|
@ -6,14 +6,7 @@
|
|||
# Autogenerated dirs
|
||||
|
||||
/bin
|
||||
/out
|
||||
/target
|
||||
/staging
|
||||
|
||||
/**/bin
|
||||
/**/out
|
||||
/**/target
|
||||
/**/staging
|
||||
|
||||
# Autogenerated files
|
||||
|
||||
|
@ -21,12 +14,7 @@ lune.yml
|
|||
luneDocs.json
|
||||
luneTypes.d.luau
|
||||
|
||||
# Dirs generated by runtime or build scripts
|
||||
|
||||
/types
|
||||
|
||||
# Files generated by runtime or build scripts
|
||||
|
||||
scripts/brick_color.rs
|
||||
scripts/font_enum_map.rs
|
||||
scripts/physical_properties_enum_map.rs
|
||||
|
|
133
.justfile
|
@ -1,130 +1,11 @@
|
|||
EXT := if os() == "windows" { ".exe" } else { "" }
|
||||
CWD := invocation_directory()
|
||||
BIN_NAME := "lune"
|
||||
|
||||
# Default hidden recipe for listing other recipes + cwd
|
||||
[no-cd]
|
||||
[no-exit-message]
|
||||
[private]
|
||||
default:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "Current directory:\n {{CWD}}\n"
|
||||
just --list
|
||||
|
||||
# Builds the Lune CLI binary
|
||||
[no-exit-message]
|
||||
build *ARGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo build --bin {{BIN_NAME}} {{ARGS}}
|
||||
# Run an individual test using the Lune CLI
|
||||
run-test TEST_NAME:
|
||||
cargo run -- "tests/{{TEST_NAME}}"
|
||||
|
||||
# Run an individual file using the Lune CLI
|
||||
[no-exit-message]
|
||||
run FILE_PATH:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo run --bin {{BIN_NAME}} -- run "{{FILE_PATH}}"
|
||||
run-file FILE_NAME:
|
||||
cargo run -- "{{FILE_NAME}}"
|
||||
|
||||
# Run tests for the Lune library
|
||||
[no-exit-message]
|
||||
test *ARGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo test --lib -- {{ARGS}}
|
||||
|
||||
# Run tests for the Lune binary
|
||||
[no-exit-message]
|
||||
test-bin *ARGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo test --bin {{BIN_NAME}} -- {{ARGS}}
|
||||
|
||||
# Apply formatting for all Rust & Luau files
|
||||
[no-exit-message]
|
||||
fmt:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
stylua .lune crates scripts tests \
|
||||
--glob "tests/**/*.luau" \
|
||||
--glob "!tests/roblox/rbx-test-files/**"
|
||||
cargo fmt
|
||||
|
||||
# Check formatting for all Rust & Luau files
|
||||
[no-exit-message]
|
||||
fmt-check:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
stylua .lune crates scripts tests \
|
||||
--glob "tests/**/*.luau" \
|
||||
--glob "!tests/roblox/rbx-test-files/**"
|
||||
cargo fmt --check
|
||||
|
||||
# Analyze and lint Luau files using luau-lsp
|
||||
[no-exit-message]
|
||||
analyze:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
lune run scripts/analyze_copy_typedefs
|
||||
luau-lsp analyze \
|
||||
--settings=".vscode/settings.json" \
|
||||
--ignore="tests/roblox/rbx-test-files/**" \
|
||||
.lune crates scripts tests
|
||||
|
||||
# Zips up the built binary into a single zip file
|
||||
[no-exit-message]
|
||||
zip-release TARGET_TRIPLE:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
rm -rf staging
|
||||
rm -rf release.zip
|
||||
mkdir -p staging
|
||||
cp "target/{{TARGET_TRIPLE}}/release/{{BIN_NAME}}{{EXT}}" staging/
|
||||
cd staging
|
||||
if [ "{{os_family()}}" = "windows" ]; then
|
||||
7z a ../release.zip *
|
||||
else
|
||||
chmod +x {{BIN_NAME}}
|
||||
zip ../release.zip *
|
||||
fi
|
||||
cd "{{CWD}}"
|
||||
rm -rf staging
|
||||
|
||||
# Used in GitHub workflow to move per-matrix release zips
|
||||
[no-exit-message]
|
||||
[private]
|
||||
unpack-releases RELEASES_DIR:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
if [ ! -d "{{RELEASES_DIR}}" ]; then
|
||||
echo "Releases directory is missing"
|
||||
exit 1
|
||||
fi
|
||||
#
|
||||
cd "{{RELEASES_DIR}}"
|
||||
echo ""
|
||||
echo "Releases dir:"
|
||||
ls -lhrt
|
||||
echo ""
|
||||
echo "Searching for zipped releases..."
|
||||
#
|
||||
for DIR in * ; do
|
||||
if [ -d "$DIR" ]; then
|
||||
cd "$DIR"
|
||||
for FILE in * ; do
|
||||
if [ ! -d "$FILE" ]; then
|
||||
if [ "$FILE" = "release.zip" ]; then
|
||||
echo "Found zipped release '$DIR'"
|
||||
mv "$FILE" "../$DIR.zip"
|
||||
rm -rf "../$DIR/"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
#
|
||||
echo ""
|
||||
echo "Releases dir:"
|
||||
ls -lhrt
|
||||
test:
|
||||
cargo test --lib
|
||||
|
|
7
.luaurc
|
@ -7,10 +7,5 @@
|
|||
"typeErrors": true,
|
||||
"globals": [
|
||||
"warn"
|
||||
],
|
||||
"aliases": {
|
||||
"lune": "./types/",
|
||||
"tests": "./tests",
|
||||
"require-tests": "./tests/require/tests"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
local fs = require("@lune/fs")
|
||||
local net = require("@lune/net")
|
||||
local process = require("@lune/process")
|
||||
local serde = require("@lune/serde")
|
||||
local stdio = require("@lune/stdio")
|
||||
local task = require("@lune/task")
|
||||
|
||||
|
@ -130,7 +129,7 @@ end
|
|||
]]
|
||||
|
||||
print("Sending 4 pings to google 🌏")
|
||||
local result = process.exec("ping", {
|
||||
local result = process.spawn("ping", {
|
||||
"google.com",
|
||||
"-c 4",
|
||||
})
|
||||
|
@ -146,8 +145,10 @@ local result = process.exec("ping", {
|
|||
|
||||
if result.ok then
|
||||
assert(#result.stdout > 0, "Result output was empty")
|
||||
local min, avg, max, stddev =
|
||||
string.match(result.stdout, "min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms")
|
||||
local min, avg, max, stddev = string.match(
|
||||
result.stdout,
|
||||
"min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms"
|
||||
)
|
||||
print(string.format("Minimum ping time: %.3fms", assert(tonumber(min))))
|
||||
print(string.format("Maximum ping time: %.3fms", assert(tonumber(max))))
|
||||
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
|
||||
|
@ -170,8 +171,8 @@ local apiResult = net.request({
|
|||
method = "PATCH",
|
||||
headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
} :: { [string]: string },
|
||||
body = serde.encode("json", {
|
||||
},
|
||||
body = net.jsonEncode({
|
||||
title = "foo",
|
||||
body = "bar",
|
||||
}),
|
||||
|
@ -191,7 +192,7 @@ type ApiResponse = {
|
|||
userId: number,
|
||||
}
|
||||
|
||||
local apiResponse: ApiResponse = serde.decode("json", apiResult.body)
|
||||
local apiResponse: ApiResponse = net.jsonDecode(apiResult.body)
|
||||
assert(apiResponse.title == "foo", "Invalid json response")
|
||||
assert(apiResponse.body == "bar", "Invalid json response")
|
||||
print("Got valid JSON response with changes applied")
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
local net = require("@lune/net")
|
||||
local process = require("@lune/process")
|
||||
local task = require("@lune/task")
|
||||
|
||||
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
|
||||
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
|
||||
|
@ -10,10 +11,6 @@ local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
|
|||
|
||||
-- Create our responder functions
|
||||
|
||||
local function root(_request: net.ServeRequest): string
|
||||
return `Hello from Lune server!`
|
||||
end
|
||||
|
||||
local function pong(request: net.ServeRequest): string
|
||||
return `Pong!\n{request.path}\n{request.body}`
|
||||
end
|
||||
|
@ -32,12 +29,10 @@ local function notFound(_request: net.ServeRequest): net.ServeResponse
|
|||
}
|
||||
end
|
||||
|
||||
-- Run the server on the port forever
|
||||
-- Run the server on port 8080
|
||||
|
||||
net.serve(PORT, function(request)
|
||||
if request.path == "/" then
|
||||
return root(request)
|
||||
elseif string.sub(request.path, 1, 5) == "/ping" then
|
||||
local handle = net.serve(PORT, function(request)
|
||||
if string.sub(request.path, 1, 5) == "/ping" then
|
||||
return pong(request)
|
||||
elseif string.sub(request.path, 1, 7) == "/teapot" then
|
||||
return teapot(request)
|
||||
|
@ -47,4 +42,12 @@ net.serve(PORT, function(request)
|
|||
end)
|
||||
|
||||
print(`Listening on port {PORT} 🚀`)
|
||||
print("Press Ctrl+C to stop")
|
||||
|
||||
-- Exit our example after a small delay, if you copy this
|
||||
-- example just remove this part to keep the server running
|
||||
|
||||
task.delay(2, function()
|
||||
print("Shutting down...")
|
||||
task.wait(1)
|
||||
handle.stop()
|
||||
end)
|
||||
|
|
|
@ -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.")
|
||||
|
|
10
.vscode/settings.json
vendored
|
@ -1,17 +1,21 @@
|
|||
{
|
||||
// Luau - disable Roblox features, enable Lune typedefs & requires
|
||||
"luau-lsp.sourcemap.enabled": false,
|
||||
"luau-lsp.types.roblox": false,
|
||||
"luau-lsp.require.mode": "relativeToFile",
|
||||
"luau-lsp.require.directoryAliases": {
|
||||
"@lune/": "./types/",
|
||||
"@tests/": "./tests/",
|
||||
"@require-tests/": "./tests/require/tests/"
|
||||
"@lune/": "~/.lune/.typedefs/0.7.5/"
|
||||
},
|
||||
// Luau - ignore type defs file in docs dir and dev scripts we use
|
||||
"luau-lsp.ignoreGlobs": [
|
||||
"docs/*.d.luau",
|
||||
"packages/lib-roblox/scripts/*.luau",
|
||||
"tests/roblox/rbx-test-files/**/*.lua",
|
||||
"tests/roblox/rbx-test-files/**/*.luau"
|
||||
],
|
||||
// Rust
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
// Formatting
|
||||
"editor.formatOnSave": true,
|
||||
"stylua.searchParentDirectories": true,
|
||||
"prettier.tabWidth": 2,
|
||||
|
|
673
CHANGELOG.md
|
@ -8,667 +8,6 @@ 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).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for non-UTF8 strings in arguments to `process.exec` and `process.spawn`
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved cross-platform compatibility and correctness for values in `process.args` and `process.env`, especially on Windows
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed various crashes during require that had the error `cannot mutably borrow app data container`
|
||||
|
||||
## `0.9.2` - April 30th, 2025
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved performance of `net.request` and `net.serve` when handling large request bodies
|
||||
- Improved performance and memory usage of `task.spawn`, `task.defer`, and `task.delay`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed accidental breakage of `net.request` in version `0.9.1`
|
||||
|
||||
## `0.9.1` - April 29th, 2025
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for automatic decompression of HTTP requests in `net.serve` ([#310])
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `net.serve` no longer serving requests if the returned `ServeHandle` is discarded ([#310])
|
||||
- Fixed `net.serve` having various performance issues ([#310])
|
||||
- Fixed Lune still running after cancelling a task such as `task.delay(5, ...)` and all tasks having completed
|
||||
|
||||
[#310]: https://github.com/lune-org/lune/pull/310
|
||||
|
||||
## `0.9.0` - April 25th, 2025
|
||||
|
||||
The next major version of Lune has finally been released!
|
||||
|
||||
This release has been a long time coming, and many breaking changes have been made.
|
||||
If you are an existing Lune user upgrading to this version, you will **most likely** be affected.
|
||||
The full list of breaking changes can be found on below.
|
||||
|
||||
### Breaking changes & additions
|
||||
|
||||
- The behavior of `require` has changed, according to the latest Luau RFCs and specifications.
|
||||
|
||||
For the full details, feel free to read documentation [here](https://github.com/luau-lang/rfcs), otherwise, the most notable changes here are:
|
||||
|
||||
- Paths passed to require must start with either `./`, `../` or `@` - require statements such as `require("foo")` **will now error** and must be changed to `require("./foo")`.
|
||||
- The behavior of require from within `init.luau` and `init.lua` files has changed - previously `require("./foo")` would resolve
|
||||
to the file or directory `foo` _as a **sibling** of the init file_, but will now resolve to the file or directory `foo` _which is a sibling of the **parent directory** of the init file_.
|
||||
To require files inside of the same directory as the init file, the new `@self` alias must be used - like `require("@self/foo")`.
|
||||
|
||||
- The main `lune run` subcommand will no longer sink flags passed to it - `lune run --` will now *literally* pass the string `--` as the first
|
||||
value in `process.args`, and `--` is no longer necessary to be able to pass flag arguments such as `--foo` and `-b` properly to your Lune programs.
|
||||
|
||||
- Two new process spawning functions - `process.create` and `process.exec` - replace the previous `process.spawn` API. ([#211])
|
||||
|
||||
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function, with slight changes in how the `stdin` option is passed.
|
||||
|
||||
The new `process.create` function is a non-blocking process creation API and can be used to interactively
|
||||
read and write to standard input and output streams of the child process.
|
||||
|
||||
```lua
|
||||
local child = process.create("program", {
|
||||
"first-argument",
|
||||
"second-argument"
|
||||
})
|
||||
|
||||
-- Writing to stdin
|
||||
child.stdin:write("Hello from Lune!")
|
||||
|
||||
-- Reading partial data from stdout
|
||||
local data = child.stdout:read()
|
||||
print(data)
|
||||
|
||||
-- Reading the full stdout
|
||||
local full = child.stdout:readToEnd()
|
||||
print(full)
|
||||
```
|
||||
|
||||
- Removed `net.jsonEncode` and `net.jsonDecode` - please use the equivalent `serde.encode("json", ...)` and `serde.decode("json", ...)` instead
|
||||
|
||||
- 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.
|
||||
|
||||
- Various changes have been made to the Lune Rust crates:
|
||||
|
||||
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
|
||||
- All Lune standard library crates now export a `typedefs` function that returns the source code for the respective standard library module type definitions
|
||||
- All Lune crates now depend on `mlua` version `0.10` or above
|
||||
- Most Lune crates have been migrated to the `smol` and `async-*` ecosystem instead of `tokio`, with a full migration expected soon (this will not break public types)
|
||||
- The `roblox` crate re-export has been removed from the main `lune` crate - please depend on `lune-roblox` crate directly instead
|
||||
|
||||
### Added
|
||||
|
||||
- Added functions for getting Roblox Studio locations to the `roblox` standard library ([#284])
|
||||
- Added support for the `Content` datatype in the `roblox` standard library ([#305])
|
||||
- Added support for `EnumItem` instance attributes in the `roblox` standard library ([#306])
|
||||
- Added support for RFC 2822 dates in the `datetime` standard library using `fromRfc2822` ([#285]) - the `fromIsoDate`
|
||||
function has also been deprecated (not removed yet) and `fromRfc3339` should instead be preferred for any new work.
|
||||
- Added a `readLine` function to the `stdio` standard library for reading line-by-line from stdin.
|
||||
- Added a way to disable JIT by setting the `LUNE_LUAU_JIT` environment variable to `false` before running Lune.
|
||||
- Added `process.endianness` constant ([#267])
|
||||
|
||||
### Changed
|
||||
|
||||
- Documentation comments for several standard library properties have been improved ([#248], [#250])
|
||||
- Error messages no longer contain redundant or duplicate stack trace information
|
||||
- Updated to Luau version `0.663`
|
||||
- Updated to rbx-dom database version `0.670`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed deadlock in `stdio.format` calls in `__tostring` metamethods ([#288])
|
||||
- Fixed `task.wait` and `task.delay` not being guaranteed to yield when duration is set to zero or very small values
|
||||
- Fixed `__tostring` metamethods sometimes not being respected in `print` and `stdio.format` calls
|
||||
|
||||
[#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
|
||||
[#265]: https://github.com/lune-org/lune/pull/265
|
||||
[#267]: https://github.com/lune-org/lune/pull/267
|
||||
[#284]: https://github.com/lune-org/lune/pull/284
|
||||
[#285]: https://github.com/lune-org/lune/pull/285
|
||||
[#288]: https://github.com/lune-org/lune/pull/288
|
||||
[#305]: https://github.com/lune-org/lune/pull/305
|
||||
[#306]: https://github.com/lune-org/lune/pull/306
|
||||
|
||||
## `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
|
||||
|
||||
- Added a new `datetime` built-in library for handling date & time values, parsing, formatting, and more. ([#94])
|
||||
|
||||
Example usage:
|
||||
|
||||
```lua
|
||||
local DateTime = require("@lune/datetime")
|
||||
|
||||
-- Creates a DateTime for the current exact moment in time
|
||||
local now = DateTime.now()
|
||||
|
||||
-- Formats the current moment in time as an ISO 8601 string
|
||||
print(now:toIsoDate())
|
||||
|
||||
-- Formats the current moment in time, using the local
|
||||
-- time, the French locale, and the specified time string
|
||||
print(now:formatLocalTime("%A, %d %B %Y", "fr"))
|
||||
|
||||
-- Returns a specific moment in time as a DateTime instance
|
||||
local someDayInTheFuture = DateTime.fromLocalTime({
|
||||
year = 3033,
|
||||
month = 8,
|
||||
day = 26,
|
||||
hour = 16,
|
||||
minute = 56,
|
||||
second = 28,
|
||||
millisecond = 892,
|
||||
})
|
||||
|
||||
-- Extracts the current local date & time as separate values (same values as above table)
|
||||
print(now:toLocalTime())
|
||||
|
||||
-- Returns a DateTime instance from a given float, where the whole
|
||||
-- denotes the seconds and the fraction denotes the milliseconds
|
||||
-- Note that the fraction for millis here is completely optional
|
||||
DateTime.fromUnixTimestamp(871978212313.321)
|
||||
|
||||
-- Extracts the current universal (UTC) date & time as separate values
|
||||
print(now:toUniversalTime())
|
||||
```
|
||||
|
||||
- Added support for passing `stdin` in `process.spawn` ([#106])
|
||||
- Added support for setting a custom environment in load options for `luau.load`, not subject to `getfenv` / `setfenv` deoptimizations
|
||||
- Added [Terrain:GetMaterialColor](https://create.roblox.com/docs/reference/engine/classes/Terrain#GetMaterialColor) and [Terrain:SetMaterialColor](https://create.roblox.com/docs/reference/engine/classes/Terrain#SetMaterialColor) ([#93])
|
||||
- Added support for a variable number of arguments for CFrame methods ([#85])
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Luau version `0.596`.
|
||||
- Update to rbx-dom database version `0.596`.
|
||||
- `process.spawn` now uses `powershell` instead of `/bin/bash` as the shell on Windows, with `shell = true`.
|
||||
- CFrame and Vector3 values are now rounded to the nearest 2 ^ 16 decimal place to reduce floating point errors and diff noise. Note that this does not affect intermediate calculations done in lua, and only happens when a property value is set on an Instance.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the `process` built-in library not loading correctly when using Lune in REPL mode.
|
||||
- Fixed list subcommand not listing global scripts without a local `.lune` / `lune` directory present.
|
||||
- Fixed `net.serve` stopping when the returned `ServeHandle` is garbage collected.
|
||||
- Fixed missing trailing newline when using the `warn` global.
|
||||
- Fixed constructor for `CFrame` in the `roblox` built-in library not parsing the 12-arg overload correctly. ([#102])
|
||||
- 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/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
|
||||
|
||||
### Added
|
||||
|
||||
- Added a [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) to Lune. ([#83])
|
||||
|
||||
This allows you to run scripts within Lune without writing files!
|
||||
|
||||
Example usage, inside your favorite terminal:
|
||||
|
||||
```bash
|
||||
# 1. Run the Lune executable, without any arguments
|
||||
lune
|
||||
|
||||
# 2. You will be shown the current Lune version and a blank prompt arrow:
|
||||
Lune v0.7.7
|
||||
>
|
||||
|
||||
# 3. Start typing, and hit enter when you want to run your script!
|
||||
# Your script will run until completion and output things along the way.
|
||||
> print(2 + 3)
|
||||
5
|
||||
> print("Hello, lune changelog!")
|
||||
Hello, lune changelog!
|
||||
|
||||
# 4. You can also set variables that will get preserved between runs.
|
||||
# Note that local variables do not get preserved here.
|
||||
> myVariable = 123
|
||||
> print(myVariable)
|
||||
123
|
||||
|
||||
# 5. Press either of these key combinations to exit the REPL:
|
||||
# - Ctrl + D
|
||||
# - Ctrl + C
|
||||
```
|
||||
|
||||
- Added a new `luau` built-in library for manually compiling and loading Luau source code. ([#82])
|
||||
|
||||
Example usage:
|
||||
|
||||
```lua
|
||||
local luau = require("@lune/luau")
|
||||
|
||||
local bytecode = luau.compile("print('Hello, World!')")
|
||||
local callableFn = luau.load(bytecode)
|
||||
|
||||
callableFn()
|
||||
|
||||
-- Additionally, we can skip the bytecode generation and
|
||||
-- load a callable function directly from the code itself.
|
||||
local callableFn2 = luau.load("print('Hello, World!')")
|
||||
|
||||
callableFn2()
|
||||
```
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Luau version `0.591`.
|
||||
- Lune's internal task scheduler and `require` functionality has been completely rewritten. <br/>
|
||||
The new scheduler is much more stable, conforms to a larger test suite, and has a few additional benefits:
|
||||
|
||||
- Built-in libraries are now lazily loaded, meaning nothing gets allocated until the built-in library gets loaded using `require("@lune/builtin-name")`. This also improves startup times slightly.
|
||||
- Spawned processes using `process.spawn` now run on different thread(s), freeing up resources for the main thread where luau runs.
|
||||
- Serving requests using `net.serve` now processes requests on background threads, also freeing up resources. In the future, this will also allow us to offload heavy tasks such as compression/decompression to background threads.
|
||||
- Groundwork for custom / user-defined require aliases has been implemented, as well as absolute / cwd-relative requires. These will both be exposed as options and be made available to use some time in the future.
|
||||
|
||||
- When using the `serde` built-in library, keys are now sorted during serialization. This means that the output of `encode` is now completely deterministic, and wont cause issues when committing generated files to git etc.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed not being able to pass arguments to the thread using `coroutine.resume`. ([#86])
|
||||
- Fixed a large number of long-standing issues, from the task scheduler rewrite:
|
||||
|
||||
- Fixed `require` hanging indefinitely when the module being require-d uses an async function in its main body.
|
||||
- Fixed background tasks (such as `net.serve`) not keeping Lune alive even if there are no lua threads to run.
|
||||
- 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/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
|
||||
|
||||
### Changed
|
||||
|
@ -762,9 +101,9 @@ The full list of breaking changes can be found on below.
|
|||
- 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/lune-org/lune/pull/62
|
||||
[#68]: https://github.com/lune-org/lune/pull/66
|
||||
[#72]: https://github.com/lune-org/lune/pull/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
|
||||
|
||||
## `0.7.4` - July 7th, 2023
|
||||
|
||||
|
@ -782,7 +121,7 @@ The full list of breaking changes can be found on below.
|
|||
|
||||
- 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/lune-org/lune/issues/61))
|
||||
This previously caused unnecessary diffs when using git or other kinds of source control. ([Relevant issue](https://github.com/filiptibell/lune/issues/61))
|
||||
|
||||
## `0.7.2` - June 28th, 2023
|
||||
|
||||
|
@ -807,7 +146,7 @@ The full list of breaking changes can be found on below.
|
|||
- 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/lune-org/lune/pull/57
|
||||
[#57]: https://github.com/filiptibell/lune/pull/57
|
||||
|
||||
## `0.7.0` - June 12th, 2023
|
||||
|
||||
|
@ -876,7 +215,7 @@ The full list of breaking changes can be found on below.
|
|||
- 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/lune-org/lune/pull/47
|
||||
[#47]: https://github.com/filiptibell/lune/pull/47
|
||||
|
||||
## `0.6.7` - May 14th, 2023
|
||||
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
<!-- markdownlint-disable MD001 -->
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
|
||||
# Contributing
|
||||
|
||||
---
|
||||
|
||||
### Reporting a Bug
|
||||
|
||||
- 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/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.
|
||||
- A link to the relevant issue, or a `Fixes #issue` line, if an issue exists.
|
||||
|
||||
### Contributing - Features
|
||||
|
||||
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/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:
|
||||
- A clear and concise description of the new feature or changes to the feature.
|
||||
- Test files for any added or changed functionality.
|
||||
- A link to the relevant issue, or a `Closes #issue` line.
|
||||
|
||||
### Contributing - Formatting & Cosmetic Changes
|
||||
|
||||
Changes that are purely cosmetic, and do not add to the stability, functionality, or testability of Lune, will generally not be accepted unless there has been previous discussion about the changes being made.
|
||||
|
||||
### Contributing - Documentation
|
||||
|
||||
#### Documentation Site
|
||||
|
||||
Check out the [docs](https://github.com/lune-org/docs) repository and its contribution guidelines.
|
||||
|
||||
#### Type Definitions
|
||||
|
||||
If type definitions for built-in libraries need improvements:
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### Publishing a Release
|
||||
|
||||
The Lune release process is semi-automated, and takes care of most things for you. Here's how to create a new release:
|
||||
|
||||
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/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 🚀
|
||||
|
||||
---
|
||||
|
||||
If you have any questions, check out the `#lune` channel in the [Roblox OSS discord](https://discord.gg/H9WqmFAB5Y), where most of our realtime discussion takes place!
|
||||
|
||||
Thank you for contributing to Lune! 🌙
|
3379
Cargo.lock
generated
156
Cargo.toml
|
@ -1,22 +1,41 @@
|
|||
[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",
|
||||
[package]
|
||||
name = "lune"
|
||||
version = "0.7.6"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/filiptibell/lune"
|
||||
description = "A Luau script runner"
|
||||
readme = "README.md"
|
||||
keywords = ["cli", "lua", "luau", "scripts"]
|
||||
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",
|
||||
]
|
||||
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",
|
||||
]
|
||||
|
||||
# Profile for building the release binary, with the following options set:
|
||||
|
@ -34,31 +53,82 @@ opt-level = "z"
|
|||
strip = true
|
||||
lto = true
|
||||
|
||||
# Lints for all crates in the workspace
|
||||
# All of the dependencies for Lune.
|
||||
#
|
||||
# 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 }
|
||||
# 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.10"
|
||||
dunce = "1.0"
|
||||
lz4_flex = "0.11"
|
||||
pin-project = "1.0"
|
||||
os_str_bytes = "6.4"
|
||||
urlencoding = "2.1"
|
||||
|
||||
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 }
|
||||
### RUNTIME
|
||||
|
||||
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 }
|
||||
mlua = { version = "0.9.0-beta.3", features = [
|
||||
"luau",
|
||||
"luau-jit",
|
||||
"serialize",
|
||||
] }
|
||||
tokio = { version = "1.24", features = ["full"] }
|
||||
|
||||
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 }
|
||||
### 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.7", 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"] }
|
||||
|
||||
### 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.3", features = ["glob"] }
|
||||
regex = { optional = true, version = "1.7", default-features = false, features = [
|
||||
"std",
|
||||
"unicode-perl",
|
||||
] }
|
||||
|
||||
### 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.1" }
|
||||
rbx_dom_weak = { optional = true, version = "2.5.0" }
|
||||
rbx_reflection = { optional = true, version = "4.3.0" }
|
||||
rbx_reflection_database = { optional = true, version = "0.2.7" }
|
||||
rbx_xml = { optional = true, version = "0.13.1" }
|
||||
|
|
35
README.md
|
@ -1,47 +1,44 @@
|
|||
<!-- markdownlint-disable MD033 -->
|
||||
<!-- markdownlint-disable MD041 -->
|
||||
|
||||
<img align="right" width="250" src="assets/logo/tilt_svg.svg" alt="Lune logo" />
|
||||
|
||||
<h1 align="center">Lune</h1>
|
||||
|
||||
<div align="center">
|
||||
<h1> Lune 🌙 </h1>
|
||||
<div>
|
||||
<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/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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
A standalone [Luau](https://luau-lang.org) runtime.
|
||||
A standalone [Luau](https://luau-lang.org) script runtime.
|
||||
|
||||
Write and run programs, similar to runtimes for other languages such as [Node](https://nodejs.org), [Deno](https://deno.land), [Bun](https://bun.sh), or [Luvit](https://luvit.io) for vanilla Lua.
|
||||
Write and run scripts, similar to runtimes for other languages such as [Node](https://nodejs.org) / [Deno](https://deno.land), or [Luvit](https://luvit.io) for vanilla Lua.
|
||||
|
||||
Lune provides fully asynchronous APIs wherever possible, and is built in Rust 🦀 for speed, safety and correctness.
|
||||
Lune provides fully asynchronous APIs wherever possible, and is built in Rust 🦀 for optimal safety and correctness.
|
||||
|
||||
## 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 zipped) executable
|
||||
- 🌙 A 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 (~4mb) 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
|
||||
- 🏡 A familiar scripting 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
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Making programs short and terse - proper autocomplete / intellisense make using Lune just as quick, and readability is important
|
||||
- Running full Roblox games outside of Roblox - there is some compatibility, but Lune is meant for different purposes
|
||||
- Making scripts short and terse - proper autocomplete / intellisense make scripting using Lune just as quick, and readability is important
|
||||
- Running full Roblox game scripts outside of Roblox - there is some compatibility, but Lune is meant for different purposes
|
||||
|
||||
## Where do I start?
|
||||
|
||||
|
|
4
aftman.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
[tools]
|
||||
luau-lsp = "JohnnyMorganz/luau-lsp@1.20.0"
|
||||
selene = "Kampfkarren/selene@0.24.0"
|
||||
stylua = "JohnnyMorganz/StyLua@0.17.0"
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 148 KiB |
|
@ -1,69 +0,0 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_i_6_19)">
|
||||
<g clip-path="url(#clip0_6_19)">
|
||||
<rect x="2" y="2" width="16" height="16" rx="1" fill="url(#paint0_linear_6_19)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.875 0.125L18.125 0.125V1.875H19.875V0.125ZM19.875 2.125H18.125V3.875H19.875V2.125ZM19.875 4.125H18.125V5.875H19.875V4.125ZM19.875 6.125H18.125V7.875H19.875V6.125ZM19.875 8.125H18.125V9.875H19.875V8.125ZM19.875 10.125H18.125V11.875H19.875V10.125ZM19.875 12.125H18.125V13.875H19.875V12.125ZM19.875 14.125H18.125V15.875H19.875V14.125ZM19.875 16.125H18.125V17.875H19.875V16.125ZM19.875 18.125H18.125V19.875H19.875V18.125ZM20 20V20.125H4.76837e-07V20H-0.125L-0.125 -5.21541e-07L4.76837e-07 -5.1409e-07V-0.125L20 -0.125V2.92063e-06H20.125V20H20ZM0.125 0.125L0.125 1.875H1.87499L1.87499 0.125L0.125 0.125ZM2.12499 0.125V1.875H3.87499V0.125L2.12499 0.125ZM4.12499 0.125V1.875H5.87499V0.125L4.12499 0.125ZM6.12499 0.125V1.875H7.87499V0.125L6.12499 0.125ZM8.12499 0.125V1.875H9.87499V0.125L8.12499 0.125ZM10.125 0.125V1.875H11.875V0.125L10.125 0.125ZM12.125 0.125V1.875H13.875V0.125L12.125 0.125ZM14.125 0.125V1.875H15.875V0.125L14.125 0.125ZM16.125 0.125V1.875H17.875V0.125L16.125 0.125ZM17.875 2.125H16.125V3.875H17.875V2.125ZM17.875 4.125H16.125V5.875H17.875V4.125ZM17.875 6.125H16.125V7.875H17.875V6.125ZM17.875 8.125H16.125V9.875H17.875V8.125ZM17.875 10.125H16.125V11.875H17.875V10.125ZM17.875 12.125H16.125V13.875H17.875V12.125ZM17.875 14.125H16.125V15.875H17.875V14.125ZM17.875 16.125H16.125V17.875H17.875V16.125ZM17.875 18.125H16.125V19.875H17.875V18.125ZM15.875 19.875V18.125H14.125V19.875H15.875ZM13.875 19.875V18.125H12.125V19.875H13.875ZM11.875 19.875V18.125H10.125V19.875H11.875ZM9.87499 19.875V18.125H8.12499V19.875H9.87499ZM7.87499 19.875V18.125H6.12499V19.875H7.87499ZM5.87499 19.875V18.125H4.12499V19.875H5.87499ZM3.87499 19.875V18.125H2.12499V19.875H3.87499ZM1.87499 19.875L1.87499 18.125H0.124999L0.124999 19.875H1.87499ZM0.124999 17.875H1.87499V16.125H0.124999L0.124999 17.875ZM0.124999 15.875H1.87499L1.87499 14.125H0.124999L0.124999 15.875ZM0.124999 13.875H1.87499L1.87499 12.125H0.124999L0.124999 13.875ZM0.124999 11.875H1.87499L1.87499 10.125H0.125L0.124999 11.875ZM0.125 9.875H1.87499V8.125H0.125L0.125 9.875ZM0.125 7.875H1.87499L1.87499 6.125H0.125L0.125 7.875ZM0.125 5.875H1.87499L1.87499 4.125H0.125L0.125 5.875ZM0.125 3.875H1.87499V2.125H0.125L0.125 3.875ZM2.12499 2.125L2.12499 3.875H3.87499L3.87499 2.125H2.12499ZM4.12499 2.125V3.875H5.87499V2.125H4.12499ZM6.12499 2.125V3.875H7.87499V2.125H6.12499ZM8.12499 2.125V3.875H9.87499V2.125H8.12499ZM10.125 2.125V3.875H11.875V2.125H10.125ZM12.125 2.125V3.875H13.875V2.125H12.125ZM14.125 2.125V3.875H15.875V2.125H14.125ZM15.875 4.125H14.125V5.875H15.875V4.125ZM15.875 6.125H14.125V7.875H15.875V6.125ZM15.875 8.125H14.125V9.875H15.875V8.125ZM15.875 10.125H14.125V11.875H15.875V10.125ZM15.875 12.125H14.125V13.875H15.875V12.125ZM15.875 14.125H14.125V15.875H15.875V14.125ZM15.875 16.125H14.125V17.875H15.875V16.125ZM13.875 17.875V16.125H12.125V17.875H13.875ZM11.875 17.875V16.125H10.125V17.875H11.875ZM9.87499 17.875V16.125H8.12499V17.875H9.87499ZM7.87499 17.875V16.125H6.12499V17.875H7.87499ZM5.87499 17.875V16.125H4.12499V17.875H5.87499ZM3.87499 17.875L3.87499 16.125H2.12499L2.12499 17.875H3.87499ZM2.12499 15.875H3.87499V14.125H2.12499V15.875ZM2.12499 13.875H3.87499L3.87499 12.125H2.12499L2.12499 13.875ZM2.12499 11.875H3.87499V10.125H2.12499V11.875ZM2.12499 9.875H3.87499V8.125H2.12499V9.875ZM2.12499 7.875H3.87499L3.87499 6.125H2.12499L2.12499 7.875ZM2.12499 5.875H3.87499V4.125H2.12499V5.875ZM4.12499 4.125L4.12499 5.875H5.87499L5.87499 4.125H4.12499ZM6.12499 4.125L6.12499 5.875H7.87499L7.87499 4.125H6.12499ZM8.12499 4.125V5.875H9.87499V4.125H8.12499ZM10.125 4.125V5.875H11.875V4.125H10.125ZM12.125 4.125V5.875H13.875V4.125H12.125ZM13.875 6.125H12.125V7.875H13.875V6.125ZM13.875 8.125H12.125V9.875H13.875V8.125ZM13.875 10.125H12.125V11.875H13.875V10.125ZM13.875 12.125H12.125V13.875H13.875V12.125ZM13.875 14.125H12.125V15.875H13.875V14.125ZM11.875 15.875V14.125H10.125V15.875H11.875ZM9.87499 15.875V14.125H8.12499V15.875H9.87499ZM7.87499 15.875L7.87499 14.125H6.12499L6.12499 15.875H7.87499ZM5.87499 15.875L5.87499 14.125H4.12499L4.12499 15.875H5.87499ZM4.12499 13.875H5.87499V12.125H4.12499V13.875ZM4.12499 11.875H5.87499V10.125H4.12499V11.875ZM4.12499 9.875H5.87499V8.125H4.12499V9.875ZM4.12499 7.875H5.87499V6.125H4.12499V7.875ZM6.12499 6.125V7.875H7.87499V6.125H6.12499ZM8.12499 6.125V7.875H9.87499V6.125H8.12499ZM10.125 6.125V7.875H11.875V6.125H10.125ZM11.875 8.125H10.125V9.875H11.875V8.125ZM11.875 10.125H10.125V11.875H11.875V10.125ZM11.875 12.125H10.125V13.875H11.875V12.125ZM9.87499 13.875V12.125H8.12499V13.875H9.87499ZM7.87499 13.875V12.125H6.12499V13.875H7.87499ZM6.12499 11.875H7.87499V10.125H6.12499V11.875ZM6.12499 9.875H7.87499V8.125H6.12499V9.875ZM8.12499 8.125V9.875H9.87499V8.125H8.12499ZM9.87499 10.125H8.12499V11.875H9.87499V10.125Z" fill="url(#paint1_linear_6_19)" fill-opacity="0.05"/>
|
||||
<g filter="url(#filter1_di_6_19)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.375 6.25C14.2725 6.25 15 5.52246 15 4.625C15 4.4376 14.9683 4.2576 14.9099 4.09009C15.5446 4.31125 16 4.91494 16 5.625C16 6.52246 15.2725 7.25 14.375 7.25C13.6649 7.25 13.0613 6.79458 12.8401 6.15991C13.0076 6.21828 13.1876 6.25 13.375 6.25Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_6_19)">
|
||||
<path d="M6.625 15.54V16H4.49219V15.54H6.625ZM4.60352 11.7344V16H4.03809V11.7344H4.60352ZM9.00098 15.2676V12.8301H9.5459V16H9.02734L9.00098 15.2676ZM9.10352 14.5996L9.3291 14.5938C9.3291 14.8047 9.30664 15 9.26172 15.1797C9.21875 15.3574 9.14844 15.5117 9.05078 15.6426C8.95312 15.7734 8.8252 15.876 8.66699 15.9502C8.50879 16.0225 8.31641 16.0586 8.08984 16.0586C7.93555 16.0586 7.79395 16.0361 7.66504 15.9912C7.53809 15.9463 7.42871 15.877 7.33691 15.7832C7.24512 15.6895 7.17383 15.5674 7.12305 15.417C7.07422 15.2666 7.0498 15.0859 7.0498 14.875V12.8301H7.5918V14.8809C7.5918 15.0234 7.60742 15.1416 7.63867 15.2354C7.67188 15.3271 7.71582 15.4004 7.77051 15.4551C7.82715 15.5078 7.88965 15.5449 7.95801 15.5664C8.02832 15.5879 8.10059 15.5986 8.1748 15.5986C8.40527 15.5986 8.58789 15.5547 8.72266 15.4668C8.85742 15.377 8.9541 15.2568 9.0127 15.1064C9.07324 14.9541 9.10352 14.7852 9.10352 14.5996ZM10.9141 13.5068V16H10.3721V12.8301H10.8848L10.9141 13.5068ZM10.7852 14.2949L10.5596 14.2861C10.5615 14.0693 10.5938 13.8691 10.6562 13.6855C10.7188 13.5 10.8066 13.3389 10.9199 13.2021C11.0332 13.0654 11.168 12.96 11.3242 12.8857C11.4824 12.8096 11.6572 12.7715 11.8486 12.7715C12.0049 12.7715 12.1455 12.793 12.2705 12.8359C12.3955 12.877 12.502 12.9434 12.5898 13.0352C12.6797 13.127 12.748 13.2461 12.7949 13.3926C12.8418 13.5371 12.8652 13.7139 12.8652 13.9229V16H12.3203V13.917C12.3203 13.751 12.2959 13.6182 12.2471 13.5186C12.1982 13.417 12.127 13.3438 12.0332 13.2988C11.9395 13.252 11.8242 13.2285 11.6875 13.2285C11.5527 13.2285 11.4297 13.2568 11.3184 13.3135C11.209 13.3701 11.1143 13.4482 11.0342 13.5479C10.9561 13.6475 10.8945 13.7617 10.8496 13.8906C10.8066 14.0176 10.7852 14.1523 10.7852 14.2949ZM15.0039 16.0586C14.7832 16.0586 14.583 16.0215 14.4033 15.9473C14.2256 15.8711 14.0723 15.7646 13.9434 15.6279C13.8164 15.4912 13.7188 15.3291 13.6504 15.1416C13.582 14.9541 13.5479 14.749 13.5479 14.5264V14.4033C13.5479 14.1455 13.5859 13.916 13.6621 13.7148C13.7383 13.5117 13.8418 13.3398 13.9727 13.1992C14.1035 13.0586 14.252 12.9521 14.418 12.8799C14.584 12.8076 14.7559 12.7715 14.9336 12.7715C15.1602 12.7715 15.3555 12.8105 15.5195 12.8887C15.6855 12.9668 15.8213 13.0762 15.9268 13.2168C16.0322 13.3555 16.1104 13.5195 16.1611 13.709C16.2119 13.8965 16.2373 14.1016 16.2373 14.3242V14.5674H13.8701V14.125H15.6953V14.084C15.6875 13.9434 15.6582 13.8066 15.6074 13.6738C15.5586 13.541 15.4805 13.4316 15.373 13.3457C15.2656 13.2598 15.1191 13.2168 14.9336 13.2168C14.8105 13.2168 14.6973 13.2432 14.5938 13.2959C14.4902 13.3467 14.4014 13.4229 14.3271 13.5244C14.2529 13.626 14.1953 13.75 14.1543 13.8965C14.1133 14.043 14.0928 14.2119 14.0928 14.4033V14.5264C14.0928 14.6768 14.1133 14.8184 14.1543 14.9512C14.1973 15.082 14.2588 15.1973 14.3389 15.2969C14.4209 15.3965 14.5195 15.4746 14.6348 15.5312C14.752 15.5879 14.8848 15.6162 15.0332 15.6162C15.2246 15.6162 15.3867 15.5771 15.5195 15.499C15.6523 15.4209 15.7686 15.3164 15.8682 15.1855L16.1963 15.4463C16.1279 15.5498 16.041 15.6484 15.9355 15.7422C15.8301 15.8359 15.7002 15.9121 15.5459 15.9707C15.3936 16.0293 15.2129 16.0586 15.0039 16.0586Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_6_19" x="2" y="1.5" width="16.5" height="16.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.5" dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_6_19"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_6_19" x="10.8401" y="3.09009" width="6.15991" height="6.15991" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_19"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_19" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_19"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_6_19" x="2.03809" y="10.7344" width="15.1992" height="7.32422" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_19"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_19" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_19"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_6_19" x1="10" y1="2" x2="10" y2="18" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E168FF"/>
|
||||
<stop offset="1" stop-color="#C848E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_6_19" x1="10" y1="-0.125" x2="10" y2="20.125" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.5"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6_19">
|
||||
<rect x="2" y="2" width="16" height="16" rx="1" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 138 KiB |
|
@ -1,64 +0,0 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_i_6_67)">
|
||||
<g clip-path="url(#clip0_6_67)">
|
||||
<rect x="2" y="2" width="16" height="16" rx="1" fill="url(#paint0_linear_6_67)"/>
|
||||
<g filter="url(#filter1_di_6_67)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.375 6.25C14.2725 6.25 15 5.52246 15 4.625C15 4.43759 14.9683 4.2576 14.9099 4.09009C15.5446 4.31125 16 4.91494 16 5.625C16 6.52246 15.2725 7.25 14.375 7.25C13.6649 7.25 13.0613 6.79458 12.8401 6.15991C13.0076 6.21828 13.1876 6.25 13.375 6.25Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_6_67)">
|
||||
<path d="M6.625 15.54V16H4.49219V15.54H6.625ZM4.60352 11.7344V16H4.03809V11.7344H4.60352ZM9.00098 15.2676V12.8301H9.5459V16H9.02734L9.00098 15.2676ZM9.10352 14.5996L9.3291 14.5938C9.3291 14.8047 9.30664 15 9.26172 15.1797C9.21875 15.3574 9.14844 15.5117 9.05078 15.6426C8.95312 15.7734 8.8252 15.876 8.66699 15.9502C8.50879 16.0225 8.31641 16.0586 8.08984 16.0586C7.93555 16.0586 7.79395 16.0361 7.66504 15.9912C7.53809 15.9463 7.42871 15.877 7.33691 15.7832C7.24512 15.6895 7.17383 15.5674 7.12305 15.417C7.07422 15.2666 7.0498 15.0859 7.0498 14.875V12.8301H7.5918V14.8809C7.5918 15.0234 7.60742 15.1416 7.63867 15.2354C7.67188 15.3271 7.71582 15.4004 7.77051 15.4551C7.82715 15.5078 7.88965 15.5449 7.95801 15.5664C8.02832 15.5879 8.10059 15.5986 8.1748 15.5986C8.40527 15.5986 8.58789 15.5547 8.72266 15.4668C8.85742 15.377 8.9541 15.2568 9.0127 15.1064C9.07324 14.9541 9.10352 14.7852 9.10352 14.5996ZM10.9141 13.5068V16H10.3721V12.8301H10.8848L10.9141 13.5068ZM10.7852 14.2949L10.5596 14.2861C10.5615 14.0693 10.5938 13.8691 10.6562 13.6855C10.7188 13.5 10.8066 13.3389 10.9199 13.2021C11.0332 13.0654 11.168 12.96 11.3242 12.8857C11.4824 12.8096 11.6572 12.7715 11.8486 12.7715C12.0049 12.7715 12.1455 12.793 12.2705 12.8359C12.3955 12.877 12.502 12.9434 12.5898 13.0352C12.6797 13.127 12.748 13.2461 12.7949 13.3926C12.8418 13.5371 12.8652 13.7139 12.8652 13.9229V16H12.3203V13.917C12.3203 13.751 12.2959 13.6182 12.2471 13.5186C12.1982 13.417 12.127 13.3438 12.0332 13.2988C11.9395 13.252 11.8242 13.2285 11.6875 13.2285C11.5527 13.2285 11.4297 13.2568 11.3184 13.3135C11.209 13.3701 11.1143 13.4482 11.0342 13.5479C10.9561 13.6475 10.8945 13.7617 10.8496 13.8906C10.8066 14.0176 10.7852 14.1523 10.7852 14.2949ZM15.0039 16.0586C14.7832 16.0586 14.583 16.0215 14.4033 15.9473C14.2256 15.8711 14.0723 15.7646 13.9434 15.6279C13.8164 15.4912 13.7188 15.3291 13.6504 15.1416C13.582 14.9541 13.5479 14.749 13.5479 14.5264V14.4033C13.5479 14.1455 13.5859 13.916 13.6621 13.7148C13.7383 13.5117 13.8418 13.3398 13.9727 13.1992C14.1035 13.0586 14.252 12.9521 14.418 12.8799C14.584 12.8076 14.7559 12.7715 14.9336 12.7715C15.1602 12.7715 15.3555 12.8105 15.5195 12.8887C15.6855 12.9668 15.8213 13.0762 15.9268 13.2168C16.0322 13.3555 16.1104 13.5195 16.1611 13.709C16.2119 13.8965 16.2373 14.1016 16.2373 14.3242V14.5674H13.8701V14.125H15.6953V14.084C15.6875 13.9434 15.6582 13.8066 15.6074 13.6738C15.5586 13.541 15.4805 13.4316 15.373 13.3457C15.2656 13.2598 15.1191 13.2168 14.9336 13.2168C14.8105 13.2168 14.6973 13.2432 14.5938 13.2959C14.4902 13.3467 14.4014 13.4229 14.3271 13.5244C14.2529 13.626 14.1953 13.75 14.1543 13.8965C14.1133 14.043 14.0928 14.2119 14.0928 14.4033V14.5264C14.0928 14.6768 14.1133 14.8184 14.1543 14.9512C14.1973 15.082 14.2588 15.1973 14.3389 15.2969C14.4209 15.3965 14.5195 15.4746 14.6348 15.5312C14.752 15.5879 14.8848 15.6162 15.0332 15.6162C15.2246 15.6162 15.3867 15.5771 15.5195 15.499C15.6523 15.4209 15.7686 15.3164 15.8682 15.1855L16.1963 15.4463C16.1279 15.5498 16.041 15.6484 15.9355 15.7422C15.8301 15.8359 15.7002 15.9121 15.5459 15.9707C15.3936 16.0293 15.2129 16.0586 15.0039 16.0586Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_6_67" x="2" y="1.5" width="16.5" height="16.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.5" dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_6_67"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_6_67" x="10.8401" y="3.09009" width="6.15991" height="6.15991" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_67"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_67" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_67"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_6_67" x="2.03809" y="10.7344" width="15.1992" height="7.32422" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_67"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_67" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_67"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_6_67" x1="10" y1="2" x2="10" y2="18" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E168FF"/>
|
||||
<stop offset="1" stop-color="#C848E9"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6_67">
|
||||
<rect x="2" y="2" width="16" height="16" rx="1" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 180 KiB |
|
@ -1,74 +0,0 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_97)">
|
||||
<g filter="url(#filter0_i_6_97)">
|
||||
<g clip-path="url(#clip1_6_97)">
|
||||
<rect x="4.34315" y="0.202042" width="16" height="16" rx="1" transform="rotate(15 4.34315 0.202042)" fill="url(#paint0_linear_6_97)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.875 0.125H18.125V1.875H19.875V0.125ZM19.875 2.125H18.125V3.875H19.875V2.125ZM19.875 4.125H18.125V5.875H19.875V4.125ZM19.875 6.125H18.125V7.875H19.875V6.125ZM19.875 8.125H18.125V9.875H19.875V8.125ZM19.875 10.125H18.125V11.875H19.875V10.125ZM19.875 12.125H18.125V13.875H19.875V12.125ZM19.875 14.125H18.125V15.875H19.875V14.125ZM19.875 16.125H18.125V17.875H19.875V16.125ZM19.875 18.125H18.125V19.875H19.875V18.125ZM20 20V20.125H4.76837e-07V20H-0.125V-5.21541e-07L4.76837e-07 -5.1409e-07V-0.125H20V2.92063e-06H20.125V20H20ZM0.125 0.125L0.125 1.875H1.87499L1.87499 0.125H0.125ZM2.12499 0.125V1.875H3.87499V0.125H2.12499ZM4.12499 0.125V1.875H5.87499V0.125H4.12499ZM6.12499 0.125V1.875H7.87499V0.125H6.12499ZM8.12499 0.125V1.875H9.87499V0.125H8.12499ZM10.125 0.125V1.875H11.875V0.125H10.125ZM12.125 0.125V1.875H13.875V0.125H12.125ZM14.125 0.125V1.875H15.875V0.125H14.125ZM16.125 0.125V1.875H17.875V0.125H16.125ZM17.875 2.125H16.125V3.875H17.875V2.125ZM17.875 4.125H16.125V5.875H17.875V4.125ZM17.875 6.125H16.125V7.875H17.875V6.125ZM17.875 8.125H16.125V9.875H17.875V8.125ZM17.875 10.125H16.125V11.875H17.875V10.125ZM17.875 12.125H16.125V13.875H17.875V12.125ZM17.875 14.125H16.125V15.875H17.875V14.125ZM17.875 16.125H16.125V17.875H17.875V16.125ZM17.875 18.125H16.125V19.875H17.875V18.125ZM15.875 19.875V18.125H14.125V19.875H15.875ZM13.875 19.875V18.125H12.125V19.875H13.875ZM11.875 19.875V18.125H10.125V19.875H11.875ZM9.87499 19.875V18.125H8.12499V19.875H9.87499ZM7.87499 19.875V18.125H6.12499V19.875H7.87499ZM5.87499 19.875V18.125H4.12499V19.875H5.87499ZM3.87499 19.875V18.125H2.12499V19.875H3.87499ZM1.87499 19.875L1.87499 18.125H0.124999L0.124999 19.875H1.87499ZM0.124999 17.875H1.87499V16.125H0.124999L0.124999 17.875ZM0.124999 15.875H1.87499L1.87499 14.125H0.124999L0.124999 15.875ZM0.124999 13.875H1.87499L1.87499 12.125H0.124999L0.124999 13.875ZM0.124999 11.875H1.87499V10.125H0.125L0.124999 11.875ZM0.125 9.875H1.87499V8.125H0.125L0.125 9.875ZM0.125 7.875H1.87499L1.87499 6.125H0.125L0.125 7.875ZM0.125 5.875H1.87499L1.87499 4.125H0.125L0.125 5.875ZM0.125 3.875H1.87499V2.125H0.125L0.125 3.875ZM2.12499 2.125L2.12499 3.875H3.87499L3.87499 2.125H2.12499ZM4.12499 2.125V3.875H5.87499V2.125H4.12499ZM6.12499 2.125V3.875H7.87499V2.125H6.12499ZM8.12499 2.125V3.875H9.87499V2.125H8.12499ZM10.125 2.125V3.875H11.875V2.125H10.125ZM12.125 2.125V3.875H13.875V2.125H12.125ZM14.125 2.125V3.875H15.875V2.125H14.125ZM15.875 4.125H14.125V5.875H15.875V4.125ZM15.875 6.125H14.125V7.875H15.875V6.125ZM15.875 8.125H14.125V9.875H15.875V8.125ZM15.875 10.125H14.125V11.875H15.875V10.125ZM15.875 12.125H14.125V13.875H15.875V12.125ZM15.875 14.125H14.125V15.875H15.875V14.125ZM15.875 16.125H14.125V17.875H15.875V16.125ZM13.875 17.875V16.125H12.125V17.875H13.875ZM11.875 17.875V16.125H10.125V17.875H11.875ZM9.87499 17.875V16.125H8.12499V17.875H9.87499ZM7.87499 17.875V16.125H6.12499V17.875H7.87499ZM5.87499 17.875V16.125H4.12499V17.875H5.87499ZM3.87499 17.875L3.87499 16.125H2.12499L2.12499 17.875H3.87499ZM2.12499 15.875H3.87499V14.125H2.12499V15.875ZM2.12499 13.875H3.87499L3.87499 12.125H2.12499L2.12499 13.875ZM2.12499 11.875H3.87499V10.125H2.12499V11.875ZM2.12499 9.875H3.87499V8.125H2.12499V9.875ZM2.12499 7.875H3.87499L3.87499 6.125H2.12499L2.12499 7.875ZM2.12499 5.875H3.87499V4.125H2.12499V5.875ZM4.12499 4.125L4.12499 5.875H5.87499L5.87499 4.125H4.12499ZM6.12499 4.125L6.12499 5.875H7.87499L7.87499 4.125H6.12499ZM8.12499 4.125V5.875H9.87499V4.125H8.12499ZM10.125 4.125V5.875H11.875V4.125H10.125ZM12.125 4.125V5.875H13.875V4.125H12.125ZM13.875 6.125H12.125V7.875H13.875V6.125ZM13.875 8.125H12.125V9.875H13.875V8.125ZM13.875 10.125H12.125V11.875H13.875V10.125ZM13.875 12.125H12.125V13.875H13.875V12.125ZM13.875 14.125H12.125V15.875H13.875V14.125ZM11.875 15.875V14.125H10.125V15.875H11.875ZM9.87499 15.875V14.125H8.12499V15.875H9.87499ZM7.87499 15.875L7.87499 14.125H6.12499L6.12499 15.875H7.87499ZM5.87499 15.875L5.87499 14.125H4.12499L4.12499 15.875H5.87499ZM4.12499 13.875H5.87499V12.125H4.12499V13.875ZM4.12499 11.875H5.87499V10.125H4.12499V11.875ZM4.12499 9.875H5.87499V8.125H4.12499V9.875ZM4.12499 7.875H5.87499V6.125H4.12499V7.875ZM6.12499 6.125V7.875H7.87499V6.125H6.12499ZM8.12499 6.125V7.875H9.87499V6.125H8.12499ZM10.125 6.125V7.875H11.875V6.125H10.125ZM11.875 8.125H10.125V9.875H11.875V8.125ZM11.875 10.125H10.125V11.875H11.875V10.125ZM11.875 12.125H10.125V13.875H11.875V12.125ZM9.87499 13.875V12.125H8.12499V13.875H9.87499ZM7.87499 13.875V12.125H6.12499V13.875H7.87499ZM6.12499 11.875H7.87499V10.125H6.12499V11.875ZM6.12499 9.875H7.87499V8.125H6.12499V9.875ZM8.12499 8.125V9.875H9.87499V8.125H8.12499ZM9.87499 10.125H8.12499V11.875H9.87499V10.125Z" fill="url(#paint1_linear_6_97)" fill-opacity="0.05"/>
|
||||
<g filter="url(#filter1_di_6_97)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2306 7.25129C15.0975 7.48357 15.9885 6.96913 16.2208 6.10224C16.2693 5.92123 16.2852 5.73915 16.2722 5.56225C16.828 5.94013 17.1117 6.64112 16.9279 7.32699C16.6956 8.19387 15.8046 8.70832 14.9377 8.47604C14.2518 8.29226 13.7866 7.69611 13.7372 7.02583C13.8839 7.12556 14.0496 7.20279 14.2306 7.25129Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_6_97)">
|
||||
<path d="M5.30613 14.4778L5.18709 14.922L3.12695 14.37L3.24599 13.9257L5.30613 14.4778ZM4.33851 10.2786L3.23448 14.3988L2.68832 14.2525L3.79234 10.1322L4.33851 10.2786ZM7.67167 14.8295L8.30254 12.4751L8.82889 12.6161L8.00846 15.678L7.50757 15.5438L7.67167 14.8295ZM7.9436 14.2109L8.16301 14.2636C8.10842 14.4673 8.03617 14.6502 7.94627 14.8121C7.85877 14.9727 7.75092 15.1035 7.62272 15.2046C7.49452 15.3058 7.34441 15.3717 7.17239 15.4024C7.00087 15.4313 6.80569 15.4164 6.58685 15.3578C6.43781 15.3178 6.30685 15.2595 6.19396 15.1827C6.08296 15.1065 5.99526 15.0112 5.93085 14.8969C5.86645 14.7826 5.82918 14.6462 5.81906 14.4878C5.81081 14.3299 5.83399 14.1491 5.88859 13.9453L6.41785 11.9701L6.94138 12.1104L6.41059 14.0913C6.37369 14.229 6.3582 14.3472 6.36412 14.4458C6.37244 14.5431 6.39593 14.6252 6.4346 14.6922C6.47566 14.7578 6.52642 14.8098 6.58689 14.8482C6.64925 14.8872 6.71627 14.9163 6.78796 14.9355C7.01058 14.9951 7.19835 14.9999 7.35127 14.9499C7.5047 14.898 7.62917 14.807 7.72469 14.6769C7.8226 14.5454 7.89557 14.3901 7.9436 14.2109ZM9.97528 13.6239L9.33 16.0321L8.80648 15.8919L9.62691 12.8299L10.1221 12.9626L9.97528 13.6239ZM9.64679 14.3518L9.43117 14.2849C9.48917 14.076 9.57211 13.891 9.68 13.7298C9.78839 13.5668 9.91499 13.4339 10.0598 13.3311C10.2046 13.2284 10.3621 13.1614 10.5322 13.1301C10.7047 13.0975 10.8834 13.106 11.0683 13.1555C11.2193 13.1959 11.3495 13.2531 11.4591 13.327C11.5693 13.3989 11.6549 13.4906 11.716 13.602C11.7791 13.714 11.8143 13.8467 11.8216 14.0004C11.8295 14.1521 11.8064 14.3289 11.7523 14.5308L11.2147 16.5371L10.6883 16.3961L11.2275 14.3841C11.2704 14.2237 11.2812 14.0891 11.2598 13.9802C11.239 13.8695 11.1891 13.7803 11.1101 13.7127C11.0317 13.6431 10.9265 13.5906 10.7944 13.5553C10.6642 13.5204 10.538 13.5159 10.4158 13.5418C10.2955 13.5682 10.1838 13.6191 10.0807 13.6946C9.97944 13.7706 9.89045 13.8651 9.81369 13.9779C9.73933 14.0895 9.6837 14.2141 9.64679 14.3518ZM13.2653 17.1473C13.0521 17.0901 12.8684 17.0025 12.714 16.8843C12.5621 16.7647 12.4415 16.6222 12.3524 16.4568C12.2651 16.2919 12.2128 16.11 12.1953 15.9112C12.1778 15.7124 12.1978 15.5055 12.2555 15.2904L12.2873 15.1715C12.354 14.9225 12.4502 14.7107 12.5759 14.5361C12.702 14.3596 12.8465 14.2204 13.0093 14.1184C13.1721 14.0164 13.343 13.952 13.5221 13.9252C13.7011 13.8984 13.8765 13.908 14.0482 13.954C14.267 14.0126 14.4456 14.1009 14.5838 14.2188C14.724 14.3372 14.8268 14.478 14.8922 14.6411C14.9582 14.8024 14.9912 14.9811 14.9912 15.1772C14.9918 15.3715 14.9632 15.5761 14.9056 15.7912L14.8426 16.0261L12.5561 15.4134L12.6706 14.9861L14.4336 15.4585L14.4442 15.4189C14.4731 15.281 14.4802 15.1414 14.4655 15C14.4527 14.859 14.4056 14.7332 14.324 14.6223C14.2425 14.5115 14.1121 14.4321 13.9329 14.3841C13.8141 14.3522 13.6978 14.3484 13.5842 14.3725C13.471 14.3948 13.3655 14.4454 13.2675 14.5243C13.1695 14.6032 13.0818 14.708 13.0043 14.8389C12.9267 14.9698 12.8632 15.1277 12.8137 15.3126L12.7818 15.4314C12.7429 15.5767 12.726 15.7188 12.7313 15.8577C12.7389 15.9952 12.7685 16.1224 12.8201 16.2394C12.8735 16.3568 12.9486 16.4578 13.0452 16.5423C13.1438 16.6274 13.2647 16.6891 13.4081 16.7275C13.593 16.7771 13.7597 16.7813 13.9082 16.7402C14.0567 16.6991 14.196 16.6283 14.3261 16.5276L14.5756 16.8644C14.4827 16.9467 14.3732 17.0195 14.2471 17.0828C14.121 17.146 13.9758 17.186 13.8116 17.2026C13.6493 17.2198 13.4672 17.2013 13.2653 17.1473Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_6_97" x="0.202041" y="-0.297958" width="20.0959" height="20.0959" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.5" dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_6_97"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_6_97" x="11.7372" y="4.56225" width="6.24645" height="5.96956" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_97"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_97" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_97"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_6_97" x="0.673153" y="9.13222" width="15.9027" height="10.3343" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_97"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_97" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_97"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_6_97" x1="12.3431" y1="0.202042" x2="12.3431" y2="16.202" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E168FF"/>
|
||||
<stop offset="1" stop-color="#C848E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_6_97" x1="10" y1="-0.125" x2="10" y2="20.125" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.5"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6_97">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_6_97">
|
||||
<rect x="4.34315" y="0.202042" width="16" height="16" rx="1" transform="rotate(15 4.34315 0.202042)" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 172 KiB |
|
@ -1,69 +0,0 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_77)">
|
||||
<g filter="url(#filter0_i_6_77)">
|
||||
<g clip-path="url(#clip1_6_77)">
|
||||
<rect x="4.34315" y="0.202042" width="16" height="16" rx="1" transform="rotate(15 4.34315 0.202042)" fill="url(#paint0_linear_6_77)"/>
|
||||
<g filter="url(#filter1_di_6_77)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2306 7.25129C15.0975 7.48357 15.9885 6.96913 16.2208 6.10224C16.2693 5.92123 16.2852 5.73915 16.2722 5.56225C16.828 5.94013 17.1117 6.64112 16.9279 7.32699C16.6956 8.19387 15.8046 8.70832 14.9377 8.47604C14.2518 8.29226 13.7866 7.69611 13.7372 7.02583C13.8839 7.12556 14.0496 7.20279 14.2306 7.25129Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_6_77)">
|
||||
<path d="M5.30613 14.4778L5.18709 14.922L3.12695 14.37L3.24599 13.9257L5.30613 14.4778ZM4.33851 10.2786L3.23448 14.3988L2.68832 14.2525L3.79234 10.1322L4.33851 10.2786ZM7.67167 14.8295L8.30254 12.4751L8.82889 12.6161L8.00846 15.678L7.50757 15.5438L7.67167 14.8295ZM7.9436 14.2109L8.16301 14.2636C8.10842 14.4673 8.03617 14.6502 7.94627 14.8121C7.85877 14.9727 7.75092 15.1035 7.62272 15.2046C7.49452 15.3058 7.34441 15.3717 7.17239 15.4024C7.00087 15.4313 6.80569 15.4164 6.58685 15.3578C6.43781 15.3178 6.30685 15.2595 6.19396 15.1827C6.08296 15.1065 5.99526 15.0112 5.93085 14.8969C5.86645 14.7826 5.82918 14.6462 5.81906 14.4878C5.81081 14.3299 5.83399 14.1491 5.88859 13.9453L6.41785 11.9701L6.94138 12.1104L6.41059 14.0913C6.37369 14.229 6.3582 14.3472 6.36412 14.4458C6.37244 14.5431 6.39593 14.6252 6.4346 14.6922C6.47566 14.7578 6.52642 14.8098 6.58689 14.8482C6.64925 14.8872 6.71627 14.9163 6.78796 14.9355C7.01058 14.9951 7.19835 14.9999 7.35127 14.9499C7.5047 14.898 7.62917 14.807 7.72469 14.6769C7.8226 14.5454 7.89557 14.3901 7.9436 14.2109ZM9.97528 13.6239L9.33 16.0321L8.80648 15.8919L9.62691 12.8299L10.1221 12.9626L9.97528 13.6239ZM9.64679 14.3518L9.43117 14.2849C9.48917 14.076 9.57211 13.891 9.68 13.7298C9.78839 13.5668 9.91499 13.4339 10.0598 13.3311C10.2046 13.2284 10.3621 13.1614 10.5322 13.1301C10.7047 13.0975 10.8834 13.106 11.0683 13.1555C11.2193 13.1959 11.3495 13.2531 11.4591 13.327C11.5693 13.3989 11.6549 13.4906 11.716 13.602C11.7791 13.714 11.8143 13.8467 11.8216 14.0004C11.8295 14.1521 11.8064 14.3289 11.7523 14.5308L11.2147 16.5371L10.6883 16.3961L11.2275 14.3841C11.2704 14.2237 11.2812 14.0891 11.2598 13.9802C11.239 13.8695 11.1891 13.7803 11.1101 13.7127C11.0317 13.6431 10.9265 13.5906 10.7944 13.5553C10.6642 13.5204 10.538 13.5159 10.4158 13.5418C10.2955 13.5682 10.1838 13.6191 10.0807 13.6946C9.97944 13.7706 9.89045 13.8651 9.81369 13.9779C9.73933 14.0895 9.6837 14.2141 9.64679 14.3518ZM13.2653 17.1473C13.0521 17.0901 12.8684 17.0025 12.714 16.8843C12.5621 16.7647 12.4415 16.6222 12.3524 16.4568C12.2651 16.2919 12.2128 16.11 12.1953 15.9112C12.1778 15.7124 12.1978 15.5055 12.2555 15.2904L12.2873 15.1715C12.354 14.9225 12.4502 14.7107 12.5759 14.5361C12.702 14.3596 12.8465 14.2204 13.0093 14.1184C13.1721 14.0164 13.343 13.952 13.5221 13.9252C13.7011 13.8984 13.8765 13.908 14.0482 13.954C14.267 14.0126 14.4456 14.1009 14.5838 14.2188C14.724 14.3372 14.8268 14.478 14.8922 14.6411C14.9582 14.8024 14.9912 14.9811 14.9912 15.1772C14.9918 15.3715 14.9632 15.5761 14.9056 15.7912L14.8426 16.0261L12.5561 15.4134L12.6706 14.9861L14.4336 15.4585L14.4442 15.4189C14.4731 15.281 14.4802 15.1414 14.4655 15C14.4527 14.859 14.4056 14.7332 14.324 14.6223C14.2425 14.5115 14.1121 14.4321 13.9329 14.3841C13.8141 14.3522 13.6978 14.3484 13.5842 14.3725C13.471 14.3948 13.3655 14.4454 13.2675 14.5243C13.1695 14.6032 13.0818 14.708 13.0043 14.8389C12.9267 14.9698 12.8632 15.1277 12.8137 15.3126L12.7818 15.4314C12.7429 15.5767 12.726 15.7188 12.7313 15.8577C12.7389 15.9952 12.7685 16.1224 12.8201 16.2394C12.8735 16.3568 12.9486 16.4578 13.0452 16.5423C13.1438 16.6274 13.2647 16.6891 13.4081 16.7275C13.593 16.7771 13.7597 16.7813 13.9082 16.7402C14.0567 16.6991 14.196 16.6283 14.3261 16.5276L14.5756 16.8644C14.4827 16.9467 14.3732 17.0195 14.2471 17.0828C14.121 17.146 13.9758 17.186 13.8116 17.2026C13.6493 17.2198 13.4672 17.2013 13.2653 17.1473Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_6_77" x="0.202041" y="-0.297958" width="20.0959" height="20.0959" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.5" dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_6_77"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_6_77" x="11.7372" y="4.56224" width="6.24645" height="5.96957" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_77"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_77" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_77"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_6_77" x="0.673153" y="9.13222" width="15.9027" height="10.3343" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_77"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_77" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_77"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_6_77" x1="12.3431" y1="0.202042" x2="12.3431" y2="16.202" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E168FF"/>
|
||||
<stop offset="1" stop-color="#C848E9"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6_77">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_6_77">
|
||||
<rect x="4.34315" y="0.202042" width="16" height="16" rx="1" transform="rotate(15 4.34315 0.202042)" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 7.5 KiB |
|
@ -1,28 +0,0 @@
|
|||
[package]
|
||||
name = "lune-roblox"
|
||||
version = "0.2.2"
|
||||
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.10.3", features = ["luau"] }
|
||||
|
||||
glam = "0.30"
|
||||
rand = "0.9"
|
||||
thiserror = "2.0"
|
||||
|
||||
rbx_binary = "1.0"
|
||||
rbx_dom_weak = "3.0"
|
||||
rbx_reflection = "5.0"
|
||||
rbx_reflection_database = "1.0"
|
||||
rbx_xml = "1.0"
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|
|
@ -1,61 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
pub(crate) trait DomValueExt {
|
||||
fn variant_name(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
impl DomValueExt for DomType {
|
||||
fn variant_name(&self) -> Option<&'static str> {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use DomType::*;
|
||||
Some(match self {
|
||||
Attributes => "Attributes",
|
||||
Axes => "Axes",
|
||||
BinaryString => "BinaryString",
|
||||
Bool => "Bool",
|
||||
BrickColor => "BrickColor",
|
||||
CFrame => "CFrame",
|
||||
Color3 => "Color3",
|
||||
Color3uint8 => "Color3uint8",
|
||||
ColorSequence => "ColorSequence",
|
||||
Content => "Content",
|
||||
ContentId => "ContentId",
|
||||
Enum => "Enum",
|
||||
EnumItem => "EnumItem",
|
||||
Faces => "Faces",
|
||||
Float32 => "Float32",
|
||||
Float64 => "Float64",
|
||||
Font => "Font",
|
||||
Int32 => "Int32",
|
||||
Int64 => "Int64",
|
||||
MaterialColors => "MaterialColors",
|
||||
NumberRange => "NumberRange",
|
||||
NumberSequence => "NumberSequence",
|
||||
PhysicalProperties => "PhysicalProperties",
|
||||
Ray => "Ray",
|
||||
Rect => "Rect",
|
||||
Ref => "Ref",
|
||||
Region3 => "Region3",
|
||||
Region3int16 => "Region3int16",
|
||||
SharedString => "SharedString",
|
||||
String => "String",
|
||||
Tags => "Tags",
|
||||
UDim => "UDim",
|
||||
UDim2 => "UDim2",
|
||||
UniqueId => "UniqueId",
|
||||
Vector2 => "Vector2",
|
||||
Vector2int16 => "Vector2int16",
|
||||
Vector3 => "Vector3",
|
||||
Vector3int16 => "Vector3int16",
|
||||
OptionalCFrame => "OptionalCFrame",
|
||||
SecurityCapabilities => "SecurityCapabilities",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DomValueExt for DomValue {
|
||||
fn variant_name(&self) -> Option<&'static str> {
|
||||
self.ty().variant_name()
|
||||
}
|
||||
}
|
|
@ -1,494 +0,0 @@
|
|||
#![allow(clippy::items_after_statements)]
|
||||
|
||||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
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 lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector3};
|
||||
|
||||
/**
|
||||
An implementation of the [CFrame](https://create.roblox.com/docs/reference/engine/datatypes/CFrame)
|
||||
Roblox datatype, backed by [`glam::Mat4`].
|
||||
|
||||
This implements all documented properties, methods &
|
||||
constructors of the `CFrame` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CFrame(pub Mat4);
|
||||
|
||||
impl CFrame {
|
||||
pub const IDENTITY: Self = Self(Mat4::IDENTITY);
|
||||
|
||||
fn position(&self) -> Vec3 {
|
||||
self.0.w_axis.truncate()
|
||||
}
|
||||
|
||||
fn orientation(&self) -> Mat3 {
|
||||
Mat3::from_cols(
|
||||
self.0.x_axis.truncate(),
|
||||
self.0.y_axis.truncate(),
|
||||
self.0.z_axis.truncate(),
|
||||
)
|
||||
}
|
||||
|
||||
fn inverse(&self) -> Self {
|
||||
Self(self.0.inverse())
|
||||
}
|
||||
}
|
||||
|
||||
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 = |_: &Lua, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
|
||||
};
|
||||
|
||||
let cframe_from_axis_angle = |_: &Lua, (v, r): (LuaUserDataRef<Vector3>, f32)| {
|
||||
Ok(CFrame(Mat4::from_axis_angle(v.0, r)))
|
||||
};
|
||||
|
||||
let cframe_from_euler_angles_xyz = |_: &Lua, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
|
||||
};
|
||||
|
||||
let cframe_from_euler_angles_yxz = |_: &Lua, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
|
||||
};
|
||||
|
||||
let cframe_from_matrix = |_: &Lua,
|
||||
(pos, rx, ry, rz): (
|
||||
LuaUserDataRef<Vector3>,
|
||||
LuaUserDataRef<Vector3>,
|
||||
LuaUserDataRef<Vector3>,
|
||||
Option<LuaUserDataRef<Vector3>>,
|
||||
)| {
|
||||
Ok(CFrame(Mat4::from_cols(
|
||||
rx.0.extend(0.0),
|
||||
ry.0.extend(0.0),
|
||||
rz.map_or_else(|| rx.0.cross(ry.0).normalize(), |r| r.0)
|
||||
.extend(0.0),
|
||||
pos.0.extend(1.0),
|
||||
)))
|
||||
};
|
||||
|
||||
let cframe_from_orientation = |_: &Lua, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
|
||||
};
|
||||
|
||||
let cframe_look_at = |_: &Lua,
|
||||
(from, to, up): (
|
||||
LuaUserDataRef<Vector3>,
|
||||
LuaUserDataRef<Vector3>,
|
||||
Option<LuaUserDataRef<Vector3>>,
|
||||
)| {
|
||||
Ok(CFrame(look_at(
|
||||
from.0,
|
||||
to.0,
|
||||
up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0,
|
||||
)))
|
||||
};
|
||||
|
||||
// Dynamic args constructor
|
||||
type ArgsPos = LuaUserDataRef<Vector3>;
|
||||
type ArgsLook = (
|
||||
LuaUserDataRef<Vector3>,
|
||||
LuaUserDataRef<Vector3>,
|
||||
Option<LuaUserDataRef<Vector3>>,
|
||||
);
|
||||
|
||||
type ArgsPosXYZ = (f32, f32, f32);
|
||||
type ArgsPosXYZQuat = (f32, f32, f32, f32, f32, f32, f32);
|
||||
type ArgsMatrix = (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32);
|
||||
|
||||
let cframe_new = |lua: &Lua, args: LuaMultiValue| match args.len() {
|
||||
0 => Ok(CFrame(Mat4::IDENTITY)),
|
||||
|
||||
1 => match ArgsPos::from_lua_multi(args, lua) {
|
||||
Ok(pos) => Ok(CFrame(Mat4::from_translation(pos.0))),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
3 => {
|
||||
if let Ok((from, to, up)) = ArgsLook::from_lua_multi(args.clone(), lua) {
|
||||
Ok(CFrame(look_at(
|
||||
from.0,
|
||||
to.0,
|
||||
up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0,
|
||||
)))
|
||||
} else if let Ok((x, y, z)) = ArgsPosXYZ::from_lua_multi(args, lua) {
|
||||
Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z))))
|
||||
} else {
|
||||
// TODO: Make this error message better
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
7 => match ArgsPosXYZQuat::from_lua_multi(args, lua) {
|
||||
Ok((x, y, z, qx, qy, qz, qw)) => Ok(CFrame(Mat4::from_rotation_translation(
|
||||
Quat::from_array([qx, qy, qz, qw]),
|
||||
Vec3::new(x, y, z),
|
||||
))),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
12 => match ArgsMatrix::from_lua_multi(args, lua) {
|
||||
Ok((x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)) => {
|
||||
Ok(CFrame(Mat4::from_cols_array_2d(&[
|
||||
[r00, r10, r20, 0.0],
|
||||
[r01, r11, r21, 0.0],
|
||||
[r02, r12, r22, 0.0],
|
||||
[x, y, z, 1.0],
|
||||
])))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
_ => Err(LuaError::RuntimeError(format!(
|
||||
"Invalid number of arguments: expected 0, 1, 3, 7, or 12, got {}",
|
||||
args.len()
|
||||
))),
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("Angles", cframe_angles)?
|
||||
.with_value("identity", CFrame(Mat4::IDENTITY))?
|
||||
.with_function("fromAxisAngle", cframe_from_axis_angle)?
|
||||
.with_function("fromEulerAnglesXYZ", cframe_from_euler_angles_xyz)?
|
||||
.with_function("fromEulerAnglesYXZ", cframe_from_euler_angles_yxz)?
|
||||
.with_function("fromMatrix", cframe_from_matrix)?
|
||||
.with_function("fromOrientation", cframe_from_orientation)?
|
||||
.with_function("lookAt", cframe_look_at)?
|
||||
.with_function("new", cframe_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for CFrame {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Position", |_, this| Ok(Vector3(this.position())));
|
||||
fields.add_field_method_get("Rotation", |_, this| {
|
||||
Ok(CFrame(Mat4::from_cols(
|
||||
this.0.x_axis,
|
||||
this.0.y_axis,
|
||||
this.0.z_axis,
|
||||
Vec3::ZERO.extend(1.0),
|
||||
)))
|
||||
});
|
||||
fields.add_field_method_get("X", |_, this| Ok(this.position().x));
|
||||
fields.add_field_method_get("Y", |_, this| Ok(this.position().y));
|
||||
fields.add_field_method_get("Z", |_, this| Ok(this.position().z));
|
||||
fields.add_field_method_get("XVector", |_, this| Ok(Vector3(this.orientation().x_axis)));
|
||||
fields.add_field_method_get("YVector", |_, this| Ok(Vector3(this.orientation().y_axis)));
|
||||
fields.add_field_method_get("ZVector", |_, this| Ok(Vector3(this.orientation().z_axis)));
|
||||
fields.add_field_method_get("RightVector", |_, this| {
|
||||
Ok(Vector3(this.orientation().x_axis))
|
||||
});
|
||||
fields.add_field_method_get("UpVector", |_, this| Ok(Vector3(this.orientation().y_axis)));
|
||||
fields.add_field_method_get("LookVector", |_, this| {
|
||||
Ok(Vector3(-this.orientation().z_axis))
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("Inverse", |_, this, ()| Ok(this.inverse()));
|
||||
methods.add_method(
|
||||
"Lerp",
|
||||
|_, this, (goal, alpha): (LuaUserDataRef<CFrame>, f32)| {
|
||||
let quat_this = Quat::from_mat4(&this.0);
|
||||
let quat_goal = Quat::from_mat4(&goal.0);
|
||||
let translation = this
|
||||
.0
|
||||
.w_axis
|
||||
.truncate()
|
||||
.lerp(goal.0.w_axis.truncate(), alpha);
|
||||
let rotation = quat_this.slerp(quat_goal, alpha);
|
||||
Ok(CFrame(Mat4::from_rotation_translation(
|
||||
rotation,
|
||||
translation,
|
||||
)))
|
||||
},
|
||||
);
|
||||
methods.add_method("Orthonormalize", |_, this, ()| {
|
||||
let rotation = Quat::from_mat4(&this.0);
|
||||
let translation = this.0.w_axis.truncate();
|
||||
Ok(CFrame(Mat4::from_rotation_translation(
|
||||
rotation.normalize(),
|
||||
translation,
|
||||
)))
|
||||
});
|
||||
methods.add_method(
|
||||
"ToWorldSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|cf| *this * *cf)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"ToObjectSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
|
||||
let inverse = this.inverse();
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|cf| inverse * *cf)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"PointToWorldSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| *this * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"PointToObjectSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
let inverse = this.inverse();
|
||||
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(rhs
|
||||
.into_iter()
|
||||
.map(|v3| result * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"VectorToObjectSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
let inverse = this.inverse();
|
||||
let result = inverse - Vector3(inverse.position());
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| result * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
methods.add_method("GetComponents", |_, this, ()| {
|
||||
let pos = this.position();
|
||||
let transposed = this.orientation().transpose();
|
||||
Ok((
|
||||
pos.x, pos.y, pos.z,
|
||||
transposed.x_axis.x, transposed.x_axis.y, transposed.x_axis.z,
|
||||
transposed.y_axis.x, transposed.y_axis.y, transposed.y_axis.z,
|
||||
transposed.z_axis.x, transposed.z_axis.y, transposed.z_axis.z,
|
||||
))
|
||||
});
|
||||
methods.add_method("ToEulerAnglesXYZ", |_, this, ()| {
|
||||
Ok(Quat::from_mat4(&this.0).to_euler(EulerRot::XYZ))
|
||||
});
|
||||
methods.add_method("ToEulerAnglesYXZ", |_, this, ()| {
|
||||
let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ);
|
||||
Ok((rx, ry, rz))
|
||||
});
|
||||
methods.add_method("ToOrientation", |_, this, ()| {
|
||||
let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ);
|
||||
Ok((rx, ry, rz))
|
||||
});
|
||||
methods.add_method("ToAxisAngle", |_, this, ()| {
|
||||
let (axis, angle) = Quat::from_mat4(&this.0).to_axis_angle();
|
||||
Ok((Vector3(axis), angle))
|
||||
});
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, |lua, this, rhs: LuaValue| {
|
||||
if let LuaValue::UserData(ud) = &rhs {
|
||||
if let Ok(cf) = ud.borrow::<CFrame>() {
|
||||
return lua.create_userdata(*this * *cf);
|
||||
} else if let Ok(vec) = ud.borrow::<Vector3>() {
|
||||
return lua.create_userdata(*this * *vec);
|
||||
}
|
||||
}
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: rhs.type_name(),
|
||||
to: "userdata".to_string(),
|
||||
message: Some(format!(
|
||||
"Expected CFrame or Vector3, got {}",
|
||||
rhs.type_name()
|
||||
)),
|
||||
})
|
||||
});
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Add,
|
||||
|_, this, vec: LuaUserDataRef<Vector3>| Ok(*this + *vec),
|
||||
);
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Sub,
|
||||
|_, this, vec: LuaUserDataRef<Vector3>| Ok(*this - *vec),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CFrame {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let pos = self.position();
|
||||
let transposed = self.orientation().transpose();
|
||||
write!(
|
||||
f,
|
||||
"{}, {}, {}, {}",
|
||||
Vector3(pos),
|
||||
Vector3(transposed.x_axis),
|
||||
Vector3(transposed.y_axis),
|
||||
Vector3(transposed.z_axis)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for CFrame {
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
CFrame(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<Vector3> for CFrame {
|
||||
type Output = Vector3;
|
||||
fn mul(self, rhs: Vector3) -> Self::Output {
|
||||
Vector3(self.0.project_point3(rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<Vector3> for CFrame {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Vector3) -> Self::Output {
|
||||
CFrame(Mat4::from_cols(
|
||||
self.0.x_axis,
|
||||
self.0.y_axis,
|
||||
self.0.z_axis,
|
||||
self.0.w_axis + rhs.0.extend(0.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub<Vector3> for CFrame {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Vector3) -> Self::Output {
|
||||
CFrame(Mat4::from_cols(
|
||||
self.0.x_axis,
|
||||
self.0.y_axis,
|
||||
self.0.z_axis,
|
||||
self.0.w_axis - rhs.0.extend(0.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomCFrame> for CFrame {
|
||||
fn from(v: DomCFrame) -> Self {
|
||||
let transposed = v.orientation.transpose();
|
||||
CFrame(Mat4::from_cols(
|
||||
Vector3::from(transposed.x).0.extend(0.0),
|
||||
Vector3::from(transposed.y).0.extend(0.0),
|
||||
Vector3::from(transposed.z).0.extend(0.0),
|
||||
Vector3::from(v.position).0.extend(1.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CFrame> for DomCFrame {
|
||||
fn from(v: CFrame) -> Self {
|
||||
let transposed = v.orientation().transpose();
|
||||
DomCFrame {
|
||||
position: DomVector3::from(Vector3(v.position())),
|
||||
orientation: DomMatrix3::new(
|
||||
DomVector3::from(Vector3(transposed.x_axis)),
|
||||
DomVector3::from(Vector3(transposed.y_axis)),
|
||||
DomVector3::from(Vector3(transposed.z_axis)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a matrix at the position `from`, looking towards `to`.
|
||||
|
||||
[`glam`] does provide functions such as [`look_at_lh`], [`look_at_rh`] and more but
|
||||
they all create view matrices for camera transforms which is not what we want here.
|
||||
*/
|
||||
fn look_at(from: Vec3, to: Vec3, up: Vec3) -> Mat4 {
|
||||
let dir = (to - from).normalize();
|
||||
let xaxis = up.cross(dir).normalize();
|
||||
let yaxis = dir.cross(xaxis).normalize();
|
||||
|
||||
Mat4::from_cols(
|
||||
Vec3::new(xaxis.x, yaxis.x, dir.x).extend(0.0),
|
||||
Vec3::new(xaxis.y, yaxis.y, dir.y).extend(0.0),
|
||||
Vec3::new(xaxis.z, yaxis.z, dir.z).extend(0.0),
|
||||
from.extend(1.0),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cframe_test {
|
||||
use glam::{Mat4, Vec3};
|
||||
use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3};
|
||||
|
||||
use super::CFrame;
|
||||
|
||||
#[test]
|
||||
fn dom_cframe_from_cframe() {
|
||||
let dom_cframe = DomCFrame::new(
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
DomMatrix3::new(
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
),
|
||||
);
|
||||
|
||||
let cframe = CFrame(Mat4::from_cols(
|
||||
Vec3::new(1.0, 1.0, 1.0).extend(0.0),
|
||||
Vec3::new(2.0, 2.0, 2.0).extend(0.0),
|
||||
Vec3::new(3.0, 3.0, 3.0).extend(0.0),
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(1.0),
|
||||
));
|
||||
|
||||
assert_eq!(CFrame::from(dom_cframe), cframe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cframe_from_dom_cframe() {
|
||||
let cframe = CFrame(Mat4::from_cols(
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(1.0),
|
||||
));
|
||||
|
||||
let dom_cframe = DomCFrame::new(
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
DomMatrix3::new(
|
||||
DomVector3::new(1.0, 1.0, 1.0),
|
||||
DomVector3::new(2.0, 2.0, 2.0),
|
||||
DomVector3::new(3.0, 3.0, 3.0),
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(DomCFrame::from(cframe), dom_cframe);
|
||||
}
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{
|
||||
ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint,
|
||||
};
|
||||
|
||||
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.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ColorSequence {
|
||||
pub(crate) keypoints: Vec<ColorSequenceKeypoint>,
|
||||
}
|
||||
|
||||
impl LuaExportsTable for ColorSequence {
|
||||
const EXPORT_NAME: &'static str = "ColorSequence";
|
||||
|
||||
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
|
||||
type ArgsColor = LuaUserDataRef<Color3>;
|
||||
type ArgsColors = (LuaUserDataRef<Color3>, LuaUserDataRef<Color3>);
|
||||
type ArgsKeypoints = Vec<LuaUserDataRef<ColorSequenceKeypoint>>;
|
||||
|
||||
let color_sequence_new = |lua: &Lua, args: LuaMultiValue| {
|
||||
if let Ok(color) = ArgsColor::from_lua_multi(args.clone(), lua) {
|
||||
Ok(ColorSequence {
|
||||
keypoints: vec![
|
||||
ColorSequenceKeypoint {
|
||||
time: 0.0,
|
||||
color: *color,
|
||||
},
|
||||
ColorSequenceKeypoint {
|
||||
time: 1.0,
|
||||
color: *color,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if let Ok((c0, c1)) = ArgsColors::from_lua_multi(args.clone(), lua) {
|
||||
Ok(ColorSequence {
|
||||
keypoints: vec![
|
||||
ColorSequenceKeypoint {
|
||||
time: 0.0,
|
||||
color: *c0,
|
||||
},
|
||||
ColorSequenceKeypoint {
|
||||
time: 1.0,
|
||||
color: *c1,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) {
|
||||
Ok(ColorSequence {
|
||||
keypoints: keypoints.iter().map(|k| **k).collect(),
|
||||
})
|
||||
} else {
|
||||
// FUTURE: Better error message here using given arg types
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", color_sequence_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for ColorSequence {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
|
||||
}
|
||||
|
||||
fn add_methods<M: LuaUserDataMethods<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 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}, ")?;
|
||||
} else {
|
||||
write!(f, "{keypoint}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomColorSequence> for ColorSequence {
|
||||
fn from(v: DomColorSequence) -> Self {
|
||||
Self {
|
||||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.copied()
|
||||
.map(ColorSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorSequence> for DomColorSequence {
|
||||
fn from(v: ColorSequence) -> Self {
|
||||
Self {
|
||||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.copied()
|
||||
.map(DomColorSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
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 = |_: &Lua, uri: String| Ok(Self(ContentType::Uri(uri)));
|
||||
|
||||
let from_object = |_: &Lua, 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<F: LuaUserDataFields<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<M: LuaUserDataMethods<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,129 +0,0 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{
|
||||
NumberSequence as DomNumberSequence, NumberSequenceKeypoint as DomNumberSequenceKeypoint,
|
||||
};
|
||||
|
||||
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.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NumberSequence {
|
||||
pub(crate) keypoints: Vec<NumberSequenceKeypoint>,
|
||||
}
|
||||
|
||||
impl LuaExportsTable for NumberSequence {
|
||||
const EXPORT_NAME: &'static str = "NumberSequence";
|
||||
|
||||
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
|
||||
type ArgsColor = f32;
|
||||
type ArgsColors = (f32, f32);
|
||||
type ArgsKeypoints = Vec<LuaUserDataRef<NumberSequenceKeypoint>>;
|
||||
|
||||
let number_sequence_new = |lua: &Lua, args: LuaMultiValue| {
|
||||
if let Ok(value) = ArgsColor::from_lua_multi(args.clone(), lua) {
|
||||
Ok(NumberSequence {
|
||||
keypoints: vec![
|
||||
NumberSequenceKeypoint {
|
||||
time: 0.0,
|
||||
value,
|
||||
envelope: 0.0,
|
||||
},
|
||||
NumberSequenceKeypoint {
|
||||
time: 1.0,
|
||||
value,
|
||||
envelope: 0.0,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if let Ok((v0, v1)) = ArgsColors::from_lua_multi(args.clone(), lua) {
|
||||
Ok(NumberSequence {
|
||||
keypoints: vec![
|
||||
NumberSequenceKeypoint {
|
||||
time: 0.0,
|
||||
value: v0,
|
||||
envelope: 0.0,
|
||||
},
|
||||
NumberSequenceKeypoint {
|
||||
time: 1.0,
|
||||
value: v1,
|
||||
envelope: 0.0,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) {
|
||||
Ok(NumberSequence {
|
||||
keypoints: keypoints.iter().map(|k| **k).collect(),
|
||||
})
|
||||
} else {
|
||||
// FUTURE: Better error message here using given arg types
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", number_sequence_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for NumberSequence {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
|
||||
}
|
||||
|
||||
fn add_methods<M: LuaUserDataMethods<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 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}, ")?;
|
||||
} else {
|
||||
write!(f, "{keypoint}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomNumberSequence> for NumberSequence {
|
||||
fn from(v: DomNumberSequence) -> Self {
|
||||
Self {
|
||||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.copied()
|
||||
.map(NumberSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberSequence> for DomNumberSequence {
|
||||
fn from(v: NumberSequence) -> Self {
|
||||
Self {
|
||||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.copied()
|
||||
.map(DomNumberSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// HACK: We round to the nearest Very Small Decimal
|
||||
// to reduce writing out floating point accumulation
|
||||
// errors to files (mostly relevant for xml formats)
|
||||
const ROUNDING: usize = 65_536; // 2 ^ 16
|
||||
|
||||
pub fn round_float_decimal(value: f32) -> f32 {
|
||||
let place = ROUNDING as f32;
|
||||
|
||||
// Round only the fractional part, we do not want to
|
||||
// lose any float precision in case a user for some
|
||||
// reason has very very large float numbers in files
|
||||
let whole = value.trunc();
|
||||
let fract = (value.fract() * place).round() / place;
|
||||
|
||||
whole + fract
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
/**
|
||||
Trait for any item that should be exported as part of the `roblox` built-in library.
|
||||
|
||||
This may be an enum or a struct that should export constants and/or constructs.
|
||||
|
||||
### Example usage
|
||||
|
||||
```rs
|
||||
use mlua::prelude::*;
|
||||
|
||||
struct MyType(usize);
|
||||
|
||||
impl MyType {
|
||||
pub fn new(n: usize) -> Self {
|
||||
Self(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable for MyType {
|
||||
const EXPORT_NAME: &'static str = "MyType";
|
||||
|
||||
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable> {
|
||||
let my_type_new = |lua, n: Option<usize>| {
|
||||
Self::new(n.unwrap_or_default())
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", my_type_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for MyType {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
*/
|
||||
pub trait LuaExportsTable {
|
||||
const EXPORT_NAME: &'static str;
|
||||
|
||||
fn create_exports_table(lua: Lua) -> LuaResult<LuaTable>;
|
||||
}
|
||||
|
||||
/**
|
||||
Exports a single item that implements the [`LuaExportsTable`] trait.
|
||||
|
||||
Returns the name of the export, as well as the export table.
|
||||
|
||||
### Example usage
|
||||
|
||||
```rs
|
||||
let lua: mlua::Lua::new();
|
||||
|
||||
let (name1, table1) = export::<Type1>(lua)?;
|
||||
let (name2, table2) = export::<Type2>(lua)?;
|
||||
```
|
||||
*/
|
||||
pub fn export<T>(lua: Lua) -> LuaResult<(&'static str, LuaValue)>
|
||||
where
|
||||
T: LuaExportsTable,
|
||||
{
|
||||
Ok((
|
||||
T::EXPORT_NAME,
|
||||
<T as LuaExportsTable>::create_exports_table(lua.clone())?.into_lua(&lua)?,
|
||||
))
|
||||
}
|
|
@ -1,270 +0,0 @@
|
|||
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,
|
||||
class_name: &str,
|
||||
method_name: &str,
|
||||
method: LuaFunction,
|
||||
) -> 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,
|
||||
class_name: &str,
|
||||
property_name: &str,
|
||||
property_getter: LuaFunction,
|
||||
) -> 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,
|
||||
class_name: &str,
|
||||
property_name: &str,
|
||||
property_setter: LuaFunction,
|
||||
) -> 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, instance: &Instance, method_name: &str) -> Option<LuaFunction> {
|
||||
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,
|
||||
instance: &Instance,
|
||||
property_name: &str,
|
||||
) -> Option<LuaFunction> {
|
||||
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,
|
||||
instance: &Instance,
|
||||
property_name: &str,
|
||||
) -> Option<LuaFunction> {
|
||||
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,93 +0,0 @@
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{MaterialColors, TerrainMaterials, Variant};
|
||||
|
||||
use crate::{
|
||||
datatypes::types::{Color3, EnumItem},
|
||||
shared::classes::{add_class_restricted_method, add_class_restricted_method_mut},
|
||||
};
|
||||
|
||||
use super::Instance;
|
||||
|
||||
pub const CLASS_NAME: &str = "Terrain";
|
||||
|
||||
pub fn add_methods<M: LuaUserDataMethods<Instance>>(methods: &mut M) {
|
||||
add_class_restricted_method(
|
||||
methods,
|
||||
CLASS_NAME,
|
||||
"GetMaterialColor",
|
||||
terrain_get_material_color,
|
||||
);
|
||||
|
||||
add_class_restricted_method_mut(
|
||||
methods,
|
||||
CLASS_NAME,
|
||||
"SetMaterialColor",
|
||||
terrain_set_material_color,
|
||||
);
|
||||
}
|
||||
|
||||
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
|
||||
if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") {
|
||||
inner
|
||||
} else {
|
||||
MaterialColors::default()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the color of the given terrain material.
|
||||
|
||||
### See Also
|
||||
* [`GetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#GetMaterialColor)
|
||||
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);
|
||||
|
||||
if &material.parent.desc.name != "Material" {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected Enum.Material, got Enum.{}",
|
||||
&material.parent.desc.name
|
||||
)));
|
||||
}
|
||||
|
||||
let terrain_material = material
|
||||
.name
|
||||
.parse::<TerrainMaterials>()
|
||||
.map_err(|err| LuaError::RuntimeError(err.to_string()))?;
|
||||
|
||||
Ok(material_colors.get_color(terrain_material).into())
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the color of the given terrain material.
|
||||
|
||||
### See Also
|
||||
* [`SetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#SetMaterialColor)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn terrain_set_material_color(
|
||||
_: &Lua,
|
||||
this: &mut Instance,
|
||||
args: (EnumItem, Color3),
|
||||
) -> LuaResult<()> {
|
||||
let mut material_colors = get_or_create_material_colors(this);
|
||||
let material = args.0;
|
||||
let color = args.1;
|
||||
|
||||
if &material.parent.desc.name != "Material" {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected Enum.Material, got Enum.{}",
|
||||
&material.parent.desc.name
|
||||
)));
|
||||
}
|
||||
|
||||
let terrain_material = material
|
||||
.name
|
||||
.parse::<TerrainMaterials>()
|
||||
.map_err(|err| LuaError::RuntimeError(err.to_string()))?;
|
||||
|
||||
material_colors.set_color(terrain_material, color.into());
|
||||
this.set_property("MaterialColors", Variant::MaterialColors(material_colors));
|
||||
Ok(())
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
pub mod datatypes;
|
||||
pub mod document;
|
||||
pub mod instance;
|
||||
pub mod reflection;
|
||||
|
||||
pub(crate) mod exports;
|
||||
pub(crate) mod shared;
|
||||
|
||||
use exports::export;
|
||||
|
||||
fn create_all_exports(lua: Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
|
||||
use datatypes::types::*;
|
||||
use instance::Instance;
|
||||
Ok(vec![
|
||||
// Datatypes
|
||||
export::<Axes>(lua.clone())?,
|
||||
export::<BrickColor>(lua.clone())?,
|
||||
export::<CFrame>(lua.clone())?,
|
||||
export::<Color3>(lua.clone())?,
|
||||
export::<ColorSequence>(lua.clone())?,
|
||||
export::<ColorSequenceKeypoint>(lua.clone())?,
|
||||
export::<Content>(lua.clone())?,
|
||||
export::<Faces>(lua.clone())?,
|
||||
export::<Font>(lua.clone())?,
|
||||
export::<NumberRange>(lua.clone())?,
|
||||
export::<NumberSequence>(lua.clone())?,
|
||||
export::<NumberSequenceKeypoint>(lua.clone())?,
|
||||
export::<PhysicalProperties>(lua.clone())?,
|
||||
export::<Ray>(lua.clone())?,
|
||||
export::<Rect>(lua.clone())?,
|
||||
export::<UDim>(lua.clone())?,
|
||||
export::<UDim2>(lua.clone())?,
|
||||
export::<Region3>(lua.clone())?,
|
||||
export::<Region3int16>(lua.clone())?,
|
||||
export::<Vector2>(lua.clone())?,
|
||||
export::<Vector2int16>(lua.clone())?,
|
||||
export::<Vector3>(lua.clone())?,
|
||||
export::<Vector3int16>(lua.clone())?,
|
||||
// Classes
|
||||
export::<Instance>(lua.clone())?,
|
||||
// Singletons
|
||||
("Enum", Enums.into_lua(&lua)?),
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
// save some memory and startup time. The full exports
|
||||
// table is quite big and probably won't get any smaller
|
||||
// since we impl all roblox constructors for each datatype.
|
||||
let exports = create_all_exports(lua.clone())?;
|
||||
TableBuilder::new(lua)?
|
||||
.with_values(exports)?
|
||||
.build_readonly()
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
name = "lune-std-datetime"
|
||||
version = "0.2.2"
|
||||
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.10.3", features = ["luau"] }
|
||||
|
||||
thiserror = "2.0"
|
||||
chrono = "0.4.38"
|
||||
chrono_lc = "0.1.6"
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|
|
@ -1,277 +0,0 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use chrono::DateTime as ChronoDateTime;
|
||||
use chrono_lc::LocaleDate;
|
||||
|
||||
use crate::result::{DateTimeError, DateTimeResult};
|
||||
use crate::values::DateTimeValues;
|
||||
|
||||
const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
|
||||
const DEFAULT_LOCALE: &str = "en";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct DateTime {
|
||||
// NOTE: We store this as the UTC time zone since it is the most commonly
|
||||
// used and getting the generics right for TimeZone is somewhat tricky,
|
||||
// but none of the method implementations below should rely on this tz
|
||||
inner: ChronoDateTime<Utc>,
|
||||
}
|
||||
|
||||
impl DateTime {
|
||||
/**
|
||||
Creates a new `DateTime` struct representing the current moment in time.
|
||||
|
||||
See [`chrono::DateTime::now`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn now() -> Self {
|
||||
Self { inner: Utc::now() }
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new `DateTime` struct from the given `unix_timestamp`,
|
||||
which is a float of seconds passed since the UNIX epoch.
|
||||
|
||||
This is somewhat unconventional, but fits our Luau interface and dynamic types quite well.
|
||||
To use this method the same way you would use a more traditional `from_unix_timestamp`
|
||||
that takes a `u64` of seconds or similar type, casting the value is sufficient:
|
||||
|
||||
```rust ignore
|
||||
DateTime::from_unix_timestamp_float(123456789u64 as f64)
|
||||
```
|
||||
|
||||
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;
|
||||
let fract = unix_timestamp.fract();
|
||||
let nanos = (fract * 1_000_000_000f64)
|
||||
.round()
|
||||
.clamp(u32::MIN as f64, u32::MAX as f64) as u32;
|
||||
let inner = ChronoDateTime::<Utc>::from_timestamp(whole, nanos)
|
||||
.ok_or(DateTimeError::OutOfRangeUnspecified)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Transforms individual date & time values into a new
|
||||
`DateTime` struct, using the universal (UTC) time zone.
|
||||
|
||||
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)
|
||||
.ok_or(DateTimeError::InvalidDate)?;
|
||||
|
||||
let time = NaiveTime::from_hms_milli_opt(
|
||||
values.hour,
|
||||
values.minute,
|
||||
values.second,
|
||||
values.millisecond,
|
||||
)
|
||||
.ok_or(DateTimeError::InvalidTime)?;
|
||||
|
||||
let inner = Utc.from_utc_datetime(&NaiveDateTime::new(date, time));
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Transforms individual date & time values into a new
|
||||
`DateTime` struct, using the current local time zone.
|
||||
|
||||
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)
|
||||
.ok_or(DateTimeError::InvalidDate)?;
|
||||
|
||||
let time = NaiveTime::from_hms_milli_opt(
|
||||
values.hour,
|
||||
values.minute,
|
||||
values.second,
|
||||
values.millisecond,
|
||||
)
|
||||
.ok_or(DateTimeError::InvalidTime)?;
|
||||
|
||||
let inner = Local
|
||||
.from_local_datetime(&NaiveDateTime::new(date, time))
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Formats the `DateTime` using the universal (UTC) time
|
||||
zone, the given format string, and the given locale.
|
||||
|
||||
`format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
|
||||
|
||||
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)
|
||||
.formatl(
|
||||
format.unwrap_or(DEFAULT_FORMAT),
|
||||
locale.unwrap_or(DEFAULT_LOCALE),
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Formats the `DateTime` using the universal (UTC) time
|
||||
zone, the given format string, and the given locale.
|
||||
|
||||
`format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
|
||||
|
||||
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)
|
||||
.formatl(
|
||||
format.unwrap_or(DEFAULT_FORMAT),
|
||||
locale.unwrap_or(DEFAULT_LOCALE),
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Parses a time string in the RFC 3339 format, such as
|
||||
`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_rfc_3339(date: impl AsRef<str>) -> DateTimeResult<Self> {
|
||||
let inner = ChronoDateTime::parse_from_rfc3339(date.as_ref())?.with_timezone(&Utc);
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Parses a time string in the RFC 2822 format, such as
|
||||
`Tue, 1 Jul 2003 10:52:37 +0200`, into a new `DateTime` struct.
|
||||
|
||||
See [`chrono::DateTime::parse_from_rfc2822`] for additional details.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the input string is not a valid RFC 2822 date-time.
|
||||
*/
|
||||
pub fn from_rfc_2822(date: impl AsRef<str>) -> DateTimeResult<Self> {
|
||||
let inner = ChronoDateTime::parse_from_rfc2822(date.as_ref())?.with_timezone(&Utc);
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
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))
|
||||
}
|
||||
|
||||
/**
|
||||
Formats a time string in the RFC 3339 format, such as `1996-12-19T16:39:57-08:00`.
|
||||
|
||||
See [`chrono::DateTime::to_rfc3339`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn to_rfc_3339(self) -> String {
|
||||
self.inner.to_rfc3339()
|
||||
}
|
||||
|
||||
/**
|
||||
Formats a time string in the RFC 2822 format, such as `Tue, 1 Jul 2003 10:52:37 +0200`.
|
||||
|
||||
See [`chrono::DateTime::to_rfc2822`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn to_rfc_2822(self) -> String {
|
||||
self.inner.to_rfc2822()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DateTime {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("unixTimestamp", |_, this| Ok(this.inner.timestamp()));
|
||||
fields.add_field_method_get("unixTimestampMillis", |_, this| {
|
||||
Ok(this.inner.timestamp_millis())
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
// Metamethods to compare DateTime as instants in time
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Eq,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| Ok(this.eq(&other)),
|
||||
);
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Lt,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| {
|
||||
Ok(matches!(this.cmp(&other), Ordering::Less))
|
||||
},
|
||||
);
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Le,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| {
|
||||
Ok(matches!(this.cmp(&other), Ordering::Less | Ordering::Equal))
|
||||
},
|
||||
);
|
||||
// Normal methods
|
||||
methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_rfc_3339())); // FUTURE: Remove this rfc3339 alias method
|
||||
methods.add_method("toRfc3339", |_, this, ()| Ok(this.to_rfc_3339()));
|
||||
methods.add_method("toRfc2822", |_, this, ()| Ok(this.to_rfc_2822()));
|
||||
methods.add_method(
|
||||
"formatUniversalTime",
|
||||
|_, this, (format, locale): (Option<String>, Option<String>)| {
|
||||
Ok(this.format_string_universal(format.as_deref(), locale.as_deref()))
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"formatLocalTime",
|
||||
|_, this, (format, locale): (Option<String>, Option<String>)| {
|
||||
Ok(this.format_string_local(format.as_deref(), locale.as_deref()))
|
||||
},
|
||||
);
|
||||
methods.add_method("toUniversalTime", |_, this: &Self, ()| {
|
||||
Ok(this.to_universal_time())
|
||||
});
|
||||
methods.add_method("toLocalTime", |_, this: &Self, ()| Ok(this.to_local_time()));
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
#![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;
|
||||
|
||||
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
|
||||
|
||||
/**
|
||||
Returns a string containing type definitions for the `datetime` standard library.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn typedefs() -> String {
|
||||
TYPEDEFS.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
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", |_, date: String| {
|
||||
Ok(DateTime::from_rfc_3339(date)?) // FUTURE: Remove this rfc3339 alias method
|
||||
})?
|
||||
.with_function("fromRfc3339", |_, date: String| {
|
||||
Ok(DateTime::from_rfc_3339(date)?)
|
||||
})?
|
||||
.with_function("fromRfc2822", |_, date: String| {
|
||||
Ok(DateTime::from_rfc_2822(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()
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub type DateTimeResult<T, E = DateTimeError> = Result<T, E>;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum DateTimeError {
|
||||
#[error("invalid date")]
|
||||
InvalidDate,
|
||||
#[error("invalid time")]
|
||||
InvalidTime,
|
||||
#[error("ambiguous date or time")]
|
||||
Ambiguous,
|
||||
#[error("date or time is outside allowed range")]
|
||||
OutOfRangeUnspecified,
|
||||
#[error("{name} must be within range {min} -> {max}, got {value}")]
|
||||
OutOfRange {
|
||||
name: &'static str,
|
||||
value: String,
|
||||
min: String,
|
||||
max: String,
|
||||
},
|
||||
#[error(transparent)]
|
||||
ParseError(#[from] chrono::ParseError),
|
||||
}
|
||||
|
||||
impl From<DateTimeError> for LuaError {
|
||||
fn from(value: DateTimeError) -> Self {
|
||||
LuaError::runtime(value.to_string())
|
||||
}
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use chrono::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use super::result::{DateTimeError, DateTimeResult};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DateTimeValues {
|
||||
pub year: i32,
|
||||
pub month: u32,
|
||||
pub day: u32,
|
||||
pub hour: u32,
|
||||
pub minute: u32,
|
||||
pub second: u32,
|
||||
pub millisecond: u32,
|
||||
}
|
||||
|
||||
impl DateTimeValues {
|
||||
/**
|
||||
Verifies that all of the date & time values are within allowed ranges:
|
||||
|
||||
| Name | Range |
|
||||
|---------------|----------------|
|
||||
| `year` | `1400 -> 9999` |
|
||||
| `month` | `1 -> 12` |
|
||||
| `day` | `1 -> 31` |
|
||||
| `hour` | `0 -> 23` |
|
||||
| `minute` | `0 -> 59` |
|
||||
| `second` | `0 -> 60` |
|
||||
| `millisecond` | `0 -> 999` |
|
||||
*/
|
||||
pub fn verify(self) -> DateTimeResult<Self> {
|
||||
verify_in_range("year", self.year, 1400, 9999)?;
|
||||
verify_in_range("month", self.month, 1, 12)?;
|
||||
verify_in_range("day", self.day, 1, 31)?;
|
||||
verify_in_range("hour", self.hour, 0, 23)?;
|
||||
verify_in_range("minute", self.minute, 0, 59)?;
|
||||
verify_in_range("second", self.second, 0, 60)?;
|
||||
verify_in_range("millisecond", self.millisecond, 0, 999)?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_in_range<T>(name: &'static str, value: T, min: T, max: T) -> DateTimeResult<T>
|
||||
where
|
||||
T: PartialOrd + std::fmt::Display,
|
||||
{
|
||||
assert!(max > min);
|
||||
if value < min || value > max {
|
||||
Err(DateTimeError::OutOfRange {
|
||||
name,
|
||||
min: min.to_string(),
|
||||
max: max.to_string(),
|
||||
value: value.to_string(),
|
||||
})
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Conversion methods between `DateTimeValues` and plain lua tables
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
impl FromLua for DateTimeValues {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
if !value.is_table() {
|
||||
return Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "DateTimeValues".to_string(),
|
||||
message: Some("value must be a table".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
let value = value.as_table().unwrap();
|
||||
let values = Self {
|
||||
year: value.get("year")?,
|
||||
month: value.get("month")?,
|
||||
day: value.get("day")?,
|
||||
hour: value.get("hour")?,
|
||||
minute: value.get("minute")?,
|
||||
second: value.get("second")?,
|
||||
millisecond: value.get("millisecond").unwrap_or(0),
|
||||
};
|
||||
|
||||
match values.verify() {
|
||||
Ok(dt) => Ok(dt),
|
||||
Err(e) => Err(LuaError::FromLuaConversionError {
|
||||
from: "table",
|
||||
to: "DateTimeValues".to_string(),
|
||||
message: Some(e.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoLua for DateTimeValues {
|
||||
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
|
||||
let tab = TableBuilder::new(lua.clone())?
|
||||
.with_value("year", self.year)?
|
||||
.with_values(vec![
|
||||
("month", self.month),
|
||||
("day", self.day),
|
||||
("hour", self.hour),
|
||||
("minute", self.minute),
|
||||
("second", self.second),
|
||||
("millisecond", self.millisecond),
|
||||
])?
|
||||
.build_readonly()?;
|
||||
Ok(LuaValue::Table(tab))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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 {
|
||||
fn from(value: DateTime<T>) -> Self {
|
||||
Self {
|
||||
year: value.year(),
|
||||
month: value.month(),
|
||||
day: value.day(),
|
||||
hour: value.hour(),
|
||||
minute: value.minute(),
|
||||
second: value.second(),
|
||||
millisecond: value.timestamp_subsec_millis(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DateTimeValues> for DateTime<Utc> {
|
||||
type Error = DateTimeError;
|
||||
fn try_from(value: DateTimeValues) -> Result<Self, Self::Error> {
|
||||
Utc.with_ymd_and_hms(
|
||||
value.year,
|
||||
value.month,
|
||||
value.day,
|
||||
value.hour,
|
||||
value.minute,
|
||||
value.second,
|
||||
)
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DateTimeValues> for DateTime<Local> {
|
||||
type Error = DateTimeError;
|
||||
fn try_from(value: DateTimeValues) -> Result<Self, Self::Error> {
|
||||
Local
|
||||
.with_ymd_and_hms(
|
||||
value.year,
|
||||
value.month,
|
||||
value.day,
|
||||
value.hour,
|
||||
value.minute,
|
||||
value.second,
|
||||
)
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)
|
||||
}
|
||||
}
|
|
@ -1,504 +0,0 @@
|
|||
--[[
|
||||
NOTE: We export a couple different DateTimeValues types below to ensure
|
||||
that types are completely accurate, for method args milliseconds will
|
||||
always be optional, but for return values millis are always included
|
||||
|
||||
If we figure out some better strategy here where we can
|
||||
export just a single type while maintaining accuracy we
|
||||
can change to that in a future breaking semver release
|
||||
]]
|
||||
|
||||
type OptionalMillisecond = {
|
||||
millisecond: number?,
|
||||
}
|
||||
|
||||
type Millisecond = {
|
||||
millisecond: number,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface Locale
|
||||
@within DateTime
|
||||
|
||||
Enum type representing supported DateTime locales.
|
||||
|
||||
Currently supported locales are:
|
||||
|
||||
- `en` - English
|
||||
- `de` - German
|
||||
- `es` - Spanish
|
||||
- `fr` - French
|
||||
- `it` - Italian
|
||||
- `ja` - Japanese
|
||||
- `pl` - Polish
|
||||
- `pt-br` - Brazilian Portuguese
|
||||
- `pt` - Portuguese
|
||||
- `tr` - Turkish
|
||||
]=]
|
||||
export type Locale = "en" | "de" | "es" | "fr" | "it" | "ja" | "pl" | "pt-br" | "pt" | "tr"
|
||||
|
||||
--[=[
|
||||
@interface DateTimeValues
|
||||
@within DateTime
|
||||
|
||||
Individual date & time values, representing the primitives that make up a `DateTime`.
|
||||
|
||||
This is a dictionary that will contain the following values:
|
||||
|
||||
- `year` - Year(s), in the range 1400 -> 9999
|
||||
- `month` - Month(s), in the range 1 -> 12
|
||||
- `day` - Day(s), in the range 1 -> 31
|
||||
- `hour` - Hour(s), in the range 0 -> 23
|
||||
- `minute` - Minute(s), in the range 0 -> 59
|
||||
- `second` - Second(s), in the range 0 -> 60, where 60 is a leap second
|
||||
|
||||
An additional `millisecond` value may also be included,
|
||||
and should be within the range `0 -> 999`, but is optional.
|
||||
|
||||
However, any method returning this type should be guaranteed
|
||||
to include milliseconds - see individual methods to verify.
|
||||
]=]
|
||||
export type DateTimeValues = {
|
||||
year: number,
|
||||
month: number,
|
||||
day: number,
|
||||
hour: number,
|
||||
minute: number,
|
||||
second: number,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface DateTimeValueArguments
|
||||
@within DateTime
|
||||
|
||||
Alias for `DateTimeValues` with an optional `millisecond` value.
|
||||
|
||||
Refer to the `DateTimeValues` documentation for additional information.
|
||||
]=]
|
||||
export type DateTimeValueArguments = DateTimeValues & OptionalMillisecond
|
||||
|
||||
--[=[
|
||||
@interface DateTimeValueReturns
|
||||
@within DateTime
|
||||
|
||||
Alias for `DateTimeValues` with a mandatory `millisecond` value.
|
||||
|
||||
Refer to the `DateTimeValues` documentation for additional information.
|
||||
]=]
|
||||
export type DateTimeValueReturns = DateTimeValues & Millisecond
|
||||
|
||||
--[=[
|
||||
@prop unixTimestamp number
|
||||
@within DateTime
|
||||
Number of seconds passed since the UNIX epoch.
|
||||
]=]
|
||||
|
||||
--[=[
|
||||
@prop unixTimestampMillis number
|
||||
@within DateTime
|
||||
Number of milliseconds passed since the UNIX epoch.
|
||||
]=]
|
||||
local DateTime = {
|
||||
unixTimestamp = (nil :: any) :: number,
|
||||
unixTimestampMillis = (nil :: any) :: number,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Method
|
||||
|
||||
Formats this `DateTime` using the given `formatString` and `locale`, as local time.
|
||||
|
||||
The given `formatString` is parsed using a `strftime`/`strptime`-inspired
|
||||
date and time formatting syntax, allowing tokens such as the following:
|
||||
|
||||
| Token | Example | Description |
|
||||
|-------|----------|---------------|
|
||||
| `%Y` | `1998` | Year number |
|
||||
| `%m` | `04` | Month number |
|
||||
| `%d` | `29` | Day number |
|
||||
| `%A` | `Monday` | Weekday name |
|
||||
| `%M` | `59` | Minute number |
|
||||
| `%S` | `10` | Second number |
|
||||
|
||||
For a full reference of all available tokens, see the
|
||||
[chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
|
||||
|
||||
If not provided, `formatString` and `locale` will default
|
||||
to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively.
|
||||
|
||||
@param formatString -- A string containing formatting tokens
|
||||
@param locale -- The locale the time should be formatted in
|
||||
@return string -- The formatting string
|
||||
]=]
|
||||
function DateTime.formatLocalTime(self: DateTime, formatString: string?, locale: Locale?): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Method
|
||||
|
||||
Formats this `DateTime` using the given `formatString` and `locale`, as UTC (universal) time.
|
||||
|
||||
The given `formatString` is parsed using a `strftime`/`strptime`-inspired
|
||||
date and time formatting syntax, allowing tokens such as the following:
|
||||
|
||||
| Token | Example | Description |
|
||||
|-------|----------|---------------|
|
||||
| `%Y` | `1998` | Year number |
|
||||
| `%m` | `04` | Month number |
|
||||
| `%d` | `29` | Day number |
|
||||
| `%A` | `Monday` | Weekday name |
|
||||
| `%M` | `59` | Minute number |
|
||||
| `%S` | `10` | Second number |
|
||||
|
||||
For a full reference of all available tokens, see the
|
||||
[chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html).
|
||||
|
||||
If not provided, `formatString` and `locale` will default
|
||||
to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively.
|
||||
|
||||
@param formatString -- A string containing formatting tokens
|
||||
@param locale -- The locale the time should be formatted in
|
||||
@return string -- The formatting string
|
||||
]=]
|
||||
function DateTime.formatUniversalTime(self: DateTime, formatString: string?, locale: Locale?): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Method
|
||||
|
||||
**DEPRECATED**: Use `DateTime.toRfc3339` instead.
|
||||
|
||||
Formats this `DateTime` as an ISO 8601 date-time string.
|
||||
|
||||
Some examples of ISO 8601 date-time strings are:
|
||||
|
||||
- `2020-02-22T18:12:08Z`
|
||||
- `2000-01-31T12:34:56+05:00`
|
||||
- `1970-01-01T00:00:00.055Z`
|
||||
|
||||
@return string -- The ISO 8601 formatted string
|
||||
]=]
|
||||
function DateTime.toIsoDate(self: DateTime): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Method
|
||||
|
||||
Formats this `DateTime` as an RFC 2822 date-time string.
|
||||
|
||||
Some examples of RFC 2822 date-time strings are:
|
||||
|
||||
- `Fri, 21 Nov 1997 09:55:06 -0600`
|
||||
- `Tue, 1 Jul 2003 10:52:37 +0200`
|
||||
- `Mon, 23 Dec 2024 01:58:48 GMT`
|
||||
|
||||
@return string -- The RFC 2822 formatted string
|
||||
]=]
|
||||
function DateTime.toRfc2822(self: DateTime): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Method
|
||||
|
||||
Formats this `DateTime` as an RFC 3339 date-time string.
|
||||
|
||||
Some examples of RFC 3339 date-time strings are:
|
||||
|
||||
- `2020-02-22T18:12:08Z`
|
||||
- `2000-01-31T12:34:56+05:00`
|
||||
- `1970-01-01T00:00:00.055Z`
|
||||
|
||||
@return string -- The RFC 3339 formatted string
|
||||
]=]
|
||||
function DateTime.toRfc3339(self: DateTime): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Method
|
||||
|
||||
Extracts separated local date & time values from this `DateTime`.
|
||||
|
||||
The returned table contains the following values:
|
||||
|
||||
| Key | Type | Range |
|
||||
|---------------|----------|----------------|
|
||||
| `year` | `number` | `1400 -> 9999` |
|
||||
| `month` | `number` | `1 -> 12` |
|
||||
| `day` | `number` | `1 -> 31` |
|
||||
| `hour` | `number` | `0 -> 23` |
|
||||
| `minute` | `number` | `0 -> 59` |
|
||||
| `second` | `number` | `0 -> 60` |
|
||||
| `millisecond` | `number` | `0 -> 999` |
|
||||
|
||||
@return DateTimeValueReturns -- A table of DateTime values
|
||||
]=]
|
||||
function DateTime.toLocalTime(self: DateTime): DateTimeValueReturns
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Method
|
||||
|
||||
Extracts separated UTC (universal) date & time values from this `DateTime`.
|
||||
|
||||
The returned table contains the following values:
|
||||
|
||||
| Key | Type | Range |
|
||||
|---------------|----------|----------------|
|
||||
| `year` | `number` | `1400 -> 9999` |
|
||||
| `month` | `number` | `1 -> 12` |
|
||||
| `day` | `number` | `1 -> 31` |
|
||||
| `hour` | `number` | `0 -> 23` |
|
||||
| `minute` | `number` | `0 -> 59` |
|
||||
| `second` | `number` | `0 -> 60` |
|
||||
| `millisecond` | `number` | `0 -> 999` |
|
||||
|
||||
@return DateTimeValueReturns -- A table of DateTime values
|
||||
]=]
|
||||
function DateTime.toUniversalTime(self: DateTime): DateTimeValueReturns
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
export type DateTime = typeof(DateTime)
|
||||
|
||||
--[=[
|
||||
@class DateTime
|
||||
|
||||
Built-in library for date & time
|
||||
|
||||
### Example usage
|
||||
|
||||
```lua
|
||||
local DateTime = require("@lune/datetime")
|
||||
|
||||
-- Creates a DateTime for the current exact moment in time
|
||||
local now = DateTime.now()
|
||||
|
||||
-- Formats the current moment in time as an RFC 3339 string
|
||||
print(now:toRfc3339())
|
||||
|
||||
-- Formats the current moment in time as an RFC 2822 string
|
||||
print(now:toRfc2822())
|
||||
|
||||
-- Formats the current moment in time, using the local
|
||||
-- time, the French locale, and the specified time string
|
||||
print(now:formatLocalTime("%A, %d %B %Y", "fr"))
|
||||
|
||||
-- Returns a specific moment in time as a DateTime instance
|
||||
local someDayInTheFuture = DateTime.fromLocalTime({
|
||||
year = 3033,
|
||||
month = 8,
|
||||
day = 26,
|
||||
hour = 16,
|
||||
minute = 56,
|
||||
second = 28,
|
||||
millisecond = 892,
|
||||
})
|
||||
|
||||
-- Extracts the current local date & time as separate values (same values as above table)
|
||||
print(now:toLocalTime())
|
||||
|
||||
-- Returns a DateTime instance from a given float, where the whole
|
||||
-- denotes the seconds and the fraction denotes the milliseconds
|
||||
-- Note that the fraction for millis here is completely optional
|
||||
DateTime.fromUnixTimestamp(871978212313.321)
|
||||
|
||||
-- Extracts the current universal (UTC) date & time as separate values
|
||||
print(now:toUniversalTime())
|
||||
```
|
||||
]=]
|
||||
local dateTime = {}
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Constructor
|
||||
|
||||
Returns a `DateTime` representing the current moment in time.
|
||||
|
||||
@return DateTime -- The new DateTime object
|
||||
]=]
|
||||
function dateTime.now(): DateTime
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Constructor
|
||||
|
||||
Creates a new `DateTime` from the given UNIX timestamp.
|
||||
|
||||
This timestamp may contain both a whole and fractional part -
|
||||
where the fractional part denotes milliseconds / nanoseconds.
|
||||
|
||||
Example usage of fractions:
|
||||
|
||||
- `DateTime.fromUnixTimestamp(123456789.001)` - one millisecond
|
||||
- `DateTime.fromUnixTimestamp(123456789.000000001)` - one nanosecond
|
||||
|
||||
Note that the fractional part has limited precision down to exactly
|
||||
one nanosecond, any fraction that is more precise will get truncated.
|
||||
|
||||
@param unixTimestamp -- Seconds passed since the UNIX epoch
|
||||
@return DateTime -- The new DateTime object
|
||||
]=]
|
||||
function dateTime.fromUnixTimestamp(unixTimestamp: number): DateTime
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Constructor
|
||||
|
||||
Creates a new `DateTime` from the given date & time values table, in universal (UTC) time.
|
||||
|
||||
The given table must contain the following values:
|
||||
|
||||
| Key | Type | Range |
|
||||
|----------|----------|----------------|
|
||||
| `year` | `number` | `1400 -> 9999` |
|
||||
| `month` | `number` | `1 -> 12` |
|
||||
| `day` | `number` | `1 -> 31` |
|
||||
| `hour` | `number` | `0 -> 23` |
|
||||
| `minute` | `number` | `0 -> 59` |
|
||||
| `second` | `number` | `0 -> 60` |
|
||||
|
||||
An additional `millisecond` value may also be included,
|
||||
and should be within the range `0 -> 999`, but is optional.
|
||||
|
||||
Any non-integer values in the given table will be rounded down.
|
||||
|
||||
### Errors
|
||||
|
||||
This constructor is fallible and may throw an error in the following situations:
|
||||
|
||||
- Date units (year, month, day) were given that produce an invalid date. For example, January 32nd or February 29th on a non-leap year.
|
||||
|
||||
@param values -- Table containing date & time values
|
||||
@return DateTime -- The new DateTime object
|
||||
]=]
|
||||
function dateTime.fromUniversalTime(values: DateTimeValueArguments): DateTime
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Constructor
|
||||
|
||||
Creates a new `DateTime` from the given date & time values table, in local time.
|
||||
|
||||
The given table must contain the following values:
|
||||
|
||||
| Key | Type | Range |
|
||||
|----------|----------|----------------|
|
||||
| `year` | `number` | `1400 -> 9999` |
|
||||
| `month` | `number` | `1 -> 12` |
|
||||
| `day` | `number` | `1 -> 31` |
|
||||
| `hour` | `number` | `0 -> 23` |
|
||||
| `minute` | `number` | `0 -> 59` |
|
||||
| `second` | `number` | `0 -> 60` |
|
||||
|
||||
An additional `millisecond` value may also be included,
|
||||
and should be within the range `0 -> 999`, but is optional.
|
||||
|
||||
Any non-integer values in the given table will be rounded down.
|
||||
|
||||
### Errors
|
||||
|
||||
This constructor is fallible and may throw an error in the following situations:
|
||||
|
||||
- Date units (year, month, day) were given that produce an invalid date. For example, January 32nd or February 29th on a non-leap year.
|
||||
|
||||
@param values -- Table containing date & time values
|
||||
@return DateTime -- The new DateTime object
|
||||
]=]
|
||||
function dateTime.fromLocalTime(values: DateTimeValueArguments): DateTime
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Constructor
|
||||
|
||||
**DEPRECATED**: Use `DateTime.fromRfc3339` instead.
|
||||
|
||||
Creates a new `DateTime` from an ISO 8601 date-time string.
|
||||
|
||||
### Errors
|
||||
|
||||
This constructor is fallible and may throw an error if the given
|
||||
string does not strictly follow the ISO 8601 date-time string format.
|
||||
|
||||
Some examples of valid ISO 8601 date-time strings are:
|
||||
|
||||
- `2020-02-22T18:12:08Z`
|
||||
- `2000-01-31T12:34:56+05:00`
|
||||
- `1970-01-01T00:00:00.055Z`
|
||||
|
||||
@param isoDate -- An ISO 8601 formatted string
|
||||
@return DateTime -- The new DateTime object
|
||||
]=]
|
||||
function dateTime.fromIsoDate(isoDate: string): DateTime
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Constructor
|
||||
|
||||
Creates a new `DateTime` from an RFC 3339 date-time string.
|
||||
|
||||
### Errors
|
||||
|
||||
This constructor is fallible and may throw an error if the given
|
||||
string does not strictly follow the RFC 3339 date-time string format.
|
||||
|
||||
Some examples of valid RFC 3339 date-time strings are:
|
||||
|
||||
- `2020-02-22T18:12:08Z`
|
||||
- `2000-01-31T12:34:56+05:00`
|
||||
- `1970-01-01T00:00:00.055Z`
|
||||
|
||||
@param rfc3339Date -- An RFC 3339 formatted string
|
||||
@return DateTime -- The new DateTime object
|
||||
]=]
|
||||
function dateTime.fromRfc3339(rfc3339Date: string): DateTime
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within DateTime
|
||||
@tag Constructor
|
||||
|
||||
Creates a new `DateTime` from an RFC 2822 date-time string.
|
||||
|
||||
### Errors
|
||||
|
||||
This constructor is fallible and may throw an error if the given
|
||||
string does not strictly follow the RFC 2822 date-time string format.
|
||||
|
||||
Some examples of valid RFC 2822 date-time strings are:
|
||||
|
||||
- `Fri, 21 Nov 1997 09:55:06 -0600`
|
||||
- `Tue, 1 Jul 2003 10:52:37 +0200`
|
||||
- `Mon, 23 Dec 2024 01:58:48 GMT`
|
||||
|
||||
@param rfc2822Date -- An RFC 2822 formatted string
|
||||
@return DateTime -- The new DateTime object
|
||||
]=]
|
||||
function dateTime.fromRfc2822(rfc2822Date: string): DateTime
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
return dateTime
|
|
@ -1,23 +0,0 @@
|
|||
[package]
|
||||
name = "lune-std-fs"
|
||||
version = "0.2.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.10.3", features = ["luau"] }
|
||||
|
||||
async-fs = "2.1"
|
||||
bstr = "1.9"
|
||||
futures-lite = "2.6"
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|
||||
lune-std-datetime = { version = "0.2.2", path = "../lune-std-datetime" }
|
|
@ -1,18 +0,0 @@
|
|||
[package]
|
||||
name = "lune-std-luau"
|
||||
version = "0.2.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.10.3", features = ["luau", "luau-jit"] }
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|
|
@ -1,95 +0,0 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::{jit::JitEnablement, TableBuilder};
|
||||
|
||||
mod options;
|
||||
|
||||
use self::options::{LuauCompileOptions, LuauLoadOptions};
|
||||
|
||||
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
|
||||
|
||||
/**
|
||||
Returns a string containing type definitions for the `luau` standard library.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn typedefs() -> String {
|
||||
TYPEDEFS.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
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,
|
||||
(source, options): (LuaString, LuauCompileOptions),
|
||||
) -> LuaResult<LuaString> {
|
||||
options
|
||||
.into_compiler()
|
||||
.compile(source.as_bytes())
|
||||
.and_then(|s| lua.create_string(s))
|
||||
}
|
||||
|
||||
fn load_source(
|
||||
lua: &Lua,
|
||||
(source, options): (LuaString, LuauLoadOptions),
|
||||
) -> LuaResult<LuaFunction> {
|
||||
let mut chunk = lua
|
||||
.load(source.as_bytes().to_vec())
|
||||
.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().metatable() {
|
||||
environment.set_metatable(Some(global_metatable));
|
||||
}
|
||||
} else if let Some(custom_metatable) = custom_environment.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::<JitEnablement>()
|
||||
.ok_or(LuaError::runtime(
|
||||
"Failed to get current JitStatus ref from AppData",
|
||||
))?
|
||||
.enabled(),
|
||||
);
|
||||
|
||||
Ok(function)
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
#![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,
|
||||
pub(crate) debug_level: u8,
|
||||
}
|
||||
|
||||
impl LuauCompileOptions {
|
||||
pub fn into_compiler(self) -> LuaCompiler {
|
||||
LuaCompiler::default()
|
||||
.set_optimization_level(self.optimization_level)
|
||||
.set_coverage_level(self.coverage_level)
|
||||
.set_debug_level(self.debug_level)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LuauCompileOptions {
|
||||
fn default() -> Self {
|
||||
// NOTE: This is the same as LuaCompiler::default() values, but they are
|
||||
// not accessible from outside of mlua so we need to recreate them here.
|
||||
Self {
|
||||
optimization_level: 1,
|
||||
coverage_level: 0,
|
||||
debug_level: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for LuauCompileOptions {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
Ok(match value {
|
||||
LuaValue::Nil => Self::default(),
|
||||
LuaValue::Table(t) => {
|
||||
let mut options = Self::default();
|
||||
|
||||
let get_and_check = |name: &'static str| -> LuaResult<Option<u8>> {
|
||||
match t.get(name)? {
|
||||
Some(n @ (0..=2)) => Ok(Some(n)),
|
||||
Some(n) => Err(LuaError::runtime(format!(
|
||||
"'{name}' must be one of: 0, 1, or 2 - got {n}"
|
||||
))),
|
||||
None => Ok(None),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(optimization_level) = get_and_check("optimizationLevel")? {
|
||||
options.optimization_level = optimization_level;
|
||||
}
|
||||
if let Some(coverage_level) = get_and_check("coverageLevel")? {
|
||||
options.coverage_level = coverage_level;
|
||||
}
|
||||
if let Some(debug_level) = get_and_check("debugLevel")? {
|
||||
options.debug_level = debug_level;
|
||||
}
|
||||
|
||||
options
|
||||
}
|
||||
_ => {
|
||||
return Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "CompileOptions".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid compile options - expected table, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LuauLoadOptions {
|
||||
pub(crate) debug_name: String,
|
||||
pub(crate) environment: Option<LuaTable>,
|
||||
pub(crate) inject_globals: bool,
|
||||
pub(crate) codegen_enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for LuauLoadOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
debug_name: DEFAULT_DEBUG_NAME.to_string(),
|
||||
environment: None,
|
||||
inject_globals: true,
|
||||
codegen_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for LuauLoadOptions {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
Ok(match value {
|
||||
LuaValue::Nil => Self::default(),
|
||||
LuaValue::Table(t) => {
|
||||
let mut options = Self::default();
|
||||
|
||||
if let Some(debug_name) = t.get("debugName")? {
|
||||
options.debug_name = debug_name;
|
||||
}
|
||||
|
||||
if let Some(environment) = t.get("environment")? {
|
||||
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 {
|
||||
from: value.type_name(),
|
||||
to: "LoadOptions".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid load options - expected string or table, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
--[=[
|
||||
@interface CompileOptions
|
||||
@within Luau
|
||||
|
||||
The options passed to the luau compiler while compiling bytecode.
|
||||
|
||||
This is a dictionary that may contain one or more of the following values:
|
||||
|
||||
* `optimizationLevel` - Sets the compiler option "optimizationLevel". Defaults to `1`.
|
||||
* `coverageLevel` - Sets the compiler option "coverageLevel". Defaults to `0`.
|
||||
* `debugLevel` - Sets the compiler option "debugLevel". Defaults to `1`.
|
||||
|
||||
Documentation regarding what these values represent can be found [here](https://github.com/Roblox/luau/blob/bd229816c0a82a8590395416c81c333087f541fd/Compiler/include/luacode.h#L13-L39).
|
||||
]=]
|
||||
export type CompileOptions = {
|
||||
optimizationLevel: number?,
|
||||
coverageLevel: number?,
|
||||
debugLevel: number?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface LoadOptions
|
||||
@within Luau
|
||||
|
||||
The options passed while loading a luau chunk from an arbitrary string, or bytecode.
|
||||
|
||||
This is a dictionary that may contain one or more of the following values:
|
||||
|
||||
* `debugName` - The debug name of the closure. Defaults to `luau.load(...)`.
|
||||
* `environment` - A custom environment to load the chunk in. Setting a custom environment will deoptimize the chunk and forcefully disable codegen. Defaults to the global environment.
|
||||
* `injectGlobals` - Whether or not to inject globals in the custom environment. Has no effect if no custom environment is provided. Defaults to `true`.
|
||||
* `codegenEnabled` - Whether or not to enable codegen. Defaults to `false`.
|
||||
]=]
|
||||
export type LoadOptions = {
|
||||
debugName: string?,
|
||||
environment: { [string]: any }?,
|
||||
injectGlobals: boolean?,
|
||||
codegenEnabled: boolean?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@class Luau
|
||||
|
||||
Built-in library for generating luau bytecode & functions.
|
||||
|
||||
### Example usage
|
||||
|
||||
```lua
|
||||
local luau = require("@lune/luau")
|
||||
|
||||
local bytecode = luau.compile("print('Hello, World!')")
|
||||
local callableFn = luau.load(bytecode)
|
||||
|
||||
-- Additionally, we can skip the bytecode generation and load a callable function directly from the code itself.
|
||||
-- local callableFn = luau.load("print('Hello, World!')")
|
||||
|
||||
callableFn()
|
||||
```
|
||||
|
||||
Since luau bytecode is highly compressible, it may also make sense to compress it using the `serde` library
|
||||
while transmitting large amounts of it.
|
||||
]=]
|
||||
local luau = {}
|
||||
|
||||
--[=[
|
||||
@within Luau
|
||||
|
||||
Compiles sourcecode into Luau bytecode
|
||||
|
||||
An error will be thrown if the sourcecode given isn't valid Luau code.
|
||||
|
||||
### Example usage
|
||||
|
||||
```lua
|
||||
local luau = require("@lune/luau")
|
||||
|
||||
-- Compile the source to some highly optimized bytecode
|
||||
local bytecode = luau.compile("print('Hello, World!')", {
|
||||
optimizationLevel = 2,
|
||||
coverageLevel = 0,
|
||||
debugLevel = 1,
|
||||
})
|
||||
```
|
||||
|
||||
@param source The string that will be compiled into bytecode
|
||||
@param compileOptions The options passed to the luau compiler that will output the bytecode
|
||||
|
||||
@return luau bytecode
|
||||
]=]
|
||||
function luau.compile(source: string, compileOptions: CompileOptions?): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within Luau
|
||||
|
||||
Generates a function from either bytecode or sourcecode
|
||||
|
||||
An error will be thrown if the sourcecode given isn't valid luau code.
|
||||
|
||||
### Example usage
|
||||
|
||||
```lua
|
||||
local luau = require("@lune/luau")
|
||||
|
||||
local bytecode = luau.compile("print('Hello, World!')")
|
||||
local callableFn = luau.load(bytecode, {
|
||||
debugName = "'Hello, World'"
|
||||
})
|
||||
|
||||
callableFn()
|
||||
```
|
||||
|
||||
@param source Either luau bytecode or string source code
|
||||
@param loadOptions The options passed to luau for loading the chunk
|
||||
|
||||
@return luau chunk
|
||||
]=]
|
||||
function luau.load(source: string, loadOptions: LoadOptions?): (...any) -> ...any
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
return luau
|
|
@ -1,42 +0,0 @@
|
|||
[package]
|
||||
name = "lune-std-net"
|
||||
version = "0.2.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.10.3", features = ["luau"] }
|
||||
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
|
||||
|
||||
async-channel = "2.3"
|
||||
async-executor = "1.13"
|
||||
async-io = "2.4"
|
||||
async-lock = "3.4"
|
||||
async-net = "2.0"
|
||||
async-tungstenite = "0.29"
|
||||
blocking = "1.6"
|
||||
bstr = "1.9"
|
||||
form_urlencoded = "1.2"
|
||||
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||
futures-lite = "2.6"
|
||||
futures-rustls = "0.26"
|
||||
http-body-util = "0.1"
|
||||
hyper = { version = "1.6", default-features = false, features = ["http1", "client", "server"] }
|
||||
pin-project-lite = "0.2"
|
||||
rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"] }
|
||||
rustls-pki-types = "1.11"
|
||||
url = "2.5"
|
||||
urlencoding = "2.1"
|
||||
webpki = "0.22"
|
||||
webpki-roots = "0.26"
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|
||||
lune-std-serde = { version = "0.2.2", path = "../lune-std-serde" }
|
|
@ -1,59 +0,0 @@
|
|||
use hyper::body::{Buf, Bytes};
|
||||
|
||||
use super::inner::ReadableBodyInner;
|
||||
|
||||
/**
|
||||
The cursor keeping track of inner data and its position for a readable body.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReadableBodyCursor {
|
||||
inner: ReadableBodyInner,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
impl ReadableBodyCursor {
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.inner.as_slice()[self.start..]
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, cnt: usize) {
|
||||
self.start += cnt;
|
||||
if self.start > self.inner.len() {
|
||||
self.start = self.inner.len();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Bytes {
|
||||
self.inner.into_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Buf for ReadableBodyCursor {
|
||||
fn remaining(&self) -> usize {
|
||||
self.len().saturating_sub(self.start)
|
||||
}
|
||||
|
||||
fn chunk(&self) -> &[u8] {
|
||||
self.as_slice()
|
||||
}
|
||||
|
||||
fn advance(&mut self, cnt: usize) {
|
||||
self.advance(cnt);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for ReadableBodyCursor
|
||||
where
|
||||
T: Into<ReadableBodyInner>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self {
|
||||
inner: value.into(),
|
||||
start: 0,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
use http_body_util::BodyExt;
|
||||
use hyper::{
|
||||
body::{Bytes, Incoming},
|
||||
header::CONTENT_ENCODING,
|
||||
HeaderMap,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_std_serde::{decompress, CompressDecompressFormat};
|
||||
|
||||
pub async fn handle_incoming_body(
|
||||
headers: &HeaderMap,
|
||||
body: Incoming,
|
||||
should_decompress: bool,
|
||||
) -> LuaResult<(Bytes, bool)> {
|
||||
let mut body = body.collect().await.into_lua_err()?.to_bytes();
|
||||
|
||||
let was_decompressed = if should_decompress {
|
||||
let decompress_format = headers
|
||||
.get(CONTENT_ENCODING)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(CompressDecompressFormat::detect_from_header_str);
|
||||
if let Some(format) = decompress_format {
|
||||
body = Bytes::from(decompress(body, format).await?);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok((body, was_decompressed))
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
use hyper::body::{Buf as _, Bytes};
|
||||
use mlua::{prelude::*, Buffer as LuaBuffer};
|
||||
|
||||
/**
|
||||
The inner data for a readable body.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ReadableBodyInner {
|
||||
Bytes(Bytes),
|
||||
String(String),
|
||||
LuaString(LuaString),
|
||||
LuaBuffer(LuaBuffer),
|
||||
}
|
||||
|
||||
impl ReadableBodyInner {
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::Bytes(b) => b.len(),
|
||||
Self::String(s) => s.len(),
|
||||
Self::LuaString(s) => s.as_bytes().len(),
|
||||
Self::LuaBuffer(b) => b.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
/*
|
||||
SAFETY: Reading lua strings and lua buffers as raw slices is safe while we can
|
||||
guarantee that the inner Lua value + main lua struct has not yet been dropped
|
||||
|
||||
1. Buffers are fixed-size and guaranteed to never resize
|
||||
2. We do not expose any method for writing to the body, only reading
|
||||
3. We guarantee that net.request and net.serve futures are only driven forward
|
||||
while we also know that the Lua + scheduler pair have not yet been dropped
|
||||
4. Any writes from within lua to a buffer, are considered user error,
|
||||
and are not unsafe, since the only possible outcome with the above
|
||||
guarantees is invalid / mangled contents in request / response bodies
|
||||
*/
|
||||
match self {
|
||||
Self::Bytes(b) => b.chunk(),
|
||||
Self::String(s) => s.as_bytes(),
|
||||
Self::LuaString(s) => unsafe {
|
||||
// BorrowedBytes would not let us return a plain slice here,
|
||||
// which is what the Buf implementation below needs - we need to
|
||||
// do a little hack here to re-create the slice without a lifetime
|
||||
let b = s.as_bytes();
|
||||
|
||||
let ptr = b.as_ptr();
|
||||
let len = b.len();
|
||||
|
||||
std::slice::from_raw_parts(ptr, len)
|
||||
},
|
||||
Self::LuaBuffer(b) => unsafe {
|
||||
// Similar to above, we need to get the raw slice for the buffer,
|
||||
// which is a bit trickier here because Buffer has a read + write
|
||||
// interface instead of using slices for some unknown reason
|
||||
let v = LuaValue::Buffer(b.clone());
|
||||
|
||||
let ptr = v.to_pointer().cast::<u8>();
|
||||
let len = b.len();
|
||||
|
||||
std::slice::from_raw_parts(ptr, len)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Bytes {
|
||||
match self {
|
||||
Self::Bytes(b) => b,
|
||||
Self::String(s) => Bytes::from(s),
|
||||
Self::LuaString(s) => Bytes::from(s.as_bytes().to_vec()),
|
||||
Self::LuaBuffer(b) => Bytes::from(b.to_vec()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for ReadableBodyInner {
|
||||
fn from(value: &'static str) -> Self {
|
||||
Self::Bytes(Bytes::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for ReadableBodyInner {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self::Bytes(Bytes::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for ReadableBodyInner {
|
||||
fn from(value: Bytes) -> Self {
|
||||
Self::Bytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ReadableBodyInner {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LuaString> for ReadableBodyInner {
|
||||
fn from(value: LuaString) -> Self {
|
||||
Self::LuaString(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LuaBuffer> for ReadableBodyInner {
|
||||
fn from(value: LuaBuffer) -> Self {
|
||||
Self::LuaBuffer(value)
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
#![allow(unused_imports)]
|
||||
|
||||
mod cursor;
|
||||
mod incoming;
|
||||
mod inner;
|
||||
mod readable;
|
||||
|
||||
pub use self::cursor::ReadableBodyCursor;
|
||||
pub use self::incoming::handle_incoming_body;
|
||||
pub use self::inner::ReadableBodyInner;
|
||||
pub use self::readable::ReadableBody;
|
|
@ -1,105 +0,0 @@
|
|||
use std::convert::Infallible;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use hyper::body::{Body, Bytes, Frame, SizeHint};
|
||||
use mlua::prelude::*;
|
||||
|
||||
use super::cursor::ReadableBodyCursor;
|
||||
|
||||
/**
|
||||
Zero-copy wrapper for a readable body.
|
||||
|
||||
Provides methods to read bytes that can be safely used if, and only
|
||||
if, the respective Lua struct for the body has not yet been dropped.
|
||||
|
||||
If the body was created from a `Vec<u8>`, `Bytes`, or a `String`, reading
|
||||
bytes is always safe and does not go through any additional indirections.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReadableBody {
|
||||
cursor: Option<ReadableBodyCursor>,
|
||||
}
|
||||
|
||||
impl ReadableBody {
|
||||
pub const fn empty() -> Self {
|
||||
Self { cursor: None }
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
match self.cursor.as_ref() {
|
||||
Some(cursor) => cursor.as_slice(),
|
||||
None => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Bytes {
|
||||
match self.cursor {
|
||||
Some(cursor) => cursor.into_bytes(),
|
||||
None => Bytes::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body for ReadableBody {
|
||||
type Data = ReadableBodyCursor;
|
||||
type Error = Infallible;
|
||||
|
||||
fn poll_frame(
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
Poll::Ready(self.cursor.take().map(|d| Ok(Frame::data(d))))
|
||||
}
|
||||
|
||||
fn is_end_stream(&self) -> bool {
|
||||
self.cursor.is_none()
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> SizeHint {
|
||||
self.cursor.as_ref().map_or_else(
|
||||
|| SizeHint::with_exact(0),
|
||||
|c| SizeHint::with_exact(c.len() as u64),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for ReadableBody
|
||||
where
|
||||
T: Into<ReadableBodyCursor>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self {
|
||||
cursor: Some(value.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for ReadableBody
|
||||
where
|
||||
T: Into<ReadableBodyCursor>,
|
||||
{
|
||||
fn from(value: Option<T>) -> Self {
|
||||
Self {
|
||||
cursor: value.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for ReadableBody {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
match value {
|
||||
LuaValue::Nil => Ok(Self::empty()),
|
||||
LuaValue::String(str) => Ok(Self::from(str)),
|
||||
LuaValue::Buffer(buf) => Ok(Self::from(buf)),
|
||||
v => Err(LuaError::FromLuaConversionError {
|
||||
from: v.type_name(),
|
||||
to: "Body".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid body - expected string or buffer, got {}",
|
||||
v.type_name()
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use async_net::TcpStream;
|
||||
use futures_lite::prelude::*;
|
||||
use futures_rustls::{TlsConnector, TlsStream};
|
||||
use rustls_pki_types::ServerName;
|
||||
use url::Url;
|
||||
|
||||
use crate::client::rustls::CLIENT_CONFIG;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HttpStream {
|
||||
Plain(TcpStream),
|
||||
Tls(TlsStream<TcpStream>),
|
||||
}
|
||||
|
||||
impl HttpStream {
|
||||
pub async fn connect(url: Url) -> Result<Self, io::Error> {
|
||||
let Some(host) = url.host() else {
|
||||
return Err(make_err("unknown or missing host"));
|
||||
};
|
||||
let Some(port) = url.port_or_known_default() else {
|
||||
return Err(make_err("unknown or missing port"));
|
||||
};
|
||||
|
||||
let use_tls = match url.scheme() {
|
||||
"http" => false,
|
||||
"https" => true,
|
||||
s => return Err(make_err(format!("unsupported scheme: {s}"))),
|
||||
};
|
||||
|
||||
let host = host.to_string();
|
||||
let stream = TcpStream::connect((host.clone(), port)).await?;
|
||||
|
||||
let stream = if use_tls {
|
||||
let servname = ServerName::try_from(host).map_err(make_err)?.to_owned();
|
||||
let connector = TlsConnector::from(Arc::clone(&CLIENT_CONFIG));
|
||||
let stream = connector.connect(servname, stream).await?;
|
||||
Self::Tls(TlsStream::Client(stream))
|
||||
} else {
|
||||
Self::Plain(stream)
|
||||
};
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for HttpStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
match &mut *self {
|
||||
HttpStream::Plain(stream) => Pin::new(stream).poll_read(cx, buf),
|
||||
HttpStream::Tls(stream) => Pin::new(stream).poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for HttpStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
match &mut *self {
|
||||
HttpStream::Plain(stream) => Pin::new(stream).poll_write(cx, buf),
|
||||
HttpStream::Tls(stream) => Pin::new(stream).poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
match &mut *self {
|
||||
HttpStream::Plain(stream) => Pin::new(stream).poll_close(cx),
|
||||
HttpStream::Tls(stream) => Pin::new(stream).poll_close(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
match &mut *self {
|
||||
HttpStream::Plain(stream) => Pin::new(stream).poll_flush(cx),
|
||||
HttpStream::Tls(stream) => Pin::new(stream).poll_flush(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_err(e: impl ToString) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Other, e.to_string())
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
use http_body_util::Full;
|
||||
use hyper::{
|
||||
body::Incoming,
|
||||
client::conn::http1::handshake,
|
||||
header::{HeaderValue, ACCEPT, CONTENT_LENGTH, HOST, LOCATION, USER_AGENT},
|
||||
Method, Request as HyperRequest, Response as HyperResponse, Uri,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
body::ReadableBody,
|
||||
client::{http_stream::HttpStream, ws_stream::WsStream},
|
||||
shared::{
|
||||
headers::create_user_agent_header,
|
||||
hyper::{HyperExecutor, HyperIo},
|
||||
request::Request,
|
||||
response::Response,
|
||||
websocket::Websocket,
|
||||
},
|
||||
};
|
||||
|
||||
pub mod http_stream;
|
||||
pub mod rustls;
|
||||
pub mod ws_stream;
|
||||
|
||||
const MAX_REDIRECTS: usize = 10;
|
||||
|
||||
/**
|
||||
Connects to a websocket at the given URL.
|
||||
*/
|
||||
pub async fn connect_websocket(url: Url) -> LuaResult<Websocket<WsStream>> {
|
||||
let stream = WsStream::connect(url).await?;
|
||||
Ok(Websocket::from(stream))
|
||||
}
|
||||
|
||||
/**
|
||||
Sends the request and returns the final response.
|
||||
|
||||
This will follow any redirects returned by the server,
|
||||
modifying the request method and body as necessary.
|
||||
*/
|
||||
pub async fn send_request(mut request: Request, lua: Lua) -> LuaResult<Response> {
|
||||
let url = request
|
||||
.inner
|
||||
.uri()
|
||||
.to_string()
|
||||
.parse::<Url>()
|
||||
.into_lua_err()?;
|
||||
|
||||
// Some headers are required by most if not
|
||||
// all servers, make sure those are present...
|
||||
if !request.headers().contains_key(HOST.as_str()) {
|
||||
if let Some(host) = url.host_str() {
|
||||
let host = HeaderValue::from_str(host).into_lua_err()?;
|
||||
request.inner.headers_mut().insert(HOST, host);
|
||||
}
|
||||
}
|
||||
if !request.headers().contains_key(USER_AGENT.as_str()) {
|
||||
let ua = create_user_agent_header(&lua)?;
|
||||
let ua = HeaderValue::from_str(&ua).into_lua_err()?;
|
||||
request.inner.headers_mut().insert(USER_AGENT, ua);
|
||||
}
|
||||
if !request.headers().contains_key(CONTENT_LENGTH.as_str()) && request.method() != Method::GET {
|
||||
let len = request.body().len().to_string();
|
||||
let len = HeaderValue::from_str(&len).into_lua_err()?;
|
||||
request.inner.headers_mut().insert(CONTENT_LENGTH, len);
|
||||
}
|
||||
if !request.headers().contains_key(ACCEPT.as_str()) {
|
||||
let accept = HeaderValue::from_static("*/*");
|
||||
request.inner.headers_mut().insert(ACCEPT, accept);
|
||||
}
|
||||
|
||||
// ... we can now safely continue and send the request
|
||||
loop {
|
||||
let stream = HttpStream::connect(url.clone()).await?;
|
||||
|
||||
let (mut sender, conn) = handshake(HyperIo::from(stream)).await.into_lua_err()?;
|
||||
|
||||
HyperExecutor::execute(lua.clone(), conn);
|
||||
|
||||
let (parts, body) = request.clone_inner().into_parts();
|
||||
let data = HyperRequest::from_parts(parts, Full::new(body.into_bytes()));
|
||||
let incoming = sender.send_request(data).await.into_lua_err()?;
|
||||
|
||||
if let Some((new_method, new_uri)) =
|
||||
check_redirect(request.inner.method().clone(), &incoming)
|
||||
{
|
||||
if request.redirects.is_some_and(|r| r >= MAX_REDIRECTS) {
|
||||
return Err(LuaError::external("Too many redirects"));
|
||||
}
|
||||
|
||||
if new_method == Method::GET {
|
||||
*request.inner.body_mut() = ReadableBody::empty();
|
||||
}
|
||||
|
||||
*request.inner.method_mut() = new_method;
|
||||
*request.inner.uri_mut() = new_uri;
|
||||
|
||||
*request.redirects.get_or_insert_default() += 1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break Response::from_incoming(incoming, request.decompress).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn check_redirect(method: Method, response: &HyperResponse<Incoming>) -> Option<(Method, Uri)> {
|
||||
if !response.status().is_redirection() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let location = response.headers().get(LOCATION)?;
|
||||
let location = location.to_str().ok()?;
|
||||
let location = location.parse().ok()?;
|
||||
|
||||
let method = match response.status().as_u16() {
|
||||
301..=303 => Method::GET,
|
||||
_ => method,
|
||||
};
|
||||
|
||||
Some((method, location))
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, LazyLock,
|
||||
};
|
||||
|
||||
use rustls::{crypto::ring, ClientConfig};
|
||||
|
||||
static PROVIDER_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn initialize_provider() {
|
||||
if !PROVIDER_INITIALIZED.load(Ordering::Relaxed) {
|
||||
PROVIDER_INITIALIZED.store(true, Ordering::Relaxed);
|
||||
// Only errors if already installed, which is fine
|
||||
ring::default_provider().install_default().ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub static CLIENT_CONFIG: LazyLock<Arc<ClientConfig>> = LazyLock::new(|| {
|
||||
initialize_provider();
|
||||
rustls::ClientConfig::builder()
|
||||
.with_root_certificates(rustls::RootCertStore {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||
})
|
||||
.with_no_client_auth()
|
||||
.into()
|
||||
});
|
|
@ -1,114 +0,0 @@
|
|||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use async_net::TcpStream;
|
||||
use async_tungstenite::{
|
||||
tungstenite::{Error as TungsteniteError, Message, Result as TungsteniteResult},
|
||||
WebSocketStream as TungsteniteStream,
|
||||
};
|
||||
use futures::Sink;
|
||||
use futures_lite::prelude::*;
|
||||
use futures_rustls::{TlsConnector, TlsStream};
|
||||
use rustls_pki_types::ServerName;
|
||||
use url::Url;
|
||||
|
||||
use crate::client::rustls::CLIENT_CONFIG;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WsStream {
|
||||
Plain(TungsteniteStream<TcpStream>),
|
||||
Tls(TungsteniteStream<TlsStream<TcpStream>>),
|
||||
}
|
||||
|
||||
impl WsStream {
|
||||
pub async fn connect(url: Url) -> Result<Self, io::Error> {
|
||||
let Some(host) = url.host() else {
|
||||
return Err(make_err("unknown or missing host"));
|
||||
};
|
||||
let Some(port) = url.port_or_known_default() else {
|
||||
return Err(make_err("unknown or missing port"));
|
||||
};
|
||||
|
||||
let use_tls = match url.scheme() {
|
||||
"ws" => false,
|
||||
"wss" => true,
|
||||
s => return Err(make_err(format!("unsupported scheme: {s}"))),
|
||||
};
|
||||
|
||||
let host = host.to_string();
|
||||
let stream = TcpStream::connect((host.clone(), port)).await?;
|
||||
|
||||
let stream = if use_tls {
|
||||
let servname = ServerName::try_from(host).map_err(make_err)?.to_owned();
|
||||
let connector = TlsConnector::from(Arc::clone(&CLIENT_CONFIG));
|
||||
|
||||
let stream = connector.connect(servname, stream).await?;
|
||||
let stream = TlsStream::Client(stream);
|
||||
|
||||
let stream = async_tungstenite::client_async(url.to_string(), stream)
|
||||
.await
|
||||
.map_err(make_err)?
|
||||
.0;
|
||||
Self::Tls(stream)
|
||||
} else {
|
||||
let stream = async_tungstenite::client_async(url.to_string(), stream)
|
||||
.await
|
||||
.map_err(make_err)?
|
||||
.0;
|
||||
Self::Plain(stream)
|
||||
};
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink<Message> for WsStream {
|
||||
type Error = TungsteniteError;
|
||||
|
||||
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
match &mut *self {
|
||||
WsStream::Plain(s) => Pin::new(s).poll_ready(cx),
|
||||
WsStream::Tls(s) => Pin::new(s).poll_ready(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
|
||||
match &mut *self {
|
||||
WsStream::Plain(s) => Pin::new(s).start_send(item),
|
||||
WsStream::Tls(s) => Pin::new(s).start_send(item),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
match &mut *self {
|
||||
WsStream::Plain(s) => Pin::new(s).poll_flush(cx),
|
||||
WsStream::Tls(s) => Pin::new(s).poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
match &mut *self {
|
||||
WsStream::Plain(s) => Pin::new(s).poll_close(cx),
|
||||
WsStream::Tls(s) => Pin::new(s).poll_close(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for WsStream {
|
||||
type Item = TungsteniteResult<Message>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match &mut *self {
|
||||
WsStream::Plain(s) => Pin::new(s).poll_next(cx),
|
||||
WsStream::Tls(s) => Pin::new(s).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_err(e: impl ToString) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Other, e.to_string())
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
use mlua::prelude::*;
|
||||
|
||||
pub(crate) mod body;
|
||||
pub(crate) mod client;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod shared;
|
||||
pub(crate) mod url;
|
||||
|
||||
use self::{
|
||||
client::ws_stream::WsStream,
|
||||
server::config::ServeConfig,
|
||||
shared::{request::Request, response::Response, websocket::Websocket},
|
||||
};
|
||||
|
||||
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
|
||||
|
||||
/**
|
||||
Returns a string containing type definitions for the `net` standard library.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn typedefs() -> String {
|
||||
TYPEDEFS.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Creates the `net` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
||||
// No initial rustls setup is necessary, the respective
|
||||
// functions lazily initialize anything there as needed
|
||||
TableBuilder::new(lua)?
|
||||
.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()
|
||||
}
|
||||
|
||||
async fn net_request(lua: Lua, req: Request) -> LuaResult<Response> {
|
||||
self::client::send_request(req, lua).await
|
||||
}
|
||||
|
||||
async fn net_socket(_: Lua, url: String) -> LuaResult<Websocket<WsStream>> {
|
||||
let url = url.parse().into_lua_err()?;
|
||||
self::client::connect_websocket(url).await
|
||||
}
|
||||
|
||||
async fn net_serve(lua: Lua, (port, config): (u16, ServeConfig)) -> LuaResult<LuaTable> {
|
||||
self::server::serve(lua.clone(), port, config)
|
||||
.await?
|
||||
.into_lua_table(lua)
|
||||
}
|
||||
|
||||
fn net_url_encode(
|
||||
lua: &Lua,
|
||||
(lua_string, as_binary): (LuaString, Option<bool>),
|
||||
) -> LuaResult<LuaString> {
|
||||
let as_binary = as_binary.unwrap_or_default();
|
||||
let bytes = self::url::encode(lua_string, as_binary)?;
|
||||
lua.create_string(bytes)
|
||||
}
|
||||
|
||||
fn net_url_decode(
|
||||
lua: &Lua,
|
||||
(lua_string, as_binary): (LuaString, Option<bool>),
|
||||
) -> LuaResult<LuaString> {
|
||||
let as_binary = as_binary.unwrap_or_default();
|
||||
let bytes = self::url::decode(lua_string, as_binary)?;
|
||||
lua.create_string(bytes)
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServeConfig {
|
||||
pub address: IpAddr,
|
||||
pub handle_request: LuaFunction,
|
||||
pub handle_web_socket: Option<LuaFunction>,
|
||||
}
|
||||
|
||||
impl FromLua for ServeConfig {
|
||||
fn from_lua(value: LuaValue, 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".to_string(),
|
||||
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".to_string(),
|
||||
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".to_string(),
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use async_channel::{unbounded, Receiver, Sender};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
use mlua::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServeHandle {
|
||||
addr: SocketAddr,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
sender: Sender<()>,
|
||||
}
|
||||
|
||||
impl ServeHandle {
|
||||
pub fn new(addr: SocketAddr) -> (Self, Receiver<()>) {
|
||||
let (sender, receiver) = unbounded();
|
||||
let this = Self {
|
||||
addr,
|
||||
shutdown: Arc::new(AtomicBool::new(false)),
|
||||
sender,
|
||||
};
|
||||
(this, receiver)
|
||||
}
|
||||
|
||||
// TODO: Remove this in the next major release to use colon/self
|
||||
// based call syntax and userdata implementation below instead
|
||||
pub fn into_lua_table(self, lua: Lua) -> LuaResult<LuaTable> {
|
||||
let shutdown = self.shutdown.clone();
|
||||
let sender = self.sender.clone();
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("ip", self.addr.ip().to_string())?
|
||||
.with_value("port", self.addr.port())?
|
||||
.with_function("stop", move |_, ()| {
|
||||
if shutdown.load(Ordering::SeqCst) {
|
||||
Err(LuaError::runtime("Server already stopped"))
|
||||
} else {
|
||||
shutdown.store(true, Ordering::SeqCst);
|
||||
sender.try_send(()).ok();
|
||||
sender.close();
|
||||
Ok(())
|
||||
}
|
||||
})?
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for ServeHandle {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("ip", |_, this| Ok(this.addr.ip().to_string()));
|
||||
fields.add_field_method_get("port", |_, this| Ok(this.addr.port()));
|
||||
}
|
||||
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_method("stop", |_, this, ()| {
|
||||
if this.shutdown.load(Ordering::SeqCst) {
|
||||
Err(LuaError::runtime("Server already stopped"))
|
||||
} else {
|
||||
this.shutdown.store(true, Ordering::SeqCst);
|
||||
this.sender.try_send(()).ok();
|
||||
this.sender.close();
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
use std::{cell::Cell, net::SocketAddr, rc::Rc};
|
||||
|
||||
use async_net::TcpListener;
|
||||
use futures_lite::pin;
|
||||
use hyper::server::conn::http1::Builder as Http1Builder;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::LuaSpawnExt;
|
||||
|
||||
use crate::{
|
||||
server::{config::ServeConfig, handle::ServeHandle, service::Service},
|
||||
shared::{
|
||||
futures::{either, Either},
|
||||
hyper::{HyperIo, HyperTimer},
|
||||
},
|
||||
};
|
||||
|
||||
pub mod config;
|
||||
pub mod handle;
|
||||
pub mod service;
|
||||
pub mod upgrade;
|
||||
|
||||
/**
|
||||
Starts an HTTP server using the given port and configuration.
|
||||
|
||||
Returns a `ServeHandle` that can be used to gracefully stop the server.
|
||||
*/
|
||||
pub async fn serve(lua: Lua, port: u16, config: ServeConfig) -> LuaResult<ServeHandle> {
|
||||
let address = SocketAddr::from((config.address, port));
|
||||
let service = Service {
|
||||
lua: lua.clone(),
|
||||
address,
|
||||
config,
|
||||
};
|
||||
|
||||
let listener = TcpListener::bind(address).await?;
|
||||
let (handle, shutdown_rx) = ServeHandle::new(address);
|
||||
|
||||
lua.spawn_local({
|
||||
let lua = lua.clone();
|
||||
async move {
|
||||
let handle_dropped = Rc::new(Cell::new(false));
|
||||
loop {
|
||||
// 1. Keep accepting new connections until we should shutdown
|
||||
let (conn, addr) = if handle_dropped.get() {
|
||||
// 1a. Handle has been dropped, and we don't need to listen for shutdown
|
||||
match listener.accept().await {
|
||||
Ok(acc) => acc,
|
||||
Err(_err) => {
|
||||
// TODO: Propagate error somehow
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 1b. Handle is possibly active, we must listen for shutdown
|
||||
match either(shutdown_rx.recv(), listener.accept()).await {
|
||||
Either::Left(Ok(())) => break,
|
||||
Either::Left(Err(_)) => {
|
||||
// NOTE #1: We will only get a RecvError 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.
|
||||
handle_dropped.set(true);
|
||||
continue;
|
||||
}
|
||||
Either::Right(Ok(acc)) => acc,
|
||||
Either::Right(Err(_err)) => {
|
||||
// TODO: Propagate error somehow
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 2. For each connection, spawn a new task to handle it
|
||||
lua.spawn_local({
|
||||
let rx = shutdown_rx.clone();
|
||||
let io = HyperIo::from(conn);
|
||||
|
||||
let mut svc = service.clone();
|
||||
svc.address = addr;
|
||||
|
||||
let handle_dropped = Rc::clone(&handle_dropped);
|
||||
async move {
|
||||
let conn = Http1Builder::new()
|
||||
.writev(false)
|
||||
.timer(HyperTimer)
|
||||
.keep_alive(true)
|
||||
.serve_connection(io, svc)
|
||||
.with_upgrades();
|
||||
if handle_dropped.get() {
|
||||
if let Err(_err) = conn.await {
|
||||
// TODO: Propagate error somehow
|
||||
}
|
||||
} else {
|
||||
// NOTE #2: Because we use keep_alive for websockets above, we need to
|
||||
// also manually poll this future and handle the graceful shutdown,
|
||||
// otherwise the already accepted connection will linger and run
|
||||
// even if the stop method has been called on the serve handle
|
||||
pin!(conn);
|
||||
match either(rx.recv(), conn.as_mut()).await {
|
||||
Either::Left(Ok(())) => conn.as_mut().graceful_shutdown(),
|
||||
Either::Left(Err(_)) => {
|
||||
// Same as note #1
|
||||
handle_dropped.set(true);
|
||||
if let Err(_err) = conn.await {
|
||||
// TODO: Propagate error somehow
|
||||
}
|
||||
}
|
||||
Either::Right(Ok(())) => {}
|
||||
Either::Right(Err(_err)) => {
|
||||
// TODO: Propagate error somehow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(handle)
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
use std::{future::Future, net::SocketAddr, pin::Pin};
|
||||
|
||||
use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
|
||||
use hyper::{
|
||||
body::Incoming, service::Service as HyperService, Request as HyperRequest,
|
||||
Response as HyperResponse, StatusCode,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt};
|
||||
|
||||
use crate::{
|
||||
body::ReadableBody,
|
||||
server::{
|
||||
config::ServeConfig,
|
||||
upgrade::{is_upgrade_request, make_upgrade_response},
|
||||
},
|
||||
shared::{hyper::HyperIo, request::Request, response::Response, websocket::Websocket},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Service {
|
||||
pub(super) lua: Lua,
|
||||
pub(super) address: SocketAddr, // NOTE: This must be the remote address of the connected client
|
||||
pub(super) config: ServeConfig,
|
||||
}
|
||||
|
||||
impl HyperService<HyperRequest<Incoming>> for Service {
|
||||
type Response = HyperResponse<ReadableBody>;
|
||||
type Error = LuaError;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
|
||||
fn call(&self, req: HyperRequest<Incoming>) -> Self::Future {
|
||||
if is_upgrade_request(&req) {
|
||||
if let Some(handler) = self.config.handle_web_socket.clone() {
|
||||
let lua = self.lua.clone();
|
||||
return Box::pin(async move {
|
||||
let response = match make_upgrade_response(&req) {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
return Ok(HyperResponse::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(ReadableBody::from(err.to_string()))
|
||||
.unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
lua.spawn_local({
|
||||
let lua = lua.clone();
|
||||
async move {
|
||||
if let Err(_err) = handle_websocket(lua, handler, req).await {
|
||||
// TODO: Propagate the error somehow?
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let lua = self.lua.clone();
|
||||
let address = self.address;
|
||||
let handler = self.config.handle_request.clone();
|
||||
Box::pin(async move {
|
||||
match handle_request(lua, handler, req, address).await {
|
||||
Ok(response) => Ok(response),
|
||||
Err(_err) => {
|
||||
// TODO: Propagate the error somehow?
|
||||
Ok(HyperResponse::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(ReadableBody::from("Lune: Internal server error"))
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_request(
|
||||
lua: Lua,
|
||||
handler: LuaFunction,
|
||||
request: HyperRequest<Incoming>,
|
||||
address: SocketAddr,
|
||||
) -> LuaResult<HyperResponse<ReadableBody>> {
|
||||
let request = Request::from_incoming(request, true)
|
||||
.await?
|
||||
.with_address(address);
|
||||
|
||||
let thread_id = lua.push_thread_back(handler, request)?;
|
||||
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")?;
|
||||
|
||||
let response = Response::from_lua_multi(thread_res, &lua)?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
async fn handle_websocket(
|
||||
lua: Lua,
|
||||
handler: LuaFunction,
|
||||
request: HyperRequest<Incoming>,
|
||||
) -> LuaResult<()> {
|
||||
let upgraded = hyper::upgrade::on(request).await.into_lua_err()?;
|
||||
|
||||
let stream =
|
||||
WebSocketStream::from_raw_socket(HyperIo::from(upgraded), Role::Server, None).await;
|
||||
|
||||
let websocket = Websocket::from(stream);
|
||||
lua.push_thread_back(handler, websocket)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
use async_tungstenite::tungstenite::{error::ProtocolError, handshake::derive_accept_key};
|
||||
|
||||
use hyper::{
|
||||
body::Incoming,
|
||||
header::{HeaderName, CONNECTION, UPGRADE},
|
||||
HeaderMap, Request as HyperRequest, Response as HyperResponse, StatusCode,
|
||||
};
|
||||
|
||||
use crate::body::ReadableBody;
|
||||
|
||||
const SEC_WEBSOCKET_VERSION: HeaderName = HeaderName::from_static("sec-websocket-version");
|
||||
const SEC_WEBSOCKET_KEY: HeaderName = HeaderName::from_static("sec-websocket-key");
|
||||
const SEC_WEBSOCKET_ACCEPT: HeaderName = HeaderName::from_static("sec-websocket-accept");
|
||||
|
||||
pub fn is_upgrade_request(request: &HyperRequest<Incoming>) -> bool {
|
||||
fn check_header_contains(headers: &HeaderMap, header_name: HeaderName, value: &str) -> bool {
|
||||
headers.get(header_name).is_some_and(|header| {
|
||||
header.to_str().map_or_else(
|
||||
|_| false,
|
||||
|header_str| {
|
||||
header_str
|
||||
.split(',')
|
||||
.any(|part| part.trim().eq_ignore_ascii_case(value))
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
check_header_contains(request.headers(), CONNECTION, "Upgrade")
|
||||
&& check_header_contains(request.headers(), UPGRADE, "websocket")
|
||||
}
|
||||
|
||||
pub fn make_upgrade_response(
|
||||
request: &HyperRequest<Incoming>,
|
||||
) -> Result<HyperResponse<ReadableBody>, ProtocolError> {
|
||||
let key = request
|
||||
.headers()
|
||||
.get(SEC_WEBSOCKET_KEY)
|
||||
.ok_or(ProtocolError::MissingSecWebSocketKey)?;
|
||||
|
||||
if request
|
||||
.headers()
|
||||
.get(SEC_WEBSOCKET_VERSION)
|
||||
.is_none_or(|v| v.as_bytes() != b"13")
|
||||
{
|
||||
return Err(ProtocolError::MissingSecWebSocketVersionHeader);
|
||||
}
|
||||
|
||||
Ok(HyperResponse::builder()
|
||||
.status(StatusCode::SWITCHING_PROTOCOLS)
|
||||
.header(CONNECTION, "upgrade")
|
||||
.header(UPGRADE, "websocket")
|
||||
.header(SEC_WEBSOCKET_ACCEPT, derive_accept_key(key.as_bytes()))
|
||||
.body(ReadableBody::from("switching to websocket protocol"))
|
||||
.unwrap())
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
use futures_lite::prelude::*;
|
||||
|
||||
pub use http_body_util::Either;
|
||||
|
||||
/**
|
||||
Combines the left and right futures into a single future
|
||||
that resolves to either the left or right output.
|
||||
|
||||
This combinator is biased - if both futures resolve at
|
||||
the same time, the left future's output is returned.
|
||||
*/
|
||||
pub fn either<L: Future, R: Future>(
|
||||
left: L,
|
||||
right: R,
|
||||
) -> impl Future<Output = Either<L::Output, R::Output>> {
|
||||
let fut_left = async move { Either::Left(left.await) };
|
||||
let fut_right = async move { Either::Right(right.await) };
|
||||
fut_left.or(fut_right)
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use hyper::{
|
||||
header::{CONTENT_ENCODING, CONTENT_LENGTH},
|
||||
HeaderMap,
|
||||
};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
use mlua::prelude::*;
|
||||
|
||||
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 string_map = HashMap::<String, Vec<String>>::new();
|
||||
|
||||
for (name, value) in headers {
|
||||
if let Some(name) = name {
|
||||
if let Ok(value) = value.to_str() {
|
||||
string_map
|
||||
.entry(name.to_string())
|
||||
.or_default()
|
||||
.push(value.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hash_map_to_table(lua, string_map, remove_content_headers)
|
||||
}
|
||||
|
||||
pub fn hash_map_to_table(
|
||||
lua: &Lua,
|
||||
map: impl IntoIterator<Item = (String, Vec<String>)>,
|
||||
remove_content_headers: bool,
|
||||
) -> LuaResult<LuaTable> {
|
||||
let mut string_map = HashMap::<String, Vec<String>>::new();
|
||||
for (name, values) in map {
|
||||
let name = name.as_str();
|
||||
|
||||
if remove_content_headers {
|
||||
let content_encoding_header_str = CONTENT_ENCODING.as_str();
|
||||
let content_length_header_str = CONTENT_LENGTH.as_str();
|
||||
if name == content_encoding_header_str || name == content_length_header_str {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for value in values {
|
||||
let value = value.as_str();
|
||||
string_map
|
||||
.entry(name.to_owned())
|
||||
.or_default()
|
||||
.push(value.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
let mut builder = TableBuilder::new(lua.clone())?;
|
||||
for (name, mut values) in string_map {
|
||||
if values.len() == 1 {
|
||||
let value = values.pop().unwrap().into_lua(lua)?;
|
||||
builder = builder.with_value(name, value)?;
|
||||
} else {
|
||||
let values = TableBuilder::new(lua.clone())?
|
||||
.with_sequential_values(values)?
|
||||
.build_readonly()?
|
||||
.into_lua(lua)?;
|
||||
builder = builder.with_value(name, values)?;
|
||||
}
|
||||
}
|
||||
|
||||
builder.build_readonly()
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
use std::{
|
||||
future::Future,
|
||||
io,
|
||||
pin::Pin,
|
||||
slice,
|
||||
task::{Context, Poll},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use async_io::Timer;
|
||||
use futures_lite::{prelude::*, ready};
|
||||
use hyper::rt::{self, Executor, ReadBuf, ReadBufCursor};
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::LuaSpawnExt;
|
||||
|
||||
// Hyper executor that spawns futures onto our Lua scheduler
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HyperExecutor {
|
||||
lua: Lua,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl HyperExecutor {
|
||||
pub fn execute<Fut>(lua: Lua, fut: Fut)
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: Send + 'static,
|
||||
{
|
||||
let exec = if let Some(exec) = lua.app_data_ref::<Self>() {
|
||||
exec
|
||||
} else {
|
||||
lua.set_app_data(Self { lua: lua.clone() });
|
||||
lua.app_data_ref::<Self>().unwrap()
|
||||
};
|
||||
|
||||
exec.execute(fut);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut: Future + Send + 'static> rt::Executor<Fut> for HyperExecutor
|
||||
where
|
||||
Fut::Output: Send + 'static,
|
||||
{
|
||||
fn execute(&self, fut: Fut) {
|
||||
self.lua.spawn(fut).detach();
|
||||
}
|
||||
}
|
||||
|
||||
// Hyper timer & sleep future wrapper for async-io
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HyperTimer;
|
||||
|
||||
impl rt::Timer for HyperTimer {
|
||||
fn sleep(&self, duration: Duration) -> Pin<Box<dyn rt::Sleep>> {
|
||||
Box::pin(HyperSleep::from(Timer::after(duration)))
|
||||
}
|
||||
|
||||
fn sleep_until(&self, at: Instant) -> Pin<Box<dyn rt::Sleep>> {
|
||||
Box::pin(HyperSleep::from(Timer::at(at)))
|
||||
}
|
||||
|
||||
fn reset(&self, sleep: &mut Pin<Box<dyn rt::Sleep>>, new_deadline: Instant) {
|
||||
if let Some(mut sleep) = sleep.as_mut().downcast_mut_pin::<HyperSleep>() {
|
||||
sleep.inner.set_at(new_deadline);
|
||||
} else {
|
||||
*sleep = Box::pin(HyperSleep::from(Timer::at(new_deadline)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HyperSleep {
|
||||
inner: Timer,
|
||||
}
|
||||
|
||||
impl From<Timer> for HyperSleep {
|
||||
fn from(inner: Timer) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for HyperSleep {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||
match Pin::new(&mut self.inner).poll(cx) {
|
||||
Poll::Ready(_) => Poll::Ready(()),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rt::Sleep for HyperSleep {}
|
||||
|
||||
// Hyper I/O wrapper for bidirectional compatibility
|
||||
// between hyper & futures-lite async read/write traits
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
#[derive(Debug)]
|
||||
pub struct HyperIo<T> {
|
||||
#[pin]
|
||||
inner: T
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for HyperIo<T> {
|
||||
fn from(inner: T) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> HyperIo<T> {
|
||||
pub fn pin_mut(self: Pin<&mut Self>) -> Pin<&mut T> {
|
||||
self.project().inner
|
||||
}
|
||||
}
|
||||
|
||||
// Compat for futures-lite -> hyper runtime
|
||||
|
||||
impl<T: AsyncRead> rt::Read for HyperIo<T> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
mut buf: ReadBufCursor<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
// Fill the read buffer with initialized data
|
||||
let read_slice = unsafe {
|
||||
let buffer = buf.as_mut();
|
||||
buffer.as_mut_ptr().write_bytes(0, buffer.len());
|
||||
slice::from_raw_parts_mut(buffer.as_mut_ptr().cast::<u8>(), buffer.len())
|
||||
};
|
||||
|
||||
// Read bytes from the underlying source
|
||||
let n = match self.pin_mut().poll_read(cx, read_slice) {
|
||||
Poll::Ready(Ok(n)) => n,
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
buf.advance(n);
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite> rt::Write for HyperIo<T> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
self.pin_mut().poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.pin_mut().poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.pin_mut().poll_close(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// Compat for hyper runtime -> futures-lite
|
||||
|
||||
impl<T: rt::Read> AsyncRead for HyperIo<T> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
let mut buf = ReadBuf::new(buf);
|
||||
ready!(self.pin_mut().poll_read(cx, buf.unfilled()))?;
|
||||
Poll::Ready(Ok(buf.filled().len()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: rt::Write> AsyncWrite for HyperIo<T> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
self.pin_mut().poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
|
||||
self.pin_mut().poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.pin_mut().poll_shutdown(cx)
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
use hyper::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
HeaderMap, Method,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
pub fn lua_value_to_method(value: &LuaValue) -> LuaResult<Method> {
|
||||
match value {
|
||||
LuaValue::Nil => Ok(Method::GET),
|
||||
LuaValue::String(str) => {
|
||||
let bytes = str.as_bytes().trim_ascii().to_ascii_uppercase();
|
||||
Method::from_bytes(&bytes).into_lua_err()
|
||||
}
|
||||
LuaValue::Buffer(buf) => {
|
||||
let bytes = buf.to_vec().trim_ascii().to_ascii_uppercase();
|
||||
Method::from_bytes(&bytes).into_lua_err()
|
||||
}
|
||||
v => Err(LuaError::FromLuaConversionError {
|
||||
from: v.type_name(),
|
||||
to: "Method".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid method - expected string or buffer, got {}",
|
||||
v.type_name()
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lua_table_to_header_map(table: &LuaTable) -> LuaResult<HeaderMap> {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
for pair in table.pairs::<LuaString, LuaString>() {
|
||||
let (key, val) = pair?;
|
||||
let key = HeaderName::from_bytes(&key.as_bytes()).into_lua_err()?;
|
||||
let val = HeaderValue::from_bytes(&val.as_bytes()).into_lua_err()?;
|
||||
headers.insert(key, val);
|
||||
}
|
||||
|
||||
Ok(headers)
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
pub mod futures;
|
||||
pub mod headers;
|
||||
pub mod hyper;
|
||||
pub mod lua;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod websocket;
|
|
@ -1,256 +0,0 @@
|
|||
use std::{collections::HashMap, net::SocketAddr};
|
||||
|
||||
use url::Url;
|
||||
|
||||
use hyper::{body::Incoming, HeaderMap, Method, Request as HyperRequest};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use crate::{
|
||||
body::{handle_incoming_body, ReadableBody},
|
||||
shared::{
|
||||
headers::{hash_map_to_table, header_map_to_table},
|
||||
lua::{lua_table_to_header_map, lua_value_to_method},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequestOptions {
|
||||
pub decompress: bool,
|
||||
}
|
||||
|
||||
impl Default for RequestOptions {
|
||||
fn default() -> Self {
|
||||
Self { decompress: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for RequestOptions {
|
||||
fn from_lua(value: LuaValue, _: &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 options".to_string(),
|
||||
)),
|
||||
}?;
|
||||
Ok(Self { decompress })
|
||||
} else {
|
||||
// Anything else is invalid
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "RequestOptions".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid request options - expected table or nil, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Request {
|
||||
pub(crate) inner: HyperRequest<ReadableBody>,
|
||||
pub(crate) address: Option<SocketAddr>,
|
||||
pub(crate) redirects: Option<usize>,
|
||||
pub(crate) decompress: bool,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/**
|
||||
Creates a new request from a raw incoming request.
|
||||
*/
|
||||
pub async fn from_incoming(
|
||||
incoming: HyperRequest<Incoming>,
|
||||
decompress: bool,
|
||||
) -> LuaResult<Self> {
|
||||
let (parts, body) = incoming.into_parts();
|
||||
|
||||
let (body, decompress) = handle_incoming_body(&parts.headers, body, decompress).await?;
|
||||
|
||||
Ok(Self {
|
||||
inner: HyperRequest::from_parts(parts, ReadableBody::from(body)),
|
||||
address: None,
|
||||
redirects: None,
|
||||
decompress,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Attaches a socket address to the request.
|
||||
|
||||
This will make the `ip` and `port` fields available on the request.
|
||||
*/
|
||||
pub fn with_address(mut self, address: SocketAddr) -> Self {
|
||||
self.address = Some(address);
|
||||
self
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the method of the request.
|
||||
*/
|
||||
pub fn method(&self) -> Method {
|
||||
self.inner.method().clone()
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the path of the request.
|
||||
*/
|
||||
pub fn path(&self) -> &str {
|
||||
self.inner.uri().path()
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the query parameters of the request.
|
||||
*/
|
||||
pub fn query(&self) -> HashMap<String, Vec<String>> {
|
||||
let uri = self.inner.uri();
|
||||
|
||||
let mut result = HashMap::<String, Vec<String>>::new();
|
||||
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
result
|
||||
.entry(key.to_string())
|
||||
.or_default()
|
||||
.push(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the headers of the request.
|
||||
*/
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
self.inner.headers()
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the body of the request.
|
||||
*/
|
||||
pub fn body(&self) -> &[u8] {
|
||||
self.inner.body().as_slice()
|
||||
}
|
||||
|
||||
/**
|
||||
Clones the inner `hyper` request.
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
pub fn clone_inner(&self) -> HyperRequest<ReadableBody> {
|
||||
self.inner.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
Takes the inner `hyper` request by ownership.
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
pub fn into_inner(self) -> HyperRequest<ReadableBody> {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for Request {
|
||||
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
|
||||
if let LuaValue::String(s) = value {
|
||||
// If we just got a string we assume
|
||||
// its a GET request to a given url
|
||||
let uri = s.to_str()?;
|
||||
let uri = uri.parse().into_lua_err()?;
|
||||
|
||||
let mut request = HyperRequest::new(ReadableBody::empty());
|
||||
*request.uri_mut() = uri;
|
||||
|
||||
Ok(Self {
|
||||
inner: request,
|
||||
address: None,
|
||||
redirects: None,
|
||||
decompress: RequestOptions::default().decompress,
|
||||
})
|
||||
} else if let LuaValue::Table(tab) = value {
|
||||
// If we got a table we are able to configure the
|
||||
// entire request, maybe with extra options too
|
||||
let options = match tab.get::<LuaValue>("options") {
|
||||
Ok(opts) => RequestOptions::from_lua(opts, lua)?,
|
||||
Err(_) => RequestOptions::default(),
|
||||
};
|
||||
|
||||
// Extract url (required) + optional structured query params
|
||||
let url = tab.get::<LuaString>("url")?;
|
||||
let mut url = url.to_str()?.parse::<Url>().into_lua_err()?;
|
||||
if let Some(t) = tab.get::<Option<LuaTable>>("query")? {
|
||||
let mut query = url.query_pairs_mut();
|
||||
for pair in t.pairs::<LuaString, LuaString>() {
|
||||
let (key, value) = pair?;
|
||||
let key = key.to_str()?;
|
||||
let value = value.to_str()?;
|
||||
query.append_pair(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract method
|
||||
let method = tab.get::<LuaValue>("method")?;
|
||||
let method = lua_value_to_method(&method)?;
|
||||
|
||||
// Extract headers
|
||||
let headers = tab.get::<Option<LuaTable>>("headers")?;
|
||||
let headers = headers
|
||||
.map(|t| lua_table_to_header_map(&t))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
// Extract body
|
||||
let body = tab.get::<ReadableBody>("body")?;
|
||||
|
||||
// Build the full request
|
||||
let mut request = HyperRequest::new(body);
|
||||
request.headers_mut().extend(headers);
|
||||
*request.uri_mut() = url.to_string().parse().unwrap();
|
||||
*request.method_mut() = method;
|
||||
|
||||
// All good, validated and we got what we need
|
||||
Ok(Self {
|
||||
inner: request,
|
||||
address: None,
|
||||
redirects: None,
|
||||
decompress: options.decompress,
|
||||
})
|
||||
} else {
|
||||
// Anything else is invalid
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "Request".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid request - expected string or table, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Request {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("ip", |_, this| {
|
||||
Ok(this.address.map(|address| address.ip().to_string()))
|
||||
});
|
||||
fields.add_field_method_get("port", |_, this| {
|
||||
Ok(this.address.map(|address| address.port()))
|
||||
});
|
||||
fields.add_field_method_get("method", |_, this| Ok(this.method().to_string()));
|
||||
fields.add_field_method_get("path", |_, this| Ok(this.path().to_string()));
|
||||
fields.add_field_method_get("query", |lua, this| {
|
||||
hash_map_to_table(lua, this.query(), false)
|
||||
});
|
||||
fields.add_field_method_get("headers", |lua, this| {
|
||||
header_map_to_table(lua, this.headers().clone(), this.decompress)
|
||||
});
|
||||
fields.add_field_method_get("body", |lua, this| lua.create_string(this.body()));
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
use hyper::{
|
||||
body::Incoming,
|
||||
header::{HeaderValue, CONTENT_TYPE},
|
||||
HeaderMap, Response as HyperResponse, StatusCode,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use crate::{
|
||||
body::{handle_incoming_body, ReadableBody},
|
||||
shared::{headers::header_map_to_table, lua::lua_table_to_header_map},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Response {
|
||||
pub(crate) inner: HyperResponse<ReadableBody>,
|
||||
pub(crate) decompressed: bool,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/**
|
||||
Creates a new response from a raw incoming response.
|
||||
*/
|
||||
pub async fn from_incoming(
|
||||
incoming: HyperResponse<Incoming>,
|
||||
decompress: bool,
|
||||
) -> LuaResult<Self> {
|
||||
let (parts, body) = incoming.into_parts();
|
||||
|
||||
let (body, decompressed) = handle_incoming_body(&parts.headers, body, decompress).await?;
|
||||
|
||||
Ok(Self {
|
||||
inner: HyperResponse::from_parts(parts, ReadableBody::from(body)),
|
||||
decompressed,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Returns whether the request was successful or not.
|
||||
*/
|
||||
pub fn status_ok(&self) -> bool {
|
||||
self.inner.status().is_success()
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the status code of the response.
|
||||
*/
|
||||
pub fn status_code(&self) -> u16 {
|
||||
self.inner.status().as_u16()
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the status message of the response.
|
||||
*/
|
||||
pub fn status_message(&self) -> &str {
|
||||
self.inner.status().canonical_reason().unwrap_or_default()
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the headers of the response.
|
||||
*/
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
self.inner.headers()
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the body of the response.
|
||||
*/
|
||||
pub fn body(&self) -> &[u8] {
|
||||
self.inner.body().as_slice()
|
||||
}
|
||||
|
||||
/**
|
||||
Clones the inner `hyper` response.
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
pub fn clone_inner(&self) -> HyperResponse<ReadableBody> {
|
||||
self.inner.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
Takes the inner `hyper` response by ownership.
|
||||
*/
|
||||
#[allow(dead_code)]
|
||||
pub fn into_inner(self) -> HyperResponse<ReadableBody> {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for Response {
|
||||
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
|
||||
if let Ok(body) = ReadableBody::from_lua(value.clone(), lua) {
|
||||
// String or buffer is always a 200 text/plain response
|
||||
let mut response = HyperResponse::new(body);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
|
||||
Ok(Self {
|
||||
inner: response,
|
||||
decompressed: false,
|
||||
})
|
||||
} else if let LuaValue::Table(tab) = value {
|
||||
// Extract status (required)
|
||||
let status = tab.get::<u16>("status")?;
|
||||
let status = StatusCode::from_u16(status).into_lua_err()?;
|
||||
|
||||
// Extract headers
|
||||
let headers = tab.get::<Option<LuaTable>>("headers")?;
|
||||
let headers = headers
|
||||
.map(|t| lua_table_to_header_map(&t))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
// Extract body
|
||||
let body = tab.get::<ReadableBody>("body")?;
|
||||
|
||||
// Build the full response
|
||||
let mut response = HyperResponse::new(body);
|
||||
response.headers_mut().extend(headers);
|
||||
*response.status_mut() = status;
|
||||
|
||||
// All good, validated and we got what we need
|
||||
Ok(Self {
|
||||
inner: response,
|
||||
decompressed: false,
|
||||
})
|
||||
} else {
|
||||
// Anything else is invalid
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "Response".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid response - expected table/string/buffer, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Response {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("ok", |_, this| Ok(this.status_ok()));
|
||||
fields.add_field_method_get("statusCode", |_, this| Ok(this.status_code()));
|
||||
fields.add_field_method_get("statusMessage", |lua, this| {
|
||||
lua.create_string(this.status_message())
|
||||
});
|
||||
fields.add_field_method_get("headers", |lua, this| {
|
||||
header_map_to_table(lua, this.headers().clone(), this.decompressed)
|
||||
});
|
||||
fields.add_field_method_get("body", |lua, this| lua.create_string(this.body()));
|
||||
}
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
use std::{
|
||||
error::Error,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU16, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use async_lock::Mutex as AsyncMutex;
|
||||
use async_tungstenite::tungstenite::{
|
||||
protocol::{frame::coding::CloseCode, CloseFrame},
|
||||
Message as TungsteniteMessage, Result as TungsteniteResult, Utf8Bytes,
|
||||
};
|
||||
use bstr::{BString, ByteSlice};
|
||||
use futures::{
|
||||
stream::{SplitSink, SplitStream},
|
||||
Sink, SinkExt, Stream, StreamExt,
|
||||
};
|
||||
use hyper::body::Bytes;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Websocket<T> {
|
||||
close_code_exists: Arc<AtomicBool>,
|
||||
close_code_value: Arc<AtomicU16>,
|
||||
read_stream: Arc<AsyncMutex<SplitStream<T>>>,
|
||||
write_stream: Arc<AsyncMutex<SplitSink<T, TungsteniteMessage>>>,
|
||||
}
|
||||
|
||||
impl<T> Websocket<T>
|
||||
where
|
||||
T: Stream<Item = TungsteniteResult<TungsteniteMessage>> + Sink<TungsteniteMessage> + 'static,
|
||||
<T as Sink<TungsteniteMessage>>::Error: Into<Box<dyn Error + Send + Sync + 'static>>,
|
||||
{
|
||||
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: TungsteniteMessage) -> LuaResult<()> {
|
||||
let mut ws = self.write_stream.lock().await;
|
||||
ws.send(msg).await.into_lua_err()
|
||||
}
|
||||
|
||||
pub async fn next(&self) -> LuaResult<Option<TungsteniteMessage>> {
|
||||
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(TungsteniteMessage::Close(Some(CloseFrame {
|
||||
code: match code {
|
||||
Some(code) if (1000..=4999).contains(&code) => CloseCode::from(code),
|
||||
Some(code) => {
|
||||
return Err(LuaError::runtime(format!(
|
||||
"Close code must be between 1000 and 4999, got {code}"
|
||||
)))
|
||||
}
|
||||
None => CloseCode::Normal,
|
||||
},
|
||||
reason: "".into(),
|
||||
})))
|
||||
.await?;
|
||||
|
||||
let mut ws = self.write_stream.lock().await;
|
||||
ws.close().await.into_lua_err()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Websocket<T>
|
||||
where
|
||||
T: Stream<Item = TungsteniteResult<TungsteniteMessage>> + Sink<TungsteniteMessage> + 'static,
|
||||
<T as Sink<TungsteniteMessage>>::Error: Into<Box<dyn Error + Send + Sync + 'static>>,
|
||||
{
|
||||
fn from(value: 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> LuaUserData for Websocket<T>
|
||||
where
|
||||
T: Stream<Item = TungsteniteResult<TungsteniteMessage>> + Sink<TungsteniteMessage> + 'static,
|
||||
<T as Sink<TungsteniteMessage>>::Error: Into<Box<dyn Error + Send + Sync + 'static>>,
|
||||
{
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("closeCode", |_, this| Ok(this.get_close_code()));
|
||||
}
|
||||
|
||||
fn add_methods<M: LuaUserDataMethods<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() {
|
||||
TungsteniteMessage::Binary(Bytes::from(string.to_vec()))
|
||||
} else {
|
||||
let s = string.to_str().into_lua_err()?;
|
||||
TungsteniteMessage::Text(Utf8Bytes::from(s))
|
||||
})
|
||||
.await
|
||||
},
|
||||
);
|
||||
|
||||
methods.add_async_method("next", |lua, this, (): ()| async move {
|
||||
let msg = this.next().await?;
|
||||
|
||||
if let Some(TungsteniteMessage::Close(Some(frame))) = msg.as_ref() {
|
||||
this.set_close_code(frame.code.into());
|
||||
}
|
||||
|
||||
Ok(match msg {
|
||||
Some(TungsteniteMessage::Binary(bin)) => LuaValue::String(lua.create_string(bin)?),
|
||||
Some(TungsteniteMessage::Text(txt)) => LuaValue::String(lua.create_string(txt)?),
|
||||
Some(TungsteniteMessage::Close(_)) | None => LuaValue::Nil,
|
||||
// Ignore ping/pong/frame messages, they are handled by tungstenite
|
||||
msg => unreachable!("Unhandled message: {:?}", msg),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
pub fn decode(lua_string: LuaString, as_binary: bool) -> LuaResult<Vec<u8>> {
|
||||
if as_binary {
|
||||
Ok(urlencoding::decode_binary(&lua_string.as_bytes()).into_owned())
|
||||
} else {
|
||||
Ok(urlencoding::decode(&lua_string.to_str()?)
|
||||
.map_err(|e| LuaError::RuntimeError(format!("Encountered invalid encoding - {e}")))?
|
||||
.into_owned()
|
||||
.into_bytes())
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
pub fn encode(lua_string: LuaString, as_binary: bool) -> LuaResult<Vec<u8>> {
|
||||
if as_binary {
|
||||
Ok(urlencoding::encode_binary(&lua_string.as_bytes())
|
||||
.into_owned()
|
||||
.into_bytes())
|
||||
} else {
|
||||
Ok(urlencoding::encode(&lua_string.to_str()?)
|
||||
.into_owned()
|
||||
.into_bytes())
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mod decode;
|
||||
mod encode;
|
||||
|
||||
pub use self::decode::decode;
|
||||
pub use self::encode::encode;
|
|
@ -1,32 +0,0 @@
|
|||
[package]
|
||||
name = "lune-std-process"
|
||||
version = "0.2.2"
|
||||
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.10.3", features = ["luau"] }
|
||||
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
|
||||
|
||||
directories = "6.0"
|
||||
pin-project = "1.0"
|
||||
|
||||
bstr = "1.9"
|
||||
bytes = "1.6.0"
|
||||
|
||||
async-channel = "2.3"
|
||||
async-lock = "3.4"
|
||||
async-process = "2.3"
|
||||
blocking = "1.6"
|
||||
futures-lite = "2.6"
|
||||
futures-util = "0.3" # Needed for select! macro...
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|
|
@ -1,86 +0,0 @@
|
|||
use std::process::ExitStatus;
|
||||
|
||||
use async_channel::{unbounded, Receiver, Sender};
|
||||
use async_process::Child as AsyncChild;
|
||||
use futures_util::{select, FutureExt};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::LuaSpawnExt;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use super::{ChildReader, ChildWriter};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Child {
|
||||
stdin: ChildWriter,
|
||||
stdout: ChildReader,
|
||||
stderr: ChildReader,
|
||||
kill_tx: Sender<()>,
|
||||
status_rx: Receiver<Option<ExitStatus>>,
|
||||
}
|
||||
|
||||
impl Child {
|
||||
pub fn new(lua: &Lua, mut child: AsyncChild) -> Self {
|
||||
let stdin = ChildWriter::from(child.stdin.take());
|
||||
let stdout = ChildReader::from(child.stdout.take());
|
||||
let stderr = ChildReader::from(child.stderr.take());
|
||||
|
||||
// NOTE: Kill channel is zero size, status is very small
|
||||
// and implements Copy, unbounded will be just fine here
|
||||
let (kill_tx, kill_rx) = unbounded();
|
||||
let (status_tx, status_rx) = unbounded();
|
||||
lua.spawn(handle_child(child, kill_rx, status_tx)).detach();
|
||||
|
||||
Self {
|
||||
stdin,
|
||||
stdout,
|
||||
stderr,
|
||||
kill_tx,
|
||||
status_rx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Child {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("stdin", |_, this| Ok(this.stdin.clone()));
|
||||
fields.add_field_method_get("stdout", |_, this| Ok(this.stdout.clone()));
|
||||
fields.add_field_method_get("stderr", |_, this| Ok(this.stderr.clone()));
|
||||
}
|
||||
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_method("kill", |_, this, (): ()| {
|
||||
let _ = this.kill_tx.try_send(());
|
||||
Ok(())
|
||||
});
|
||||
methods.add_async_method("status", |lua, this, (): ()| {
|
||||
let rx = this.status_rx.clone();
|
||||
async move {
|
||||
let status = rx.recv().await.ok().flatten();
|
||||
let code = status.and_then(|c| c.code()).unwrap_or(9);
|
||||
TableBuilder::new(lua.clone())?
|
||||
.with_value("ok", code == 0)?
|
||||
.with_value("code", code)?
|
||||
.build_readonly()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_child(
|
||||
mut child: AsyncChild,
|
||||
kill_rx: Receiver<()>,
|
||||
status_tx: Sender<Option<ExitStatus>>,
|
||||
) {
|
||||
let status = select! {
|
||||
s = child.status().fuse() => s.ok(), // FUTURE: Propagate this error somehow?
|
||||
_ = kill_rx.recv().fuse() => {
|
||||
let _ = child.kill(); // Will only error if already killed
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Will only error if there are no receivers waiting for the status
|
||||
let _ = status_tx.send(status).await;
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use async_lock::Mutex as AsyncMutex;
|
||||
use async_process::{ChildStderr as AsyncChildStderr, ChildStdout as AsyncChildStdout};
|
||||
use futures_lite::prelude::*;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
const DEFAULT_BUFFER_SIZE: usize = 1024;
|
||||
|
||||
// Inner (plumbing) implementation
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ChildReaderInner {
|
||||
None,
|
||||
Stdout(AsyncChildStdout),
|
||||
Stderr(AsyncChildStderr),
|
||||
}
|
||||
|
||||
impl ChildReaderInner {
|
||||
async fn read(&mut self, size: usize) -> Result<Vec<u8>, std::io::Error> {
|
||||
if matches!(self, ChildReaderInner::None) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut buf = vec![0; size];
|
||||
|
||||
let read = match self {
|
||||
ChildReaderInner::None => unreachable!(),
|
||||
ChildReaderInner::Stdout(stdout) => stdout.read(&mut buf).await?,
|
||||
ChildReaderInner::Stderr(stderr) => stderr.read(&mut buf).await?,
|
||||
};
|
||||
|
||||
buf.truncate(read);
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
async fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let read = match self {
|
||||
ChildReaderInner::None => 0,
|
||||
ChildReaderInner::Stdout(stdout) => stdout.read_to_end(&mut buf).await?,
|
||||
ChildReaderInner::Stderr(stderr) => stderr.read_to_end(&mut buf).await?,
|
||||
};
|
||||
|
||||
buf.truncate(read);
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AsyncChildStdout> for ChildReaderInner {
|
||||
fn from(stdout: AsyncChildStdout) -> Self {
|
||||
Self::Stdout(stdout)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AsyncChildStderr> for ChildReaderInner {
|
||||
fn from(stderr: AsyncChildStderr) -> Self {
|
||||
Self::Stderr(stderr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<AsyncChildStdout>> for ChildReaderInner {
|
||||
fn from(stdout: Option<AsyncChildStdout>) -> Self {
|
||||
stdout.map_or(Self::None, Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<AsyncChildStderr>> for ChildReaderInner {
|
||||
fn from(stderr: Option<AsyncChildStderr>) -> Self {
|
||||
stderr.map_or(Self::None, Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
// Outer (lua-accessible, clonable) implementation
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChildReader {
|
||||
inner: Arc<AsyncMutex<ChildReaderInner>>,
|
||||
}
|
||||
|
||||
impl LuaUserData for ChildReader {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_async_method("read", |lua, this, size: Option<usize>| {
|
||||
let inner = this.inner.clone();
|
||||
let size = size.unwrap_or(DEFAULT_BUFFER_SIZE);
|
||||
async move {
|
||||
let mut inner = inner.lock().await;
|
||||
let bytes = inner.read(size).await.into_lua_err()?;
|
||||
if bytes.is_empty() {
|
||||
Ok(LuaValue::Nil)
|
||||
} else {
|
||||
Ok(LuaValue::String(lua.create_string(bytes)?))
|
||||
}
|
||||
}
|
||||
});
|
||||
methods.add_async_method("readToEnd", |lua, this, (): ()| {
|
||||
let inner = this.inner.clone();
|
||||
async move {
|
||||
let mut inner = inner.lock().await;
|
||||
let bytes = inner.read_to_end().await.into_lua_err()?;
|
||||
Ok(lua.create_string(bytes))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<ChildReaderInner>> From<T> for ChildReader {
|
||||
fn from(inner: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(AsyncMutex::new(inner.into())),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use async_lock::Mutex as AsyncMutex;
|
||||
use async_process::ChildStdin as AsyncChildStdin;
|
||||
use futures_lite::prelude::*;
|
||||
|
||||
use bstr::BString;
|
||||
use mlua::prelude::*;
|
||||
|
||||
// Inner (plumbing) implementation
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ChildWriterInner {
|
||||
None,
|
||||
Stdin(AsyncChildStdin),
|
||||
}
|
||||
|
||||
impl ChildWriterInner {
|
||||
async fn write(&mut self, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||
match self {
|
||||
ChildWriterInner::None => Ok(()),
|
||||
ChildWriterInner::Stdin(stdin) => stdin.write_all(&data).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn close(&mut self) -> Result<(), std::io::Error> {
|
||||
match self {
|
||||
ChildWriterInner::None => Ok(()),
|
||||
ChildWriterInner::Stdin(stdin) => stdin.close().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AsyncChildStdin> for ChildWriterInner {
|
||||
fn from(stdin: AsyncChildStdin) -> Self {
|
||||
ChildWriterInner::Stdin(stdin)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<AsyncChildStdin>> for ChildWriterInner {
|
||||
fn from(stdin: Option<AsyncChildStdin>) -> Self {
|
||||
stdin.map_or(Self::None, Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
// Outer (lua-accessible, clonable) implementation
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChildWriter {
|
||||
inner: Arc<AsyncMutex<ChildWriterInner>>,
|
||||
}
|
||||
|
||||
impl LuaUserData for ChildWriter {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_async_method("write", |_, this, data: BString| {
|
||||
let inner = this.inner.clone();
|
||||
let data = data.to_vec();
|
||||
async move {
|
||||
let mut inner = inner.lock().await;
|
||||
inner.write(data).await.into_lua_err()
|
||||
}
|
||||
});
|
||||
methods.add_async_method("close", |_, this, (): ()| {
|
||||
let inner = this.inner.clone();
|
||||
async move {
|
||||
let mut inner = inner.lock().await;
|
||||
inner.close().await.into_lua_err()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<ChildWriterInner>> From<T> for ChildWriter {
|
||||
fn from(inner: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(AsyncMutex::new(inner.into())),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mod child;
|
||||
mod child_reader;
|
||||
mod child_writer;
|
||||
|
||||
pub use self::child::Child;
|
||||
pub use self::child_reader::ChildReader;
|
||||
pub use self::child_writer::ChildWriter;
|
|
@ -1,51 +0,0 @@
|
|||
use async_process::Child;
|
||||
use futures_lite::prelude::*;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use super::options::ProcessSpawnOptionsStdioKind;
|
||||
|
||||
mod tee_writer;
|
||||
mod wait_for_child;
|
||||
|
||||
use self::wait_for_child::wait_for_child;
|
||||
|
||||
pub async fn exec(
|
||||
lua: Lua,
|
||||
mut child: Child,
|
||||
stdin: Option<Vec<u8>>,
|
||||
stdout: ProcessSpawnOptionsStdioKind,
|
||||
stderr: ProcessSpawnOptionsStdioKind,
|
||||
) -> LuaResult<LuaTable> {
|
||||
// Write to stdin before anything else - if we got it
|
||||
if let Some(stdin) = stdin {
|
||||
let mut child_stdin = child.stdin.take().unwrap();
|
||||
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
||||
}
|
||||
|
||||
let res = wait_for_child(child, stdout, stderr).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
|
||||
let stdout = lua.create_string(&res.stdout)?;
|
||||
let stderr = lua.create_string(&res.stderr)?;
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("ok", code == 0)?
|
||||
.with_value("code", code)?
|
||||
.with_value("stdout", stdout)?
|
||||
.with_value("stderr", stderr)?
|
||||
.build_readonly()
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
use std::{io::stdout, process::ExitStatus};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use async_process::Child;
|
||||
use blocking::Unblock;
|
||||
use futures_lite::{io, prelude::*};
|
||||
|
||||
use super::tee_writer::AsyncTeeWriter;
|
||||
use crate::options::ProcessSpawnOptionsStdioKind;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct WaitForChildResult {
|
||||
pub status: ExitStatus,
|
||||
pub stdout: Vec<u8>,
|
||||
pub stderr: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn read_with_stdio_kind<R>(
|
||||
read_from: Option<R>,
|
||||
kind: ProcessSpawnOptionsStdioKind,
|
||||
) -> LuaResult<Vec<u8>>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
Ok(match kind {
|
||||
ProcessSpawnOptionsStdioKind::None | ProcessSpawnOptionsStdioKind::Forward => Vec::new(),
|
||||
ProcessSpawnOptionsStdioKind::Default => {
|
||||
let mut read_from =
|
||||
read_from.expect("read_from must be Some when stdio kind is Default");
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
read_from.read_to_end(&mut buffer).await.into_lua_err()?;
|
||||
|
||||
buffer
|
||||
}
|
||||
ProcessSpawnOptionsStdioKind::Inherit => {
|
||||
let mut read_from =
|
||||
read_from.expect("read_from must be Some when stdio kind is Inherit");
|
||||
|
||||
let mut stdout = Unblock::new(stdout());
|
||||
let mut tee = AsyncTeeWriter::new(&mut stdout);
|
||||
|
||||
io::copy(&mut read_from, &mut tee).await.into_lua_err()?;
|
||||
|
||||
tee.into_vec()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn wait_for_child(
|
||||
mut child: Child,
|
||||
stdout_kind: ProcessSpawnOptionsStdioKind,
|
||||
stderr_kind: ProcessSpawnOptionsStdioKind,
|
||||
) -> LuaResult<WaitForChildResult> {
|
||||
let stdout_opt = child.stdout.take();
|
||||
let stderr_opt = child.stderr.take();
|
||||
|
||||
let stdout_task = read_with_stdio_kind(stdout_opt, stdout_kind);
|
||||
let stderr_task = read_with_stdio_kind(stderr_opt, stderr_kind);
|
||||
|
||||
let status = child.status().await.into_lua_err()?;
|
||||
|
||||
let stdout_buffer = stdout_task.await.into_lua_err()?;
|
||||
let stderr_buffer = stderr_task.await.into_lua_err()?;
|
||||
|
||||
Ok(WaitForChildResult {
|
||||
status,
|
||||
stdout: stdout_buffer,
|
||||
stderr: stderr_buffer,
|
||||
})
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use std::{
|
||||
env::consts::{ARCH, OS},
|
||||
path::MAIN_SEPARATOR,
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::Functions;
|
||||
|
||||
use lune_utils::{
|
||||
path::get_current_dir,
|
||||
process::{ProcessArgs, ProcessEnv},
|
||||
TableBuilder,
|
||||
};
|
||||
|
||||
mod create;
|
||||
mod exec;
|
||||
mod options;
|
||||
|
||||
use self::options::ProcessSpawnOptions;
|
||||
|
||||
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
|
||||
|
||||
/**
|
||||
Returns a string containing type definitions for the `process` standard library.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn typedefs() -> String {
|
||||
TYPEDEFS.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
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"
|
||||
})?;
|
||||
|
||||
// Extract stored userdatas for args + env, the runtime struct should always provide this
|
||||
let process_args = lua
|
||||
.app_data_ref::<ProcessArgs>()
|
||||
.ok_or_else(|| LuaError::runtime("Missing process args in Lua app data"))?
|
||||
.clone();
|
||||
let process_env = lua
|
||||
.app_data_ref::<ProcessEnv>()
|
||||
.ok_or_else(|| LuaError::runtime("Missing process env in Lua app data"))?
|
||||
.clone();
|
||||
|
||||
// Create our process exit function, the scheduler crate provides this
|
||||
let fns = Functions::new(lua.clone())?;
|
||||
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", process_args)?
|
||||
.with_value("cwd", cwd_str)?
|
||||
.with_value("env", process_env)?
|
||||
.with_value("exit", process_exit)?
|
||||
.with_async_function("exec", process_exec)?
|
||||
.with_function("create", process_create)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
async fn process_exec(
|
||||
lua: Lua,
|
||||
(program, args, mut options): (String, ProcessArgs, ProcessSpawnOptions),
|
||||
) -> LuaResult<LuaTable> {
|
||||
let stdin = options.stdio.stdin.take();
|
||||
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()?;
|
||||
|
||||
exec::exec(lua, child, stdin, stdout, stderr).await
|
||||
}
|
||||
|
||||
fn process_create(
|
||||
lua: &Lua,
|
||||
(program, args, options): (String, ProcessArgs, ProcessSpawnOptions),
|
||||
) -> LuaResult<LuaValue> {
|
||||
let child = options
|
||||
.into_command(program, args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
create::Child::new(lua, child).into_lua(lua)
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
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 FromLua for ProcessSpawnOptionsStdioKind {
|
||||
fn from_lua(value: LuaValue, _: &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".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid spawn options stdio kind - expected string, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
env::{self},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use lune_utils::process::ProcessArgs;
|
||||
use mlua::prelude::*;
|
||||
|
||||
use async_process::Command;
|
||||
use directories::UserDirs;
|
||||
|
||||
mod kind;
|
||||
mod stdio;
|
||||
|
||||
pub(super) use kind::*;
|
||||
pub(super) use stdio::*;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(super) struct ProcessSpawnOptions {
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub envs: HashMap<String, String>,
|
||||
pub shell: Option<String>,
|
||||
pub stdio: ProcessSpawnOptionsStdio,
|
||||
}
|
||||
|
||||
impl FromLua for ProcessSpawnOptions {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
let mut this = Self::default();
|
||||
let value = match value {
|
||||
LuaValue::Nil => return Ok(this),
|
||||
LuaValue::Table(t) => t,
|
||||
_ => {
|
||||
return Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "ProcessSpawnOptions".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid spawn options - expected table, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
If we got a working directory to use:
|
||||
|
||||
1. Substitute leading tilde (~) for the users home dir
|
||||
2. Make sure it exists
|
||||
*/
|
||||
match value.get("cwd")? {
|
||||
LuaValue::Nil => {}
|
||||
LuaValue::String(s) => {
|
||||
let mut cwd = PathBuf::from(s.to_str()?.to_string());
|
||||
if let Ok(stripped) = cwd.strip_prefix("~") {
|
||||
let user_dirs = UserDirs::new().ok_or_else(|| {
|
||||
LuaError::runtime(
|
||||
"Invalid value for option 'cwd' - failed to get home directory",
|
||||
)
|
||||
})?;
|
||||
cwd = user_dirs.home_dir().join(stripped);
|
||||
}
|
||||
if !cwd.exists() {
|
||||
return Err(LuaError::runtime(
|
||||
"Invalid value for option 'cwd' - path does not exist",
|
||||
));
|
||||
}
|
||||
this.cwd = Some(cwd);
|
||||
}
|
||||
value => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Invalid type for option 'cwd' - expected string, got '{}'",
|
||||
value.type_name()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
If we got environment variables, make sure they are strings
|
||||
*/
|
||||
match value.get("env")? {
|
||||
LuaValue::Nil => {}
|
||||
LuaValue::Table(e) => {
|
||||
for pair in e.pairs::<String, String>() {
|
||||
let (k, v) = pair.context("Environment variables must be strings")?;
|
||||
this.envs.insert(k, v);
|
||||
}
|
||||
}
|
||||
value => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Invalid type for option 'env' - expected table, got '{}'",
|
||||
value.type_name()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
If we got a shell to use:
|
||||
|
||||
1. When given as a string, use that literally
|
||||
2. When set to true, use a default shell for the platform
|
||||
*/
|
||||
match value.get("shell")? {
|
||||
LuaValue::Nil => {}
|
||||
LuaValue::String(s) => this.shell = Some(s.to_string_lossy().to_string()),
|
||||
LuaValue::Boolean(true) => {
|
||||
this.shell = match env::consts::FAMILY {
|
||||
"unix" => Some("/bin/sh".to_string()),
|
||||
"windows" => Some("powershell".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
value => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Invalid type for option 'shell' - expected 'true' or 'string', got '{}'",
|
||||
value.type_name()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
If we got options for stdio handling, parse those as well
|
||||
|
||||
This may optionally contain configuration for any or all of: stdin, stdout, stderr
|
||||
*/
|
||||
this.stdio = value.get("stdio")?;
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessSpawnOptions {
|
||||
pub fn into_command(self, program: impl Into<OsString>, args: ProcessArgs) -> Command {
|
||||
let mut program: OsString = program.into();
|
||||
let mut args = args.into_iter().collect::<Vec<_>>();
|
||||
|
||||
// Run a shell using the command param if wanted
|
||||
if let Some(shell) = self.shell {
|
||||
let mut shell_command = program.clone();
|
||||
for arg in args {
|
||||
shell_command.push(" ");
|
||||
shell_command.push(arg);
|
||||
}
|
||||
args = vec![OsString::from("-c"), shell_command];
|
||||
program = shell.into();
|
||||
}
|
||||
|
||||
// Create command with the wanted options
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args);
|
||||
|
||||
// Set dir to run in and env variables
|
||||
if let Some(cwd) = self.cwd {
|
||||
cmd.current_dir(cwd);
|
||||
}
|
||||
if !self.envs.is_empty() {
|
||||
cmd.envs(self.envs);
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use bstr::BString;
|
||||
use mlua::prelude::*;
|
||||
|
||||
use super::kind::ProcessSpawnOptionsStdioKind;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProcessSpawnOptionsStdio {
|
||||
pub stdout: ProcessSpawnOptionsStdioKind,
|
||||
pub stderr: ProcessSpawnOptionsStdioKind,
|
||||
pub stdin: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl From<ProcessSpawnOptionsStdioKind> for ProcessSpawnOptionsStdio {
|
||||
fn from(value: ProcessSpawnOptionsStdioKind) -> Self {
|
||||
Self {
|
||||
stdout: value,
|
||||
stderr: value,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for ProcessSpawnOptionsStdio {
|
||||
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
|
||||
match value {
|
||||
LuaValue::Nil => Ok(Self::default()),
|
||||
LuaValue::String(s) => {
|
||||
Ok(ProcessSpawnOptionsStdioKind::from_lua(LuaValue::String(s), lua)?.into())
|
||||
}
|
||||
LuaValue::Table(t) => {
|
||||
let mut this = Self::default();
|
||||
|
||||
if let Some(stdin) = t.get::<Option<BString>>("stdin")? {
|
||||
this.stdin = Some(stdin.to_vec());
|
||||
}
|
||||
|
||||
if let Some(stdout) = t.get("stdout")? {
|
||||
this.stdout = stdout;
|
||||
}
|
||||
|
||||
if let Some(stderr) = t.get("stderr")? {
|
||||
this.stderr = stderr;
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
_ => Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "ProcessSpawnOptionsStdio".to_string(),
|
||||
message: Some(format!(
|
||||
"Invalid spawn options stdio - expected string or table, got {}",
|
||||
value.type_name()
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,362 +0,0 @@
|
|||
export type OS = "linux" | "macos" | "windows"
|
||||
export type Arch = "x86_64" | "aarch64"
|
||||
export type Endianness = "big" | "little"
|
||||
|
||||
--[=[
|
||||
@interface ExecStdioKind
|
||||
@within Process
|
||||
|
||||
Enum determining how to treat a standard input/output stream for `process.exec`.
|
||||
|
||||
Can be one of the following values:
|
||||
|
||||
* `default` - The default behavior, writing to the final result table only
|
||||
* `inherit` - Inherit the stream from the parent process, writing to both the result table and the respective stream for the parent process
|
||||
* `forward` - Forward the stream to the parent process, without writing to the result table, only respective stream for the parent process
|
||||
* `none` - Do not create any input/output stream
|
||||
]=]
|
||||
export type ExecStdioKind = "default" | "inherit" | "forward" | "none"
|
||||
|
||||
--[=[
|
||||
@interface ExecStdioOptions
|
||||
@within Process
|
||||
|
||||
A dictionary of stdio-specific options for `process.exec`, with the following available values:
|
||||
|
||||
* `stdin` - A buffer or string to write to the stdin of the process
|
||||
* `stdout` - How to treat the stdout stream from the child process - see `ExecStdioKind` for more info
|
||||
* `stderr` - How to treat the stderr stream from the child process - see `ExecStdioKind` for more info
|
||||
]=]
|
||||
export type ExecStdioOptions = {
|
||||
stdin: (buffer | string)?,
|
||||
stdout: ExecStdioKind?,
|
||||
stderr: ExecStdioKind?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface ExecOptions
|
||||
@within Process
|
||||
|
||||
A dictionary of options for `process.exec`, with the following available values:
|
||||
|
||||
* `cwd` - The current working directory for the process
|
||||
* `env` - Extra environment variables to give to the process
|
||||
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
|
||||
* `stdio` - How to treat output and error streams from the child process - see `StdioKind` and `StdioOptions` for more info
|
||||
]=]
|
||||
export type ExecOptions = {
|
||||
cwd: string?,
|
||||
env: { [string]: string }?,
|
||||
shell: (boolean | string)?,
|
||||
stdio: (ExecStdioKind | ExecStdioOptions)?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface CreateOptions
|
||||
@within Process
|
||||
|
||||
A dictionary of options for `process.create`, with the following available values:
|
||||
|
||||
* `cwd` - The current working directory for the process
|
||||
* `env` - Extra environment variables to give to the process
|
||||
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
|
||||
]=]
|
||||
export type CreateOptions = {
|
||||
cwd: string?,
|
||||
env: { [string]: string }?,
|
||||
shell: (boolean | string)?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@class ChildProcessReader
|
||||
@within Process
|
||||
|
||||
A reader class to read data from a child process' streams in realtime.
|
||||
]=]
|
||||
local ChildProcessReader = {}
|
||||
|
||||
--[=[
|
||||
@within ChildProcessReader
|
||||
|
||||
Reads a chunk of data up to the specified length, or a default of 1KB at a time.
|
||||
|
||||
Returns nil if there is no more data to read.
|
||||
|
||||
This function may yield until there is new data to read from reader, if all data
|
||||
till present has already been read, and the process has not exited.
|
||||
|
||||
@return The string containing the data read from the reader
|
||||
]=]
|
||||
function ChildProcessReader:read(chunkSize: number?): string?
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within ChildProcessReader
|
||||
|
||||
Reads all the data currently present in the reader as a string.
|
||||
This function will yield until the process exits.
|
||||
|
||||
@return The string containing the data read from the reader
|
||||
]=]
|
||||
function ChildProcessReader:readToEnd(): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@class ChildProcessWriter
|
||||
@within Process
|
||||
|
||||
A writer class to write data to a child process' streams in realtime.
|
||||
]=]
|
||||
local ChildProcessWriter = {}
|
||||
|
||||
--[=[
|
||||
@within ChildProcessWriter
|
||||
|
||||
Writes a buffer or string of data to the writer.
|
||||
|
||||
@param data The data to write to the writer
|
||||
]=]
|
||||
function ChildProcessWriter:write(data: buffer | string): ()
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within ChildProcessWriter
|
||||
|
||||
Closes the underlying I/O stream for the writer.
|
||||
]=]
|
||||
function ChildProcessWriter:close(): ()
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@interface ChildProcess
|
||||
@within Process
|
||||
|
||||
Result type for child processes in `process.create`.
|
||||
|
||||
This is a dictionary containing the following values:
|
||||
|
||||
* `stdin` - A writer to write to the child process' stdin - see `ChildProcessWriter` for more info
|
||||
* `stdout` - A reader to read from the child process' stdout - see `ChildProcessReader` for more info
|
||||
* `stderr` - A reader to read from the child process' stderr - see `ChildProcessReader` for more info
|
||||
* `kill` - A method that kills the child process
|
||||
* `status` - A method that yields and returns the exit status of the child process
|
||||
]=]
|
||||
export type ChildProcess = {
|
||||
stdin: typeof(ChildProcessWriter),
|
||||
stdout: typeof(ChildProcessReader),
|
||||
stderr: typeof(ChildProcessReader),
|
||||
kill: (self: ChildProcess) -> (),
|
||||
status: (self: ChildProcess) -> {
|
||||
ok: boolean,
|
||||
code: number,
|
||||
},
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface ExecResult
|
||||
@within Process
|
||||
|
||||
Result type for child processes in `process.exec`.
|
||||
|
||||
This is a dictionary containing the following values:
|
||||
|
||||
* `ok` - If the child process exited successfully or not, meaning the exit code was zero or not set
|
||||
* `code` - The exit code set by the child process, or 0 if one was not set
|
||||
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
|
||||
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
|
||||
]=]
|
||||
export type ExecResult = {
|
||||
ok: boolean,
|
||||
code: number,
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@class Process
|
||||
|
||||
Built-in functions for the current process & child processes
|
||||
|
||||
### Example usage
|
||||
|
||||
```lua
|
||||
local process = require("@lune/process")
|
||||
|
||||
-- Getting the arguments passed to the Lune script
|
||||
for index, arg in process.args do
|
||||
print("Process argument #" .. tostring(index) .. ": " .. arg)
|
||||
end
|
||||
|
||||
-- Getting the currently available environment variables
|
||||
local PORT: string? = process.env.PORT
|
||||
local HOME: string? = process.env.HOME
|
||||
for name, value in process.env do
|
||||
print("Environment variable " .. name .. " is set to " .. value)
|
||||
end
|
||||
|
||||
-- Getting the current os and processor architecture
|
||||
print("Running " .. process.os .. " on " .. process.arch .. "!")
|
||||
|
||||
-- Executing a command
|
||||
local result = process.exec("program", {
|
||||
"cli argument",
|
||||
"other cli argument"
|
||||
})
|
||||
if result.ok then
|
||||
print(result.stdout)
|
||||
else
|
||||
print(result.stderr)
|
||||
end
|
||||
|
||||
-- Spawning a child process
|
||||
local child = process.create("program", {
|
||||
"cli argument",
|
||||
"other cli argument"
|
||||
})
|
||||
|
||||
-- Writing to the child process' stdin
|
||||
child.stdin:write("Hello from Lune!")
|
||||
|
||||
-- Reading from the child process' stdout
|
||||
local data = child.stdout:read()
|
||||
print(data)
|
||||
```
|
||||
]=]
|
||||
local process = {}
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
@prop os OS
|
||||
@tag read_only
|
||||
|
||||
The current operating system being used.
|
||||
|
||||
Possible values:
|
||||
|
||||
* `"linux"`
|
||||
* `"macos"`
|
||||
* `"windows"`
|
||||
]=]
|
||||
process.os = (nil :: any) :: OS
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
@prop arch Arch
|
||||
@tag read_only
|
||||
|
||||
The architecture of the processor currently being used.
|
||||
|
||||
Possible values:
|
||||
|
||||
* `"x86_64"`
|
||||
* `"aarch64"`
|
||||
]=]
|
||||
process.arch = (nil :: any) :: Arch
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
@prop endianness Endianness
|
||||
@tag read_only
|
||||
|
||||
The endianness of the processor currently being used.
|
||||
|
||||
Possible values:
|
||||
|
||||
* `"big"`
|
||||
* `"little"`
|
||||
]=]
|
||||
process.endianness = (nil :: any) :: Endianness
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
@prop args { string }
|
||||
@tag read_only
|
||||
|
||||
The arguments given when running the Lune script.
|
||||
]=]
|
||||
process.args = (nil :: any) :: { string }
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
@prop cwd string
|
||||
@tag read_only
|
||||
|
||||
The current working directory in which the Lune script is running.
|
||||
]=]
|
||||
process.cwd = (nil :: any) :: string
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
@prop env { [string]: string? }
|
||||
@tag read_write
|
||||
|
||||
Current environment variables for this process.
|
||||
|
||||
Setting a value on this table will set the corresponding environment variable.
|
||||
]=]
|
||||
process.env = (nil :: any) :: { [string]: string? }
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
|
||||
Exits the currently running script as soon as possible with the given exit code.
|
||||
|
||||
Exit code 0 is treated as a successful exit, any other value is treated as an error.
|
||||
|
||||
Setting the exit code using this function will override any otherwise automatic exit code.
|
||||
|
||||
@param code The exit code to set
|
||||
]=]
|
||||
function process.exit(code: number?): never
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
|
||||
Spawns a child process in the background that runs the program `program`,
|
||||
and immediately returns readers and writers to communicate with it.
|
||||
|
||||
In order to execute a command and wait for its output, see `process.exec`.
|
||||
|
||||
The second argument, `params`, can be passed as a list of string parameters to give to the program.
|
||||
|
||||
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
|
||||
Refer to the documentation for `SpawnOptions` for specific option keys and their values.
|
||||
|
||||
@param program The program to Execute as a child process
|
||||
@param params Additional parameters to pass to the program
|
||||
@param options A dictionary of options for the child process
|
||||
@return A dictionary with the readers and writers to communicate with the child process
|
||||
]=]
|
||||
function process.create(program: string, params: { string }?, options: CreateOptions?): ChildProcess
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within Process
|
||||
|
||||
Executes a child process that will execute the command `program`, waiting for it to exit.
|
||||
Upon exit, it returns a dictionary that describes the final status and ouput of the child process.
|
||||
|
||||
In order to spawn a child process in the background, see `process.create`.
|
||||
|
||||
The second argument, `params`, can be passed as a list of string parameters to give to the program.
|
||||
|
||||
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
|
||||
Refer to the documentation for `ExecOptions` for specific option keys and their values.
|
||||
|
||||
@param program The program to Execute as a child process
|
||||
@param params Additional parameters to pass to the program
|
||||
@param options A dictionary of options for the child process
|
||||
@return A dictionary representing the result of the child process
|
||||
]=]
|
||||
function process.exec(program: string, params: { string }?, options: ExecOptions?): ExecResult
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
return process
|
|
@ -1,21 +0,0 @@
|
|||
[package]
|
||||
name = "lune-std-regex"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - RegEx"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.10.3", features = ["luau"] }
|
||||
|
||||
regex = "1.10"
|
||||
self_cell = "1.0"
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|
|
@ -1,91 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use regex::{Captures, Regex};
|
||||
use self_cell::self_cell;
|
||||
|
||||
use super::matches::LuaMatch;
|
||||
|
||||
type OptionalCaptures<'a> = Option<Captures<'a>>;
|
||||
|
||||
self_cell! {
|
||||
struct LuaCapturesInner {
|
||||
owner: Arc<String>,
|
||||
#[covariant]
|
||||
dependent: OptionalCaptures,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A wrapper over the `regex::Captures` struct that can be used from Lua.
|
||||
*/
|
||||
pub struct LuaCaptures {
|
||||
inner: LuaCapturesInner,
|
||||
}
|
||||
|
||||
impl LuaCaptures {
|
||||
/**
|
||||
Create a new `LuaCaptures` instance from a `Regex` pattern and a `String` text.
|
||||
|
||||
Returns `Some(_)` if captures were found, `None` if no captures were found.
|
||||
*/
|
||||
pub fn new(pattern: &Regex, text: String) -> Option<Self> {
|
||||
let inner =
|
||||
LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str()));
|
||||
if inner.borrow_dependent().is_some() {
|
||||
Some(Self { inner })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn captures(&self) -> &Captures {
|
||||
self.inner
|
||||
.borrow_dependent()
|
||||
.as_ref()
|
||||
.expect("None captures should not be used")
|
||||
}
|
||||
|
||||
fn num_captures(&self) -> usize {
|
||||
// NOTE: Here we exclude the match for the entire regex
|
||||
// pattern, only counting the named and numbered captures
|
||||
self.captures().len() - 1
|
||||
}
|
||||
|
||||
fn text(&self) -> Arc<String> {
|
||||
Arc::clone(self.inner.borrow_owner())
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for LuaCaptures {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_method("get", |_, this, index: usize| {
|
||||
Ok(this
|
||||
.captures()
|
||||
.get(index)
|
||||
.map(|m| LuaMatch::new(this.text(), m)))
|
||||
});
|
||||
|
||||
methods.add_method("group", |_, this, group: String| {
|
||||
Ok(this
|
||||
.captures()
|
||||
.name(&group)
|
||||
.map(|m| LuaMatch::new(this.text(), m)))
|
||||
});
|
||||
|
||||
methods.add_method("format", |_, this, format: String| {
|
||||
let mut new = String::new();
|
||||
this.captures().expand(&format, &mut new);
|
||||
Ok(new)
|
||||
});
|
||||
|
||||
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures()));
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
||||
Ok(format!("{}", this.num_captures()))
|
||||
});
|
||||
}
|
||||
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures");
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
mod captures;
|
||||
mod matches;
|
||||
mod regex;
|
||||
|
||||
use self::regex::LuaRegex;
|
||||
|
||||
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
|
||||
|
||||
/**
|
||||
Returns a string containing type definitions for the `regex` standard library.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn typedefs() -> String {
|
||||
TYPEDEFS.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Creates the `regex` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", new_regex)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
fn new_regex(_: &Lua, pattern: String) -> LuaResult<LuaRegex> {
|
||||
LuaRegex::new(pattern)
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use regex::Match;
|
||||
|
||||
/**
|
||||
A wrapper over the `regex::Match` struct that can be used from Lua.
|
||||
*/
|
||||
pub struct LuaMatch {
|
||||
text: Arc<String>,
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl LuaMatch {
|
||||
/**
|
||||
Create a new `LuaMatch` instance from a `String` text and a `regex::Match`.
|
||||
*/
|
||||
pub fn new(text: Arc<String>, matched: Match) -> Self {
|
||||
Self {
|
||||
text,
|
||||
start: matched.start(),
|
||||
end: matched.end(),
|
||||
}
|
||||
}
|
||||
|
||||
fn range(&self) -> Range<usize> {
|
||||
self.start..self.end
|
||||
}
|
||||
|
||||
fn slice(&self) -> &str {
|
||||
&self.text[self.range()]
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for LuaMatch {
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
// NOTE: Strings are 0 based in Rust but 1 based in Luau, and end of range in Rust is exclusive
|
||||
fields.add_field_method_get("start", |_, this| Ok(this.start.saturating_add(1)));
|
||||
fields.add_field_method_get("finish", |_, this| Ok(this.end));
|
||||
fields.add_field_method_get("len", |_, this| Ok(this.range().len()));
|
||||
fields.add_field_method_get("text", |_, this| Ok(this.slice().to_string()));
|
||||
|
||||
fields.add_meta_field(LuaMetaMethod::Type, "RegexMatch");
|
||||
}
|
||||
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len()));
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
||||
Ok(this.slice().to_string())
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use regex::Regex;
|
||||
|
||||
use super::{captures::LuaCaptures, matches::LuaMatch};
|
||||
|
||||
/**
|
||||
A wrapper over the `regex::Regex` struct that can be used from Lua.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LuaRegex {
|
||||
inner: Regex,
|
||||
}
|
||||
|
||||
impl LuaRegex {
|
||||
/**
|
||||
Create a new `LuaRegex` instance from a `String` pattern.
|
||||
*/
|
||||
pub fn new(pattern: String) -> LuaResult<Self> {
|
||||
Regex::new(&pattern)
|
||||
.map(|inner| Self { inner })
|
||||
.map_err(LuaError::external)
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for LuaRegex {
|
||||
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_method("isMatch", |_, this, text: String| {
|
||||
Ok(this.inner.is_match(&text))
|
||||
});
|
||||
|
||||
methods.add_method("find", |_, this, text: String| {
|
||||
let arc = Arc::new(text);
|
||||
Ok(this
|
||||
.inner
|
||||
.find(&arc)
|
||||
.map(|m| LuaMatch::new(Arc::clone(&arc), m)))
|
||||
});
|
||||
|
||||
methods.add_method("captures", |_, this, text: String| {
|
||||
Ok(LuaCaptures::new(&this.inner, text))
|
||||
});
|
||||
|
||||
methods.add_method("split", |_, this, text: String| {
|
||||
Ok(this
|
||||
.inner
|
||||
.split(&text)
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>())
|
||||
});
|
||||
|
||||
// TODO: Determine whether it's desirable and / or feasible to support
|
||||
// using a function or table for `replace` like in the lua string library
|
||||
methods.add_method(
|
||||
"replace",
|
||||
|_, this, (haystack, replacer): (String, String)| {
|
||||
Ok(this.inner.replace(&haystack, replacer).to_string())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"replaceAll",
|
||||
|_, this, (haystack, replacer): (String, String)| {
|
||||
Ok(this.inner.replace_all(&haystack, replacer).to_string())
|
||||
},
|
||||
);
|
||||
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
||||
Ok(this.inner.as_str().to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_meta_field(LuaMetaMethod::Type, "Regex");
|
||||
}
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
--[=[
|
||||
@class RegexMatch
|
||||
|
||||
A match from a regular expression.
|
||||
|
||||
Contains the following values:
|
||||
|
||||
- `start` -- The start index of the match in the original string.
|
||||
- `finish` -- The end index of the match in the original string.
|
||||
- `text` -- The text that was matched.
|
||||
- `len` -- The length of the text that was matched.
|
||||
]=]
|
||||
local RegexMatch = {
|
||||
start = 0,
|
||||
finish = 0,
|
||||
text = "",
|
||||
len = 0,
|
||||
}
|
||||
|
||||
type RegexMatch = typeof(RegexMatch)
|
||||
|
||||
local RegexCaptures = {}
|
||||
|
||||
function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch?
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch?
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
function RegexCaptures.format(self: RegexCaptures, format: string): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@class RegexCaptures
|
||||
|
||||
Captures from a regular expression.
|
||||
]=]
|
||||
export type RegexCaptures = typeof(setmetatable(
|
||||
{} :: {
|
||||
--[=[
|
||||
@within RegexCaptures
|
||||
@tag Method
|
||||
@method get
|
||||
|
||||
Returns the match at the given index, if one exists.
|
||||
|
||||
@param index -- The index of the match to get
|
||||
@return RegexMatch -- The match, if one exists
|
||||
]=]
|
||||
|
||||
get: (self: RegexCaptures, index: number) -> RegexMatch?,
|
||||
|
||||
--[=[
|
||||
@within RegexCaptures
|
||||
@tag Method
|
||||
@method group
|
||||
|
||||
Returns the match for the given named match group, if one exists.
|
||||
|
||||
@param group -- The name of the group to get
|
||||
@return RegexMatch -- The match, if one exists
|
||||
]=]
|
||||
group: (self: RegexCaptures, group: string) -> RegexMatch?,
|
||||
|
||||
--[=[
|
||||
@within RegexCaptures
|
||||
@tag Method
|
||||
@method format
|
||||
|
||||
Formats the captures using the given format string.
|
||||
|
||||
### Example usage
|
||||
|
||||
```lua
|
||||
local regex = require("@lune/regex")
|
||||
|
||||
local re = regex.new("(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})")
|
||||
|
||||
local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb.");
|
||||
assert(caps ~= nil, "Example pattern should match example text")
|
||||
|
||||
local formatted = caps:format("year=$year, month=$month, day=$day")
|
||||
print(formatted) -- "year=2010, month=03, day=14"
|
||||
```
|
||||
|
||||
@param format -- The format string to use
|
||||
@return string -- The formatted string
|
||||
]=]
|
||||
format: (self: RegexCaptures, format: string) -> string,
|
||||
},
|
||||
{} :: {
|
||||
__len: (self: RegexCaptures) -> number,
|
||||
}
|
||||
))
|
||||
|
||||
local Regex = {}
|
||||
|
||||
--[=[
|
||||
@within Regex
|
||||
@tag Method
|
||||
|
||||
Check if the given text matches the regular expression.
|
||||
|
||||
This method may be slightly more efficient than calling `find`
|
||||
if you only need to know if the text matches the pattern.
|
||||
|
||||
@param text -- The text to search
|
||||
@return boolean -- Whether the text matches the pattern
|
||||
]=]
|
||||
function Regex.isMatch(self: Regex, text: string): boolean
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within Regex
|
||||
@tag Method
|
||||
|
||||
Finds the first match in the given text.
|
||||
|
||||
Returns `nil` if no match was found.
|
||||
|
||||
@param text -- The text to search
|
||||
@return RegexMatch? -- The match object
|
||||
]=]
|
||||
function Regex.find(self: Regex, text: string): RegexMatch?
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within Regex
|
||||
@tag Method
|
||||
|
||||
Finds all matches in the given text as a `RegexCaptures` object.
|
||||
|
||||
Returns `nil` if no matches are found.
|
||||
|
||||
@param text -- The text to search
|
||||
@return RegexCaptures? -- The captures object
|
||||
]=]
|
||||
function Regex.captures(self: Regex, text: string): RegexCaptures?
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within Regex
|
||||
@tag Method
|
||||
|
||||
Splits the given text using the regular expression.
|
||||
|
||||
@param text -- The text to split
|
||||
@return { string } -- The split text
|
||||
]=]
|
||||
function Regex.split(self: Regex, text: string): { string }
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within Regex
|
||||
@tag Method
|
||||
|
||||
Replaces the first match in the given text with the given replacer string.
|
||||
|
||||
@param haystack -- The text to search
|
||||
@param replacer -- The string to replace matches with
|
||||
@return string -- The text with the first match replaced
|
||||
]=]
|
||||
function Regex.replace(self: Regex, haystack: string, replacer: string): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within Regex
|
||||
@tag Method
|
||||
|
||||
Replaces all matches in the given text with the given replacer string.
|
||||
|
||||
@param haystack -- The text to search
|
||||
@param replacer -- The string to replace matches with
|
||||
@return string -- The text with all matches replaced
|
||||
]=]
|
||||
function Regex.replaceAll(self: Regex, haystack: string, replacer: string): string
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
export type Regex = typeof(Regex)
|
||||
|
||||
--[=[
|
||||
@class Regex
|
||||
|
||||
Built-in library for regular expressions
|
||||
|
||||
### Example 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
|
||||
```
|
||||
]=]
|
||||
local regex = {}
|
||||
|
||||
--[=[
|
||||
@within Regex
|
||||
@tag Constructor
|
||||
|
||||
Creates a new `Regex` from a given string pattern.
|
||||
|
||||
### Errors
|
||||
|
||||
This constructor throws an error if the given pattern is invalid.
|
||||
|
||||
@param pattern -- The string pattern to use
|
||||
@return Regex -- The new Regex object
|
||||
]=]
|
||||
function regex.new(pattern: string): Regex
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
return regex
|
|
@ -1,23 +0,0 @@
|
|||
[package]
|
||||
name = "lune-std-roblox"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - Roblox"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.10.3", features = ["luau"] }
|
||||
mlua-luau-scheduler = { version = "0.1.2", path = "../mlua-luau-scheduler" }
|
||||
|
||||
rbx_cookie = { version = "0.1.4", default-features = false }
|
||||
roblox_install = "1.0"
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|
||||
lune-roblox = { version = "0.2.2", path = "../lune-roblox" }
|
|
@ -1,183 +0,0 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use mlua_luau_scheduler::LuaSpawnExt;
|
||||
|
||||
use lune_roblox::{
|
||||
document::{Document, DocumentError, DocumentFormat, DocumentKind},
|
||||
instance::{registry::InstanceRegistry, Instance},
|
||||
reflection::Database as ReflectionDatabase,
|
||||
};
|
||||
|
||||
static REFLECTION_DATABASE: OnceLock<ReflectionDatabase> = OnceLock::new();
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
use roblox_install::RobloxStudio;
|
||||
|
||||
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
|
||||
|
||||
/**
|
||||
Returns a string containing type definitions for the `roblox` standard library.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn typedefs() -> String {
|
||||
TYPEDEFS.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Creates the `roblox` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
||||
let mut roblox_constants = Vec::new();
|
||||
|
||||
let roblox_module = lune_roblox::module(lua.clone())?;
|
||||
for pair in roblox_module.pairs::<LuaValue, LuaValue>() {
|
||||
roblox_constants.push(pair?);
|
||||
}
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_values(roblox_constants)?
|
||||
.with_async_function("deserializePlace", deserialize_place)?
|
||||
.with_async_function("deserializeModel", deserialize_model)?
|
||||
.with_async_function("serializePlace", serialize_place)?
|
||||
.with_async_function("serializeModel", serialize_model)?
|
||||
.with_function("getAuthCookie", get_auth_cookie)?
|
||||
.with_function("getReflectionDatabase", get_reflection_database)?
|
||||
.with_function("implementProperty", implement_property)?
|
||||
.with_function("implementMethod", implement_method)?
|
||||
.with_function("studioApplicationPath", studio_application_path)?
|
||||
.with_function("studioContentPath", studio_content_path)?
|
||||
.with_function("studioPluginPath", studio_plugin_path)?
|
||||
.with_function("studioBuiltinPluginPath", studio_builtin_plugin_path)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
async fn deserialize_place(lua: Lua, contents: LuaString) -> LuaResult<LuaValue> {
|
||||
let bytes = contents.as_bytes().to_vec();
|
||||
let fut = lua.spawn_blocking(move || {
|
||||
let doc = Document::from_bytes(bytes, DocumentKind::Place)?;
|
||||
let data_model = doc.into_data_model_instance()?;
|
||||
Ok::<_, DocumentError>(data_model)
|
||||
});
|
||||
fut.await.into_lua_err()?.into_lua(&lua)
|
||||
}
|
||||
|
||||
async fn deserialize_model(lua: Lua, contents: LuaString) -> LuaResult<LuaValue> {
|
||||
let bytes = contents.as_bytes().to_vec();
|
||||
let fut = lua.spawn_blocking(move || {
|
||||
let doc = Document::from_bytes(bytes, DocumentKind::Model)?;
|
||||
let instance_array = doc.into_instance_array()?;
|
||||
Ok::<_, DocumentError>(instance_array)
|
||||
});
|
||||
fut.await.into_lua_err()?.into_lua(&lua)
|
||||
}
|
||||
|
||||
async fn serialize_place(
|
||||
lua: Lua,
|
||||
(data_model, as_xml): (LuaUserDataRef<Instance>, Option<bool>),
|
||||
) -> LuaResult<LuaString> {
|
||||
let data_model = *data_model;
|
||||
let fut = lua.spawn_blocking(move || {
|
||||
let doc = Document::from_data_model_instance(data_model)?;
|
||||
let bytes = doc.to_bytes_with_format(match as_xml {
|
||||
Some(true) => DocumentFormat::Xml,
|
||||
_ => DocumentFormat::Binary,
|
||||
})?;
|
||||
Ok::<_, DocumentError>(bytes)
|
||||
});
|
||||
let bytes = fut.await.into_lua_err()?;
|
||||
lua.create_string(bytes)
|
||||
}
|
||||
|
||||
async fn serialize_model(
|
||||
lua: Lua,
|
||||
(instances, as_xml): (Vec<LuaUserDataRef<Instance>>, Option<bool>),
|
||||
) -> LuaResult<LuaString> {
|
||||
let instances = instances.iter().map(|i| **i).collect();
|
||||
let fut = lua.spawn_blocking(move || {
|
||||
let doc = Document::from_instance_array(instances)?;
|
||||
let bytes = doc.to_bytes_with_format(match as_xml {
|
||||
Some(true) => DocumentFormat::Xml,
|
||||
_ => DocumentFormat::Binary,
|
||||
})?;
|
||||
Ok::<_, DocumentError>(bytes)
|
||||
});
|
||||
let bytes = fut.await.into_lua_err()?;
|
||||
lua.create_string(bytes)
|
||||
}
|
||||
|
||||
fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
|
||||
if matches!(raw, Some(true)) {
|
||||
Ok(rbx_cookie::get_value())
|
||||
} else {
|
||||
Ok(rbx_cookie::get())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
|
||||
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
|
||||
}
|
||||
|
||||
fn implement_property(
|
||||
lua: &Lua,
|
||||
(class_name, property_name, property_getter, property_setter): (
|
||||
String,
|
||||
String,
|
||||
LuaFunction,
|
||||
Option<LuaFunction>,
|
||||
),
|
||||
) -> LuaResult<()> {
|
||||
let property_setter = if let Some(setter) = property_setter {
|
||||
setter
|
||||
} else {
|
||||
let property_name = property_name.clone();
|
||||
lua.create_function(move |_, _: LuaMultiValue| {
|
||||
Err::<(), _>(LuaError::runtime(format!(
|
||||
"Property '{property_name}' is read-only"
|
||||
)))
|
||||
})?
|
||||
};
|
||||
InstanceRegistry::insert_property_getter(lua, &class_name, &property_name, property_getter)
|
||||
.into_lua_err()?;
|
||||
InstanceRegistry::insert_property_setter(lua, &class_name, &property_name, property_setter)
|
||||
.into_lua_err()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn implement_method(
|
||||
lua: &Lua,
|
||||
(class_name, method_name, method): (String, String, LuaFunction),
|
||||
) -> LuaResult<()> {
|
||||
InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn studio_application_path(_: &Lua, _: ()) -> LuaResult<String> {
|
||||
RobloxStudio::locate()
|
||||
.map(|rs| rs.application_path().display().to_string())
|
||||
.map_err(LuaError::external)
|
||||
}
|
||||
|
||||
fn studio_content_path(_: &Lua, _: ()) -> LuaResult<String> {
|
||||
RobloxStudio::locate()
|
||||
.map(|rs| rs.content_path().display().to_string())
|
||||
.map_err(LuaError::external)
|
||||
}
|
||||
|
||||
fn studio_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
|
||||
RobloxStudio::locate()
|
||||
.map(|rs| rs.plugins_path().display().to_string())
|
||||
.map_err(LuaError::external)
|
||||
}
|
||||
|
||||
fn studio_builtin_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
|
||||
RobloxStudio::locate()
|
||||
.map(|rs| rs.built_in_plugins_path().display().to_string())
|
||||
.map_err(LuaError::external)
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
[package]
|
||||
name = "lune-std-serde"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - Serde"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.10.3", features = ["luau", "serialize", "error-send"] }
|
||||
|
||||
async-compression = { version = "0.4", features = [
|
||||
"futures-io",
|
||||
"brotli",
|
||||
"deflate",
|
||||
"gzip",
|
||||
"zlib",
|
||||
] }
|
||||
|
||||
blocking = "1.6"
|
||||
bstr = "1.9"
|
||||
futures-lite = "2.6"
|
||||
lz4 = "1.26"
|
||||
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"] }
|
||||
|
||||
digest = "0.10.7"
|
||||
hmac = "0.12.1"
|
||||
md-5 = "0.10.6"
|
||||
sha1 = "0.10.6"
|
||||
sha2 = "0.10.8"
|
||||
sha3 = "0.10.8"
|
||||
# This feature MIGHT break due to the unstable nature of the digest crate.
|
||||
# Check before updating it.
|
||||
blake3 = { version = "=1.5.0", features = ["traits-preview"] }
|
||||
|
||||
lune-utils = { version = "0.2.2", path = "../lune-utils" }
|