Compare commits

..

No commits in common. "main" and "v0.7.0" have entirely different histories.
main ... v0.7.0

409 changed files with 13049 additions and 24118 deletions

7
.gitattributes vendored
View file

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

5
.gitbook.yaml Normal file
View file

@ -0,0 +1,5 @@
root: ./docs
structure:
readme: ./README.md
summary: ./SUMMARY.md

View file

@ -5,97 +5,36 @@ on:
pull_request:
workflow_dispatch:
defaults:
run:
shell: bash
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 project tools
uses: ok-nick/setup-aftman@v0.3.0
- 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 test \
--lib --workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Test - Lune
run: just test
- name: Test - CLI
run: just test-cli

24
.github/workflows/publish.yaml vendored Normal file
View 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

View file

@ -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
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 }}
file: Cargo.toml
field: workspace.package.version
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 }}

19
.gitignore vendored
View file

@ -3,17 +3,12 @@
/.DS_Store
/**/.DS_Store
# Autogenerated dirs
/bin
/out
/target
/staging
/**/bin
/**/out
/**/target
/**/staging
/gitbook
# Autogenerated files
@ -21,7 +16,11 @@ lune.yml
luneDocs.json
luneTypes.d.luau
# NOTE: Don't remove the typedefs file in the docs dir!
# We depend on it to autogenerate all of the above files.
!/docs/luneTypes.d.luau
# Files generated by runtime or build scripts
scripts/brick_color.rs
scripts/font_enum_map.rs
scripts/physical_properties_enum_map.rs
packages/lib-roblox/scripts/brick_color.rs
packages/lib-roblox/scripts/font_enum_map.rs
packages/lib-roblox/scripts/physical_properties_enum_map.rs

143
.justfile
View file

@ -1,129 +1,34 @@
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}}
test:
cargo test --package lune -- --test-threads 1
# 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}}
# Run tests for the Lune CLI
test-cli:
cargo test --package lune-cli
# Apply formatting for all Rust & Luau files
[no-exit-message]
fmt:
#!/usr/bin/env bash
set -euo pipefail
stylua .lune scripts tests types \
--glob "tests/**/*.luau" \
--glob "!tests/roblox/rbx-test-files/**"
cargo fmt
# Generate gitbook directory
generate-gitbook:
rm -rf ./gitbook
# Check formatting for all Rust & Luau files
[no-exit-message]
fmt-check:
#!/usr/bin/env bash
set -euo pipefail
stylua .lune scripts tests types \
--glob "tests/**/*.luau" \
--glob "!tests/roblox/rbx-test-files/**"
cargo fmt --check
mkdir gitbook
mkdir gitbook/docs
# Analyze and lint Luau files using luau-lsp
[no-exit-message]
analyze:
#!/usr/bin/env bash
set -euo pipefail
luau-lsp analyze \
--settings=".vscode/settings.json" \
--ignore="tests/roblox/rbx-test-files/**" \
.lune scripts tests types
cp -R docs gitbook
cp README.md gitbook/docs/README.md
cp .gitbook.yaml gitbook/.gitbook.yaml
# 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
rm -rf gitbook/docs/typedefs
# 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
cargo run -- --generate-gitbook-dir
# Publish gitbook directory to gitbook branch
publish-gitbook: generate-gitbook
npx push-dir --dir=gitbook --branch=gitbook

View file

@ -7,10 +7,5 @@
"typeErrors": true,
"globals": [
"warn"
],
"aliases": {
"lune": "./types/",
"tests": "./tests",
"require-tests": "./tests/require/tests"
}
]
}

View file

@ -129,7 +129,7 @@ end
]]
print("Sending 4 pings to google 🌏")
local result = process.exec("ping", {
local result = process.spawn("ping", {
"google.com",
"-c 4",
})
@ -171,7 +171,7 @@ local apiResult = net.request({
method = "PATCH",
headers = {
["Content-Type"] = "application/json",
} :: { [string]: string },
},
body = net.jsonEncode({
title = "foo",
body = "bar",

View file

@ -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! 🌙")

View file

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

18
.vscode/settings.json vendored
View file

@ -1,17 +1,27 @@
{
// 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/"
"luau-lsp.require.fileAliases": {
"@lune/fs": "./docs/typedefs/FS.luau",
"@lune/net": "./docs/typedefs/Net.luau",
"@lune/process": "./docs/typedefs/Process.luau",
"@lune/roblox": "./docs/typedefs/Roblox.luau",
"@lune/serde": "./docs/typedefs/Serde.luau",
"@lune/stdio": "./docs/typedefs/Stdio.luau",
"@lune/task": "./docs/typedefs/Task.luau"
},
// 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,

View file

@ -8,712 +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).
## `0.9.0`
### Breaking changes
- Added two new process spawning functions - `process.create` and `process.exec`, removing the previous `process.spawn` API completely. ([#211])
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function.
The new `process.create` function is a non-blocking process creation API and can be used to interactively
read and write stdio of the process.
```lua
local child = process.create("program", {
"cli-argument",
"other-cli-argument"
})
-- Writing to stdin
child.stdin:write("Hello from Lune!")
-- Reading from stdout
local data = child.stdout:read()
print(buffer.tostring(data))
```
- WebSocket methods in `net.socket` and `net.serve` now use standard Lua method calling convention and colon syntax.
This means `socket.send(...)` is now `socket:send(...)`, `socket.close(...)` is now `socket:close(...)`, and so on.
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
### Changed
- Documentation comments for several standard library properties have been improved ([#248], [#250])
- Error messages no longer contain redundant or duplicate stack trace information
[#178]: https://github.com/lune-org/lune/pull/178
[#211]: https://github.com/lune-org/lune/pull/211
[#248]: https://github.com/lune-org/lune/pull/248
[#250]: https://github.com/lune-org/lune/pull/250
## `0.8.9` - October 7th, 2024
### Changed
- Updated to Luau version `0.640`
## `0.8.8` - August 22nd, 2024
### Fixed
- Fixed errors when deserializing `Lighting.AttributesSerialize` by updating `rbx-dom` dependencies ([#245])
[#245]: https://github.com/lune-org/lune/pull/245
## `0.8.7` - August 10th, 2024
### Added
- Added a compression level option to `serde.compress` ([#224])
- Added missing vector methods to the `roblox` library ([#228])
### Changed
- Updated to Luau version `0.635`
- Updated to rbx-dom database version `0.634`
### Fixed
- Fixed `fs.readDir` with trailing forward-slash on Windows ([#220])
- Fixed `__type` and `__tostring` metamethods not always being respected when formatting tables
[#220]: https://github.com/lune-org/lune/pull/220
[#224]: https://github.com/lune-org/lune/pull/224
[#228]: https://github.com/lune-org/lune/pull/228
## `0.8.6` - June 23rd, 2024
### Added
- Added a builtin API for hashing and calculating HMACs as part of the `serde` library ([#193])
Basic usage:
```lua
local serde = require("@lune/serde")
local hash = serde.hash("sha256", "a message to hash")
local hmac = serde.hmac("sha256", "a message to hash", "a secret string")
print(hash)
print(hmac)
```
The returned hashes are sequences of lowercase hexadecimal digits. The following algorithms are supported:
`md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`, `blake3`
- Added two new options to `luau.load`:
- `codegenEnabled` - whether or not codegen should be enabled for the loaded chunk.
- `injectGlobals` - whether or not to inject globals into a passed `environment`.
By default, globals are injected and codegen is disabled.
Check the documentation for the `luau` standard library for more information.
- Implemented support for floor division operator / `__idiv` for the `Vector2` and `Vector3` types in the `roblox` standard library ([#196])
- Fixed the `_VERSION` global containing an incorrect Lune version string.
### Changed
- Sandboxing and codegen in the Luau VM is now fully enabled, resulting in up to 2x or faster code execution.
This should not result in any behavior differences in Lune, but if it does, please open an issue.
- Improved formatting of custom error objects (such as when `fs.readFile` returns an error) when printed or formatted using `stdio.format`.
### Fixed
- Fixed `__type` and `__tostring` metamethods on userdatas and tables not being respected when printed or formatted using `stdio.format`.
[#193]: https://github.com/lune-org/lune/pull/193
[#196]: https://github.com/lune-org/lune/pull/196
## `0.8.5` - June 1st, 2024
### Changed
- Improved table pretty formatting when using `print`, `warn`, and `stdio.format`:
- Keys are sorted numerically / alphabetically when possible.
- Keys of different types are put in distinct sections for mixed tables.
- Tables that are arrays no longer display their keys.
- Empty tables are no longer spread across lines.
## Fixed
- Fixed formatted values in tables not being separated by newlines.
- Fixed panicking (crashing) when using `process.spawn` with a program that does not exist.
- Fixed `instance:SetAttribute("name", nil)` throwing an error and not removing the attribute.
## `0.8.4` - May 12th, 2024
### Added
- Added a builtin API for regular expressions.
Example basic usage:
```lua
local Regex = require("@lune/regex")
local re = Regex.new("hello")
if re:isMatch("hello, world!") then
print("Matched!")
end
local caps = re:captures("hello, world! hello, again!")
print(#caps) -- 2
print(caps:get(1)) -- "hello"
print(caps:get(2)) -- "hello"
print(caps:get(3)) -- nil
```
Check out the documentation for more details.
- Added support for buffers as arguments in builtin APIs ([#148])
This includes APIs such as `fs.writeFile`, `serde.encode`, and more.
- Added support for cross-compilation of standalone binaries ([#162])
You can now compile standalone binaries for other platforms by passing
an additional `target` argument to the `build` subcommand:
```sh
lune build my-file.luau --output my-bin --target windows-x86_64
```
Currently supported targets are the same as the ones included with each
release of Lune on GitHub. Check releases for a full list of targets.
- Added `stdio.readToEnd()` for reading the entire stdin passed to Lune
### Changed
- Split the repository into modular crates instead of a monolith. ([#188])
If you previously depended on Lune as a crate, nothing about it has changed for version `0.8.4`, but now each individual sub-crate has also been published and is available for use:
- `lune` (old)
- `lune-utils`
- `lune-roblox`
- `lune-std-*` for every builtin library
When depending on the main `lune` crate, each builtin library also has a feature flag that can be toggled in the format `std-*`.
In general, this should mean that it is now much easier to make your own Lune builtin, publish your own flavor of a Lune CLI, or take advantage of all the work that has been done for Lune as a runtime when making your own Rust programs.
- Changed the `User-Agent` header in `net.request` to be more descriptive ([#186])
- Updated to Luau version `0.622`.
### Fixed
- Fixed not being able to decompress `lz4` format in high compression mode
- Fixed stack overflow for tables with circular keys ([#183])
- Fixed `net.serve` no longer accepting ipv6 addresses
- Fixed headers in `net.serve` being raw bytes instead of strings
[#148]: https://github.com/lune-org/lune/pull/148
[#162]: https://github.com/lune-org/lune/pull/162
[#183]: https://github.com/lune-org/lune/pull/183
[#186]: https://github.com/lune-org/lune/pull/186
[#188]: https://github.com/lune-org/lune/pull/188
## `0.8.3` - April 15th, 2024
### Fixed
- Fixed `require` not throwing syntax errors ([#168])
- Fixed `require` caching not working correctly ([#171])
- Fixed case-sensitivity issue in `require` with aliases ([#173])
- Fixed `itertools` dependency being marked optional even though it is mandatory ([#176])
- Fixed test cases for the `net` built-in library on Windows ([#177])
[#168]: https://github.com/lune-org/lune/pull/168
[#171]: https://github.com/lune-org/lune/pull/171
[#173]: https://github.com/lune-org/lune/pull/173
[#176]: https://github.com/lune-org/lune/pull/176
[#177]: https://github.com/lune-org/lune/pull/177
## `0.8.2` - March 12th, 2024
### Fixed
- Fixed REPL panicking after the first evaluation / run.
- Fixed globals reloading on each run in the REPL, causing unnecessary slowdowns.
- Fixed `net.serve` requests no longer being plain tables in Lune `0.8.1`, breaking usage of things such as `table.clone`.
## `0.8.1` - March 11th, 2024
### Added
- Added the ability to specify an address in `net.serve`. ([#142])
### Changed
- Update to Luau version `0.616`.
- Major performance improvements when using a large amount of threads / asynchronous Lune APIs. ([#165])
- Minor performance improvements and less overhead for `net.serve` and `net.socket`. ([#165])
### Fixed
- Fixed `fs.copy` not working with empty dirs. ([#155])
- Fixed stack overflow when printing tables with cyclic references. ([#158])
- Fixed not being able to yield in `net.serve` handlers without blocking other requests. ([#165])
- Fixed various scheduler issues / panics. ([#165])
[#142]: https://github.com/lune-org/lune/pull/142
[#155]: https://github.com/lune-org/lune/pull/155
[#158]: https://github.com/lune-org/lune/pull/158
[#165]: https://github.com/lune-org/lune/pull/165
## `0.8.0` - January 14th, 2024
### Breaking Changes
- The Lune CLI now uses subcommands instead of flag options: <br/>
- `lune script_name arg1 arg2 arg3` -> `lune run script_name arg1 arg2 arg3`
- `lune --list` -> `lune list`
- `lune --setup` -> `lune setup`
This unfortunately hurts ergonomics for quickly running scripts but is a necessary change to allow us to add more commands, such as the new `build` subcommand.
- The `createdAt`, `modifiedAt`, and `accessedAt` properties returned from `fs.metadata` are now `DateTime` values instead of numbers.
- The `Lune` struct has been renamed to `Runtime` in the Lune rust crate.
### Added
- Added support for compiling single Lune scripts into standalone executables! ([#140])
Example usage:
```lua
-- my_cool_script.luau
print("Hello, standalone!")
```
```sh
> lune build my_cool_script.luau
# Creates `my_cool_script.exe` (Windows) or `my_cool_script` (macOS / Linux)
```
```sh
> ./my_cool_script.exe # Windows
> ./my_cool_script # macOS / Linux
> "Hello, standalone!"
```
To compile scripts that use `require` and reference multiple files, a bundler such as [darklua](https://github.com/seaofvoices/darklua) should preferrably be used. You may also distribute files alongside the standalone binary, they will still be able to be `require`-d. This limitation will be lifted in the future and Lune will automatically bundle any referenced scripts.
- Added support for path aliases using `.luaurc` config files!
For full documentation and reference, check out the [official Luau RFC](https://rfcs.luau-lang.org/require-by-string-aliases.html), but here's a quick example:
```jsonc
// .luaurc
{
"aliases": {
"modules": "./some/long/path/to/modules"
}
}
```
```lua
-- ./some/long/path/to/modules/foo.luau
return { World = "World!" }
-- ./anywhere/you/want/my_script.luau
local mod = require("@modules/foo")
print("Hello, " .. mod.World)
```
- Added support for multiple values for a single query, and multiple values for a single header, in `net.request`. This is a part of the HTTP specification that is not widely used but that may be useful in certain cases. To clarify:
- Single values remain unchanged and will work exactly the same as before. <br/>
```lua
-- https://example.com/?foo=bar&baz=qux
local net = require("@lune/net")
net.request({
url = "example.com",
query = {
foo = "bar",
baz = "qux",
}
})
```
- Multiple values _on a single query / header_ are represented as an ordered array of strings. <br/>
```lua
-- https://example.com/?foo=first&foo=second&foo=third&bar=baz
local net = require("@lune/net")
net.request({
url = "example.com",
query = {
foo = { "first", "second", "third" },
bar = "baz",
}
})
```
[#140]: https://github.com/lune-org/lune/pull/140
### Changed
- Update to Luau version `0.606`.
### Fixed
- Fixed the `print` and `warn` global functions yielding the thread, preventing them from being used in places such as the callback to `table.sort`.
- Fixed the `overwrite` option for `fs.move` not correctly removing existing files / directories. ([#133])
[#133]: https://github.com/lune-org/lune/pull/133
## `0.7.11` - October 29th, 2023
### Changed
- Update to Luau version `0.601`.
### Fixed
- Fixed `roblox.getAuthCookie` not being compatible with the latest cookie format by upgrading rbx_cookie.
## `0.7.10` - October 25th, 2023
### Added
- Added the `GetDebugId` instance method to the `roblox` built-in. This will return the internal id used by the instance, and as the name implies, it should be primarily used for _debugging_ purposes and cases where you need a globally unique identifier for an instance. It is guaranteed to be a 32-digit hexadecimal string.
### Fixed
- Fixed issues with `SecurityCapabilities` on instances in the `roblox` built-in by upgrading rbx-dom.
## `0.7.9` - October 21st, 2023
### Added
- Added `implementProperty` and `implementMethod` to the `roblox` built-in library to fill in missing functionality that Lune does not aim to implement itself.
Example usage:
```lua
local roblox = require("@lune/roblox")
local part = roblox.Instance.new("Part")
roblox.implementMethod("BasePart", "TestMethod", function(_, ...)
print("Tried to call TestMethod with", ...)
end)
part:TestMethod("Hello", "world!")
```
### Changed
- Update to Luau version `0.599`.
- Stdio options when using `process.spawn` can now be set with more granularity, allowing stderr & stdout to be disabled individually and completely to improve memory usage when they are not being used.
## `0.7.8` - October 5th, 2023
### Added
- 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/Readevalprint_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
- Update to Luau version `0.588`
- Enabled Luau JIT backend for potential performance improvements 🚀 <br/>
If you run into any strange behavior please open an issue!
### Fixed
- Fixed publishing of the Lune library to `crates.io`
- Fixed `serde.decode` deserializing `null` values as `userdata` instead of `nil`.
- Fixed not being able to require files with multiple extensions, eg. `module.spec.luau` was not require-able using `require("module.spec")`.
- Fixed instances and `roblox` built-in library APIs erroring when used asynchronously/concurrently.
## `0.7.5` - July 22nd, 2023
### Added
- Lune now has a new documentation site! </br>
This addresses new APIs from version `0.7.0` not being available on the docs site, brings much improved searching functionality, and will help us keep documentation more up-to-date going forward with a more automated process. You can check out the new site at [lune-org.github.io](https://lune-org.github.io/docs).
- Added `fs.copy` to recursively copy files and directories.
Example usage:
```lua
local fs = require("@lune/fs")
fs.writeDir("myCoolDir")
fs.writeFile("myCoolDir/myAwesomeFile.json", "{}")
fs.copy("myCoolDir", "myCoolDir2")
assert(fs.isDir("myCoolDir2"))
assert(fs.isFile("myCoolDir2/myAwesomeFile.json"))
assert(fs.readFile("myCoolDir2/myAwesomeFile.json") == "{}")
```
- Added `fs.metadata` to get metadata about files and directories.
Example usage:
```lua
local fs = require("@lune/fs")
fs.writeFile("myAwesomeFile.json", "{}")
local meta = fs.metadata("myAwesomeFile.json")
print(meta.exists) --> true
print(meta.kind) --> "file"
print(meta.createdAt) --> 1689848548.0577152 (unix timestamp)
print(meta.permissions) --> { readOnly: false }
```
- Added `roblox.getReflectionDatabase` to access the builtin database containing information about classes and enums.
Example usage:
```lua
local roblox = require("@lune/roblox")
local db = roblox.getReflectionDatabase()
print("There are", #db:GetClassNames(), "classes in the reflection database")
print("All base instance properties:")
local class = db:GetClass("Instance")
for name, prop in class.Properties do
print(string.format(
"- %s with datatype %s and default value %s",
prop.Name,
prop.Datatype,
tostring(class.DefaultProperties[prop.Name])
))
end
```
- Added support for running directories with an `init.luau` or `init.lua` file in them in the CLI.
### Changed
- Update to Luau version `0.583`
### Fixed
- Fixed publishing of Lune to crates.io by migrating away from a monorepo.
- Fixed crashes when writing a very deeply nested `Instance` to a file. ([#62])
- 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
## `0.7.4` - July 7th, 2023
### Added
- Added support for `CFrame` and `Font` types in attributes when using the `roblox` builtin.
### Fixed
- Fixed `roblox.serializeModel` still keeping some unique ids.
## `0.7.3` - July 5th, 2023
### Changed
- 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))
## `0.7.2` - June 28th, 2023
### Added
- Added support for `init` files in directories, similar to Rojo, or `index.js` / `mod.rs` in JavaScript / Rust. <br/>
This means that placing a file named `init.luau` or `init.lua` in a directory will now let you `require` that directory.
### Changed
- The `lune --setup` command is now much more user-friendly.
- Update to Luau version `0.581`
## `0.7.1` - June 17th, 2023
### Added
- Added support for TLS in websockets, enabling usage of `wss://`-prefixed URLs. ([#57])
### Fixed
- 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
## `0.7.0` - June 12th, 2023
### Breaking Changes
@ -749,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
local compressed = serde.compress("gzip", INPUT)
local decompressed = serde.decompress("gzip", compressed)
assert(compressed == "H4sIAAAAAAAAA/PMKygtUSguKcrMS1coyVdIzs8tKEotLvYcFaeLOADSF8BBgAEAAA==")
assert(decompressed == INPUT)
```
@ -781,7 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed `stdio.write` and `stdio.ewrite` not being flushed and causing output to be interleaved. ([#47])
- Fixed `typeof` returning `userdata` for roblox types such as `Instance`, `Vector3`, ...
[#47]: https://github.com/lune-org/lune/pull/47
[#47]: https://github.com/filiptibell/lune/pull/47
## `0.6.7` - May 14th, 2023
@ -851,7 +146,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added support for instance tags & `CollectionService` in the `roblox` built-in. <br />
Currently implemented methods are listed on the [docs site](https://lune-org.github.io/docs/roblox/4-api-status).
Currently implemented methods are listed on the [docs site](https://lune.gitbook.io/lune/roblox/api-status).
### Fixed
@ -889,7 +184,7 @@ This release adds some new features and fixes for the `roblox` built-in.
- Added a `roblox` built-in
If you're familiar with [Remodel](https://github.com/rojo-rbx/remodel), this new built-in contains more or less the same APIs, integrated into Lune. <br />
There are just too many new APIs to list in this changelog, so head over to the [docs sit](https://lune-org.github.io/docs/roblox/1-introduction) to learn more!
There are just too many new APIs to list in this changelog, so head over to the [docs sit](https://lune.gitbook.io/lune/roblox/intro) to learn more!
- Added a `serde` built-in

View file

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

2880
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,38 @@
[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",
]
members = ["packages/cli", "packages/lib", "packages/lib-roblox"]
default-members = ["packages/cli"]
# Package config values shared across all packages,
# such as version, license, and other metadata
[workspace.package]
version = "0.7.0"
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"]
# Shared dependencies that are used across 2 or more packages
# These are declared here to ensure consistent versioning
[workspace.dependencies]
console = "0.15"
directories = "5.0"
futures-util = "0.3"
once_cell = "1.17"
mlua = { version = "0.9.0-beta.3", features = ["luau", "serialize"] }
tokio = { version = "1.24", features = ["full"] }
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
] }
# Serde dependencies, supporting user-facing formats: json, yaml, toml
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"] }
# Profile for building the release binary, with the following options set:
#
@ -33,32 +48,3 @@ members = [
opt-level = "z"
strip = true
lto = true
# Lints for all crates in the workspace
#
# 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 }
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 }
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 }
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 }

View file

@ -1,48 +1,45 @@
<!-- 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="Current Lune library version" />
</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 (~3mb) 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?
Head over to the [Installation](https://lune-org.github.io/docs/getting-started/1-installation) page to get started using Lune!
Head over to the [installation](https://lune.gitbook.io/lune/home/installation) page to get started using Lune!

5
aftman.toml Normal file
View file

@ -0,0 +1,5 @@
[tools]
just = "readysetplay/just@1.8.0"
luau-lsp = "JohnnyMorganz/luau-lsp@1.20.0"
selene = "Kampfkarren/selene@0.24.0"
stylua = "JohnnyMorganz/StyLua@0.17.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

View file

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

View file

@ -1,29 +0,0 @@
[package]
name = "lune-roblox"
version = "0.1.4"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Roblox library for Lune"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
glam = "0.27"
rand = "0.8"
thiserror = "1.0"
once_cell = "1.17"
rbx_binary = "1.0.0"
rbx_dom_weak = "3.0.0"
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_xml = "1.0.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

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

View file

@ -1,493 +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 = |_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
};
let cframe_from_axis_angle =
|_, (v, r): (LuaUserDataRef<Vector3>, f32)| Ok(CFrame(Mat4::from_axis_angle(v.0, r)));
let cframe_from_euler_angles_xyz = |_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
};
let cframe_from_euler_angles_yxz = |_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
};
let cframe_from_matrix = |_,
(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 = |_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
};
let cframe_look_at = |_,
(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<'lua> = LuaUserDataRef<'lua, Vector3>;
type ArgsLook<'lua> = (
LuaUserDataRef<'lua, Vector3>,
LuaUserDataRef<'lua, Vector3>,
Option<LuaUserDataRef<'lua, 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, 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<'lua, F: LuaUserDataFields<'lua, 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<'lua, M: LuaUserDataMethods<'lua, 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",
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);
}
}

View file

@ -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<'lua> = LuaUserDataRef<'lua, Color3>;
type ArgsColors<'lua> = (LuaUserDataRef<'lua, Color3>, LuaUserDataRef<'lua, Color3>);
type ArgsKeypoints<'lua> = Vec<LuaUserDataRef<'lua, ColorSequenceKeypoint>>;
let color_sequence_new = |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<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl fmt::Display for 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(),
}
}
}

View file

@ -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 = |_, uri: String| Ok(Self(ContentType::Uri(uri)));
let from_object = |_, obj: LuaUserDataRef<Instance>| {
let database = rbx_reflection_database::get();
let instance_descriptor = database
.classes
.get("Instance")
.expect("the reflection database should always have Instance in it");
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
"you should not be able to construct an Instance that is not known to Lune",
);
if database.has_superclass(param_descriptor, instance_descriptor) {
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
} else {
Ok(Content(ContentType::Object(obj.dom_ref)))
}
};
TableBuilder::new(lua)?
.with_value("none", Content(ContentType::None))?
.with_function("fromUri", from_uri)?
.with_function("fromObject", from_object)?
.build_readonly()
}
}
impl LuaUserData for Content {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("SourceType", |_, this| {
let variant_name = match &this.0 {
ContentType::None => "None",
ContentType::Uri(_) => "Uri",
ContentType::Object(_) => "Object",
other => {
return Err(LuaError::runtime(format!(
"cannot get SourceType: unknown ContentType variant '{other:?}'"
)))
}
};
Ok(EnumItem::from_enum_name_and_name(
"ContentSourceType",
variant_name,
))
});
fields.add_field_method_get("Uri", |_, this| {
if let ContentType::Uri(uri) = &this.0 {
Ok(Some(uri.to_owned()))
} else {
Ok(None)
}
});
fields.add_field_method_get("Object", |_, this| {
if let ContentType::Object(referent) = &this.0 {
Ok(Instance::new_opt(*referent))
} else {
Ok(None)
}
});
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl fmt::Display for Content {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Regardless of the actual content of the Content, Roblox just emits
// `Content` when casting it to a string. We do not do that.
write!(f, "Content(")?;
match &self.0 {
ContentType::None => write!(f, "None")?,
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
ContentType::Object(_) => write!(f, "Object")?,
other => write!(f, "UnknownType({other:?})")?,
}
write!(f, ")")
}
}
impl From<DomContent> for Content {
fn from(value: DomContent) -> Self {
Self(value.value().clone())
}
}
impl From<Content> for DomContent {
fn from(value: Content) -> Self {
match value.0 {
ContentType::None => Self::none(),
ContentType::Uri(uri) => Self::from_uri(uri),
ContentType::Object(referent) => Self::from_referent(referent),
other => unimplemented!("unknown variant of ContentType: {other:?}"),
}
}
}

View file

@ -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<'lua> = Vec<LuaUserDataRef<'lua, NumberSequenceKeypoint>>;
let number_sequence_new = |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<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl fmt::Display for 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(),
}
}
}

View file

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

View file

@ -1,48 +0,0 @@
use rbx_dom_weak::{
types::{Ref as DomRef, VariantType as DomType},
ustr, Instance as DomInstance, WeakDom,
};
use crate::shared::instance::class_is_a;
pub fn postprocess_dom_for_place(_dom: &mut WeakDom) {
// Nothing here yet
}
pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
let root_ref = dom.root_ref();
recurse_instances(dom, root_ref, &|inst| {
// Get rid of some unique ids - roblox does not
// save these in model files, and we shouldn't either
remove_matching_prop(inst, DomType::UniqueId, "UniqueId");
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
// Similar story with ScriptGuid - this is used
// in the studio-only cloud script drafts feature
if class_is_a(inst.class, "LuaSourceContainer").unwrap_or(false) {
inst.properties.remove(&ustr("ScriptGuid"));
}
});
}
fn recurse_instances<F>(dom: &mut WeakDom, dom_ref: DomRef, f: &F)
where
F: Fn(&mut DomInstance) + 'static,
{
let child_refs = match dom.get_by_ref_mut(dom_ref) {
Some(inst) => {
f(inst);
inst.children().to_vec()
}
None => Vec::new(),
};
for child_ref in child_refs {
recurse_instances(dom, child_ref, f);
}
}
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
let name = &ustr(name);
if inst.properties.get(name).is_some_and(|u| u.ty() == ty) {
inst.properties.remove(name);
}
}

View file

@ -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<'lua> {
const EXPORT_NAME: &'static str;
fn create_exports_table(lua: &'lua Lua) -> LuaResult<LuaTable<'lua>>;
}
/**
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<'lua, T>(lua: &'lua Lua) -> LuaResult<(&'static str, LuaValue<'lua>)>
where
T: LuaExportsTable<'lua>,
{
Ok((
T::EXPORT_NAME,
<T as LuaExportsTable>::create_exports_table(lua)?.into_lua(lua)?,
))
}

View file

@ -1,363 +0,0 @@
#![allow(clippy::items_after_statements)]
use mlua::prelude::*;
use rbx_dom_weak::{
types::{Variant as DomValue, VariantType as DomType},
Instance as DomInstance,
};
use crate::{
datatypes::{
attributes::{ensure_valid_attribute_name, ensure_valid_attribute_value},
conversion::{DomValueToLua, LuaToDomValue},
types::EnumItem,
userdata_impl_eq, userdata_impl_to_string,
},
shared::instance::{class_is_a, find_property_info},
};
use super::{data_model, registry::InstanceRegistry, Instance};
#[allow(clippy::too_many_lines)]
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| {
ensure_not_destroyed(this)?;
userdata_impl_to_string(lua, this, ())
});
m.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
m.add_meta_method(LuaMetaMethod::Index, instance_property_get);
m.add_meta_method_mut(LuaMetaMethod::NewIndex, instance_property_set);
m.add_method("Clone", |lua, this, ()| {
ensure_not_destroyed(this)?;
this.clone_instance().into_lua(lua)
});
m.add_method_mut("Destroy", |_, this, ()| {
this.destroy();
Ok(())
});
m.add_method_mut("ClearAllChildren", |_, this, ()| {
this.clear_all_children();
Ok(())
});
m.add_method("GetChildren", |lua, this, ()| {
ensure_not_destroyed(this)?;
this.get_children().into_lua(lua)
});
m.add_method("GetDescendants", |lua, this, ()| {
ensure_not_destroyed(this)?;
this.get_descendants().into_lua(lua)
});
m.add_method("GetFullName", |lua, this, ()| {
ensure_not_destroyed(this)?;
this.get_full_name().into_lua(lua)
});
m.add_method("GetDebugId", |lua, this, ()| {
this.dom_ref.to_string().into_lua(lua)
});
m.add_method("FindFirstAncestor", |lua, this, name: String| {
ensure_not_destroyed(this)?;
this.find_ancestor(|child| child.name == name).into_lua(lua)
});
m.add_method(
"FindFirstAncestorOfClass",
|lua, this, class_name: String| {
ensure_not_destroyed(this)?;
this.find_ancestor(|child| child.class == class_name)
.into_lua(lua)
},
);
m.add_method(
"FindFirstAncestorWhichIsA",
|lua, this, class_name: String| {
ensure_not_destroyed(this)?;
this.find_ancestor(|child| class_is_a(child.class, &class_name).unwrap_or(false))
.into_lua(lua)
},
);
m.add_method(
"FindFirstChild",
|lua, this, (name, recursive): (String, Option<bool>)| {
ensure_not_destroyed(this)?;
let predicate = |child: &DomInstance| child.name == name;
if matches!(recursive, Some(true)) {
this.find_descendant(predicate).into_lua(lua)
} else {
this.find_child(predicate).into_lua(lua)
}
},
);
m.add_method(
"FindFirstChildOfClass",
|lua, this, (class_name, recursive): (String, Option<bool>)| {
ensure_not_destroyed(this)?;
let predicate = |child: &DomInstance| child.class == class_name;
if matches!(recursive, Some(true)) {
this.find_descendant(predicate).into_lua(lua)
} else {
this.find_child(predicate).into_lua(lua)
}
},
);
m.add_method(
"FindFirstChildWhichIsA",
|lua, this, (class_name, recursive): (String, Option<bool>)| {
ensure_not_destroyed(this)?;
let predicate =
|child: &DomInstance| class_is_a(child.class, &class_name).unwrap_or(false);
if matches!(recursive, Some(true)) {
this.find_descendant(predicate).into_lua(lua)
} else {
this.find_child(predicate).into_lua(lua)
}
},
);
m.add_method("IsA", |_, this, class_name: String| {
Ok(class_is_a(this.class_name, class_name).unwrap_or(false))
});
m.add_method(
"IsAncestorOf",
|_, this, instance: LuaUserDataRef<Instance>| {
ensure_not_destroyed(this)?;
Ok(instance
.find_ancestor(|ancestor| ancestor.referent() == this.dom_ref)
.is_some())
},
);
m.add_method(
"IsDescendantOf",
|_, this, instance: LuaUserDataRef<Instance>| {
ensure_not_destroyed(this)?;
Ok(this
.find_ancestor(|ancestor| ancestor.referent() == instance.dom_ref)
.is_some())
},
);
m.add_method("GetAttribute", |lua, this, name: String| {
ensure_not_destroyed(this)?;
match this.get_attribute(name) {
Some(attribute) => Ok(LuaValue::dom_value_to_lua(lua, &attribute)?),
None => Ok(LuaValue::Nil),
}
});
m.add_method("GetAttributes", |lua, this, ()| {
ensure_not_destroyed(this)?;
let attributes = this.get_attributes();
let tab = lua.create_table_with_capacity(0, attributes.len())?;
for (key, value) in attributes {
tab.set(key, LuaValue::dom_value_to_lua(lua, &value)?)?;
}
Ok(tab)
});
m.add_method(
"SetAttribute",
|lua, this, (attribute_name, lua_value): (String, LuaValue)| {
ensure_not_destroyed(this)?;
ensure_valid_attribute_name(&attribute_name)?;
if lua_value.is_nil() || lua_value.is_null() {
this.remove_attribute(attribute_name);
Ok(())
} else {
match lua_value.lua_to_dom_value(lua, None) {
Ok(dom_value) => {
ensure_valid_attribute_value(&dom_value)?;
this.set_attribute(attribute_name, dom_value);
Ok(())
}
Err(e) => Err(e.into()),
}
}
},
);
m.add_method("GetTags", |_, this, ()| {
ensure_not_destroyed(this)?;
Ok(this.get_tags())
});
m.add_method("HasTag", |_, this, tag: String| {
ensure_not_destroyed(this)?;
Ok(this.has_tag(tag))
});
m.add_method("AddTag", |_, this, tag: String| {
ensure_not_destroyed(this)?;
this.add_tag(tag);
Ok(())
});
m.add_method("RemoveTag", |_, this, tag: String| {
ensure_not_destroyed(this)?;
this.remove_tag(tag);
Ok(())
});
}
fn ensure_not_destroyed(inst: &Instance) -> LuaResult<()> {
if inst.is_destroyed() {
Err(LuaError::RuntimeError(
"Instance has been destroyed".to_string(),
))
} else {
Ok(())
}
}
/*
Gets a property value for an instance.
Getting a value does the following:
1. Check if it is a special property like "ClassName", "Name" or "Parent"
2. Check if a property exists for the wanted name
2a. Get an existing instance property OR
2b. Get a property from a known default value
3. Get a current child of the instance
4. No valid property or instance found, throw error
*/
fn instance_property_get<'lua>(
lua: &'lua Lua,
this: &Instance,
prop_name: String,
) -> LuaResult<LuaValue<'lua>> {
match prop_name.as_str() {
"ClassName" => return this.get_class_name().into_lua(lua),
"Parent" => {
return this.get_parent().into_lua(lua);
}
_ => {}
}
ensure_not_destroyed(this)?;
if prop_name.as_str() == "Name" {
return this.get_name().into_lua(lua);
}
if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(prop) = this.get_property(&prop_name) {
if let DomValue::Enum(enum_value) = prop {
let enum_name = info.enum_name.ok_or_else(|| {
LuaError::RuntimeError(format!(
"Failed to get property '{prop_name}' - encountered unknown enum",
))
})?;
EnumItem::from_enum_name_and_value(&enum_name, enum_value.to_u32())
.ok_or_else(|| {
LuaError::RuntimeError(format!(
"Failed to get property '{}' - Enum.{} does not contain numeric value {}",
prop_name, enum_name, enum_value.to_u32()
))
})?
.into_lua(lua)
} else {
Ok(LuaValue::dom_value_to_lua(lua, &prop)?)
}
} else if let (Some(enum_name), Some(enum_value)) = (info.enum_name, info.enum_default) {
EnumItem::from_enum_name_and_value(&enum_name, enum_value)
.ok_or_else(|| {
LuaError::RuntimeError(format!(
"Failed to get property '{prop_name}' - Enum.{enum_name} does not contain numeric value {enum_value}",
))
})?
.into_lua(lua)
} else if let Some(prop_default) = info.value_default {
Ok(LuaValue::dom_value_to_lua(lua, prop_default)?)
} else if info.value_type.is_some() {
if info.value_type == Some(DomType::Ref) {
Ok(LuaValue::Nil)
} else {
Err(LuaError::RuntimeError(format!(
"Failed to get property '{prop_name}' - missing default value",
)))
}
} else {
Err(LuaError::RuntimeError(format!(
"Failed to get property '{prop_name}' - malformed property info",
)))
}
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
getter.call(*this)
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
Ok(LuaValue::Function(method))
} else {
Err(LuaError::RuntimeError(format!(
"{prop_name} is not a valid member of {this}",
)))
}
}
/*
Sets a property value for an instance.
Setting a value does the following:
1. Check if it is a special property like "ClassName", "Name" or "Parent"
2. Check if a property exists for the wanted name
2a. Set a strict enum from a given EnumItem OR
2b. Set a normal property from a given value
*/
fn instance_property_set<'lua>(
lua: &'lua Lua,
this: &mut Instance,
(prop_name, prop_value): (String, LuaValue<'lua>),
) -> LuaResult<()> {
ensure_not_destroyed(this)?;
match prop_name.as_str() {
"ClassName" => {
return Err(LuaError::RuntimeError(
"Failed to set ClassName - property is read-only".to_string(),
));
}
"Name" => {
let name = String::from_lua(prop_value, lua)?;
this.set_name(name);
return Ok(());
}
"Parent" => {
if this.get_class_name() == data_model::CLASS_NAME {
return Err(LuaError::RuntimeError(
"Failed to set Parent - DataModel can not be reparented".to_string(),
));
}
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
let parent = Parent::from_lua(prop_value, lua)?;
this.set_parent(parent.map(|p| *p));
return Ok(());
}
_ => {}
}
if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(enum_name) = info.enum_name {
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
this.set_property(prop_name, DomValue::EnumItem((*given_enum).clone().into()));
Ok(())
}
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
"Failed to set property '{}' - expected Enum.{}, got Enum.{}",
prop_name, enum_name, given_enum.parent.desc.name
))),
Err(e) => Err(e),
}
} else if let Some(dom_type) = info.value_type {
match prop_value.lua_to_dom_value(lua, Some(dom_type)) {
Ok(dom_value) => {
this.set_property(prop_name, dom_value);
Ok(())
}
Err(e) => Err(e.into()),
}
} else {
Err(LuaError::RuntimeError(format!(
"Failed to set property '{prop_name}' - malformed property info",
)))
}
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
setter.call((*this, prop_value))
} else {
Err(LuaError::RuntimeError(format!(
"{prop_name} is not a valid member of {this}",
)))
}
}

View file

@ -1,803 +0,0 @@
#![allow(clippy::missing_panics_doc)]
use std::{
collections::{BTreeMap, VecDeque},
fmt,
hash::{Hash, Hasher},
sync::Mutex,
};
use mlua::prelude::*;
use once_cell::sync::Lazy;
use rbx_dom_weak::{
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
ustr, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, Ustr, WeakDom,
};
use lune_utils::TableBuilder;
use crate::{
exports::LuaExportsTable,
shared::instance::{class_exists, class_is_a},
};
pub(crate) mod base;
pub(crate) mod data_model;
pub(crate) mod terrain;
pub(crate) mod workspace;
pub mod registry;
const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes";
const PROPERTY_NAME_TAGS: &str = "Tags";
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
#[derive(Debug, Clone, Copy)]
pub struct Instance {
pub(crate) dom_ref: DomRef,
pub(crate) class_name: Ustr,
}
impl Instance {
/**
Creates a new `Instance` from an existing dom object ref.
Panics if the instance does not exist in the internal dom,
or if the given dom object ref points to the internal dom root.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
#[must_use]
pub fn new(dom_ref: DomRef) -> Self {
Self::new_opt(dom_ref).expect("Failed to find instance in document")
}
/**
Creates a new `Instance` from a dom object ref, if the instance exists.
Panics if the given dom object ref points to the internal dom root.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
#[must_use]
pub fn new_opt(dom_ref: DomRef) -> Option<Self> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
if let Some(instance) = dom.get_by_ref(dom_ref) {
assert!(
!(instance.referent() == dom.root_ref()),
"Instances can not be created from dom roots"
);
Some(Self {
dom_ref,
class_name: instance.class,
})
} else {
None
}
}
/**
Creates a new orphaned `Instance` with a given class name.
An orphaned instance is an instance at the root of Lune's internal weak dom.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
#[must_use]
pub fn new_orphaned(class_name: impl AsRef<str>) -> Self {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let class_name = class_name.as_ref();
let instance = DomInstanceBuilder::new(class_name);
let dom_root = dom.root_ref();
let dom_ref = dom.insert(dom_root, instance);
Self {
dom_ref,
class_name: ustr(class_name),
}
}
/**
Creates a new orphaned `Instance` by transferring
it from an external weak dom to the internal one.
An orphaned instance is an instance at the root of Lune's internal weak dom.
Panics if the given dom ref is the root dom ref of the external weak dom.
*/
#[must_use]
pub fn from_external_dom(external_dom: &mut WeakDom, external_dom_ref: DomRef) -> Self {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let dom_root = dom.root_ref();
external_dom.transfer(external_dom_ref, &mut dom, dom_root);
drop(dom); // Self::new needs mutex handle, drop it first
Self::new(external_dom_ref)
}
/**
Clones an instance to an external weak dom.
This will place the instance as a child of the
root of the weak dom, and return its referent.
*/
pub fn clone_into_external_dom(self, external_dom: &mut WeakDom) -> DomRef {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let cloned = dom.clone_into_external(self.dom_ref, external_dom);
external_dom.transfer_within(cloned, external_dom.root_ref());
cloned
}
/**
Clones multiple instances to an external weak dom.
This will place the instances as children of the
root of the weak dom, and return their referents.
*/
pub fn clone_multiple_into_external_dom(
referents: &[DomRef],
external_dom: &mut WeakDom,
) -> Vec<DomRef> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let cloned = dom.clone_multiple_into_external(referents, external_dom);
for referent in &cloned {
external_dom.transfer_within(*referent, external_dom.root_ref());
}
cloned
}
/**
Clones the instance and all of its descendants, and orphans it.
To then save the new instance it must be re-parented,
which matches the exact behavior of Roblox's instances.
### See Also
* [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone)
on the Roblox Developer Hub
*/
#[must_use]
pub fn clone_instance(&self) -> Self {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let new_ref = dom.clone_within(self.dom_ref);
drop(dom); // Self::new needs mutex handle, drop it first
let new_inst = Self::new(new_ref);
new_inst.set_parent(None);
new_inst
}
/**
Destroys the instance, removing it completely
from the weak dom with no way of recovering it.
All member methods will throw errors when called from lua and panic
when called from rust after the instance has been destroyed.
Returns `true` if destroyed successfully, `false` if already destroyed.
### See Also
* [`Destroy`](https://create.roblox.com/docs/reference/engine/classes/Instance#Destroy)
on the Roblox Developer Hub
*/
pub fn destroy(&mut self) -> bool {
if self.is_destroyed() {
false
} else {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
dom.destroy(self.dom_ref);
true
}
}
fn is_destroyed(&self) -> bool {
// NOTE: This property can not be cached since instance references
// other than this one may have destroyed this one, and we don't
// keep track of all current instance reference structs
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
dom.get_by_ref(self.dom_ref).is_none()
}
/**
Destroys all child instances.
### See Also
* [`Instance::Destroy`] for more info about what happens when an instance gets destroyed
* [`ClearAllChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClearAllChildren)
on the Roblox Developer Hub
*/
pub fn clear_all_children(&mut self) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let instance = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
let child_refs = instance.children().to_vec();
for child_ref in child_refs {
dom.destroy(child_ref);
}
}
/**
Checks if the instance matches or inherits a given class name.
### See Also
* [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
on the Roblox Developer Hub
*/
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
class_is_a(self.class_name, class_name).unwrap_or(false)
}
/**
Gets the class name of the instance.
This will return the correct class name even if the instance has been destroyed.
### See Also
* [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName)
on the Roblox Developer Hub
*/
#[must_use]
pub fn get_class_name(&self) -> &str {
self.class_name.as_str()
}
/**
Gets the name of the instance, if it exists.
### See Also
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
on the Roblox Developer Hub
*/
pub fn get_name(&self) -> String {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
dom.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.name
.clone()
}
/**
Sets the name of the instance, if it exists.
### See Also
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
on the Roblox Developer Hub
*/
pub fn set_name(&self, name: impl Into<String>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
dom.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document")
.name = name.into();
}
/**
Gets the parent of the instance, if it exists.
### See Also
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
on the Roblox Developer Hub
*/
pub fn get_parent(&self) -> Option<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let parent_ref = dom.get_by_ref(self.dom_ref)?.parent();
if parent_ref == dom.root_ref() {
None
} else {
drop(dom); // Self::new needs mutex handle, drop it first
Some(Self::new(parent_ref))
}
}
/**
Sets the parent of the instance, if it exists.
If the provided parent is [`None`] the instance will become orphaned.
An orphaned instance is an instance at the root of Lune's internal weak dom.
### See Also
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
on the Roblox Developer Hub
*/
pub fn set_parent(&self, parent: Option<Instance>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let parent_ref = parent.map_or_else(|| dom.root_ref(), |parent| parent.dom_ref);
dom.transfer_within(self.dom_ref, parent_ref);
}
/**
Gets a property for the instance, if it exists.
*/
pub fn get_property(&self, name: impl AsRef<str>) -> Option<DomValue> {
INTERNAL_DOM
.lock()
.expect("Failed to lock document")
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.properties
.get(&ustr(name.as_ref()))
.cloned()
}
/**
Sets a property for the instance.
Note that setting a property here will not fail even if the
property does not actually exist for the instance class.
*/
pub fn set_property(&self, name: impl AsRef<str>, value: DomValue) {
INTERNAL_DOM
.lock()
.expect("Failed to lock document")
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document")
.properties
.insert(ustr(name.as_ref()), value);
}
/**
Gets an attribute for the instance, if it exists.
### See Also
* [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute)
on the Roblox Developer Hub
*/
pub fn get_attribute(&self, name: impl AsRef<str>) -> Option<DomValue> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.get(name.as_ref()).cloned()
} else {
None
}
}
/**
Gets all known attributes for the instance.
### See Also
* [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes)
on the Roblox Developer Hub
*/
pub fn get_attributes(&self) -> BTreeMap<String, DomValue> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.clone().into_iter().collect()
} else {
BTreeMap::new()
}
}
/**
Sets an attribute for the instance.
### See Also
* [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute)
on the Roblox Developer Hub
*/
pub fn set_attribute(&self, name: impl AsRef<str>, value: DomValue) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
// NOTE: Attributes do not support integers, only floats
let value = match value {
DomValue::Int32(i) => DomValue::Float32(i as f32),
DomValue::Int64(i) => DomValue::Float64(i as f64),
value => value,
};
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.insert(name.as_ref().to_string(), value);
} else {
let mut attributes = DomAttributes::new();
attributes.insert(name.as_ref().to_string(), value);
inst.properties.insert(
ustr(PROPERTY_NAME_ATTRIBUTES),
DomValue::Attributes(attributes),
);
}
}
/**
Removes an attribute from the instance.
Note that this does not have an equivalent in the Roblox engine API,
but separating this from `set_attribute` lets `set_attribute` be more
ergonomic and not require an `Option<DomValue>` for the value argument.
The equivalent in the Roblox engine API would be `instance:SetAttribute(name, nil)`.
*/
pub fn remove_attribute(&self, name: impl AsRef<str>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.remove(name.as_ref());
if attributes.is_empty() {
inst.properties.remove(&ustr(PROPERTY_NAME_ATTRIBUTES));
}
}
}
/**
Adds a tag to the instance.
### See Also
* [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag)
on the Roblox Developer Hub
*/
pub fn add_tag(&self, name: impl AsRef<str>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
tags.push(name.as_ref());
} else {
inst.properties.insert(
ustr(PROPERTY_NAME_TAGS),
DomValue::Tags(vec![name.as_ref().to_string()].into()),
);
}
}
/**
Gets all current tags for the instance.
### See Also
* [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags)
on the Roblox Developer Hub
*/
pub fn get_tags(&self) -> Vec<String> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
tags.iter().map(ToString::to_string).collect()
} else {
Vec::new()
}
}
/**
Checks if the instance has a specific tag.
### See Also
* [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag)
on the Roblox Developer Hub
*/
pub fn has_tag(&self, name: impl AsRef<str>) -> bool {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
let name = name.as_ref();
tags.iter().any(|tag| tag == name)
} else {
false
}
}
/**
Removes a tag from the instance.
### See Also
* [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag)
on the Roblox Developer Hub
*/
pub fn remove_tag(&self, name: impl AsRef<str>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
let name = name.as_ref();
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
new_tags.retain(|tag| tag != name);
inst.properties
.insert(ustr(PROPERTY_NAME_TAGS), DomValue::Tags(new_tags.into()));
}
}
/**
Gets all of the current children of this `Instance`.
Note that this is a somewhat expensive operation and that other
operations using weak dom referents should be preferred if possible.
### See Also
* [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren)
on the Roblox Developer Hub
*/
pub fn get_children(&self) -> Vec<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let children = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.children()
.to_vec();
drop(dom); // Self::new needs mutex handle, drop it first
children.into_iter().map(Self::new).collect()
}
/**
Gets all of the current descendants of this `Instance` using a breadth-first search.
Note that this is a somewhat expensive operation and that other
operations using weak dom referents should be preferred if possible.
### See Also
* [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants)
on the Roblox Developer Hub
*/
pub fn get_descendants(&self) -> Vec<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let mut descendants = Vec::new();
let mut queue = VecDeque::from_iter(
dom.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.children(),
);
while let Some(queue_ref) = queue.pop_front() {
descendants.push(*queue_ref);
let queue_inst = dom.get_by_ref(*queue_ref).unwrap();
for queue_ref_inner in queue_inst.children().iter().rev() {
queue.push_back(queue_ref_inner);
}
}
drop(dom); // Self::new needs mutex handle, drop it first
descendants.into_iter().map(Self::new).collect()
}
/**
Gets the "full name" of this instance.
This will be a path composed of instance names from the top-level
ancestor of this instance down to itself, in the following format:
`Ancestor.Child.Descendant.Instance`
### See Also
* [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName)
on the Roblox Developer Hub
*/
pub fn get_full_name(&self) -> String {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let dom_root = dom.root_ref();
let mut parts = Vec::new();
let mut instance_ref = self.dom_ref;
while let Some(instance) = dom.get_by_ref(instance_ref) {
if instance_ref != dom_root && instance.class != data_model::CLASS_NAME {
instance_ref = instance.parent();
parts.push(instance.name.clone());
} else {
break;
}
}
parts.reverse();
parts.join(".")
}
/**
Finds a child of the instance using the given predicate callback.
### See Also
* [`FindFirstChild`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChild) on the Roblox Developer Hub
* [`FindFirstChildOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildOfClass) on the Roblox Developer Hub
* [`FindFirstChildWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildWhichIsA) on the Roblox Developer Hub
*/
pub fn find_child<F>(&self, predicate: F) -> Option<Instance>
where
F: Fn(&DomInstance) -> bool,
{
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let children = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.children()
.to_vec();
let found_ref = children.into_iter().find(|child_ref| {
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
predicate(child_inst)
} else {
false
}
});
drop(dom); // Self::new needs mutex handle, drop it first
found_ref.map(Self::new)
}
/**
Finds an ancestor of the instance using the given predicate callback.
### See Also
* [`FindFirstAncestor`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestor) on the Roblox Developer Hub
* [`FindFirstAncestorOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorOfClass) on the Roblox Developer Hub
* [`FindFirstAncestorWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorWhichIsA) on the Roblox Developer Hub
*/
pub fn find_ancestor<F>(&self, predicate: F) -> Option<Instance>
where
F: Fn(&DomInstance) -> bool,
{
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let mut ancestor_ref = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.parent();
while let Some(ancestor) = dom.get_by_ref(ancestor_ref) {
if predicate(ancestor) {
drop(dom); // Self::new needs mutex handle, drop it first
return Some(Self::new(ancestor_ref));
}
ancestor_ref = ancestor.parent();
}
None
}
/**
Finds a descendant of the instance using the given
predicate callback and a breadth-first search.
### See Also
* [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant)
on the Roblox Developer Hub
*/
pub fn find_descendant<F>(&self, predicate: F) -> Option<Instance>
where
F: Fn(&DomInstance) -> bool,
{
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let mut queue = VecDeque::from_iter(
dom.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.children(),
);
while let Some(queue_item) = queue
.pop_front()
.and_then(|queue_ref| dom.get_by_ref(*queue_ref))
{
if predicate(queue_item) {
let queue_ref = queue_item.referent();
drop(dom); // Self::new needs mutex handle, drop it first
return Some(Self::new(queue_ref));
}
queue.extend(queue_item.children());
}
None
}
}
impl LuaExportsTable<'_> for Instance {
const EXPORT_NAME: &'static str = "Instance";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let instance_new = |lua, class_name: String| {
if class_exists(&class_name) {
Instance::new_orphaned(class_name).into_lua(lua)
} else {
Err(LuaError::RuntimeError(format!(
"Failed to create Instance - '{class_name}' is not a valid class name",
)))
}
};
TableBuilder::new(lua)?
.with_function("new", instance_new)?
.build_readonly()
}
}
/*
Here we add inheritance-like behavior for instances by creating
fields that are restricted to specific classnames / base classes
Note that we should try to be conservative with how many classes
and methods we support here - we should only implement methods that
are necessary for modifying the dom and / or having ergonomic access
to the dom, not try to replicate Roblox engine behavior of instances
If a user wants to replicate Roblox engine behavior, they can use the
instance registry, and register properties + methods from the lua side
*/
impl LuaUserData for Instance {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
data_model::add_fields(fields);
workspace::add_fields(fields);
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
base::add_methods(methods);
data_model::add_methods(methods);
terrain::add_methods(methods);
}
}
impl Hash for Instance {
fn hash<H: Hasher>(&self, state: &mut H) {
self.dom_ref.hash(state);
}
}
impl fmt::Display for Instance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
if self.is_destroyed() {
"<<DESTROYED>>".to_string()
} else {
self.get_name()
}
)
}
}
impl PartialEq for Instance {
fn eq(&self, other: &Self) -> bool {
self.dom_ref == other.dom_ref
}
}
impl From<Instance> for DomRef {
fn from(value: Instance) -> Self {
value.dom_ref
}
}

View file

@ -1,274 +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: &'lua Lua,
class_name: &str,
method_name: &str,
method: LuaFunction<'lua>,
) -> Result<(), InstanceRegistryError> {
let registry = Self::get_or_create(lua);
let mut methods = registry
.methods
.lock()
.expect("Failed to lock instance registry methods");
let class_methods = methods.entry(class_name.to_string()).or_default();
if class_methods.contains_key(method_name) {
return Err(InstanceRegistryError::MethodAlreadyExists {
class_name: class_name.to_string(),
method_name: method_name.to_string(),
});
}
let key = lua
.create_registry_value(method)
.expect("Failed to store method in lua registry");
class_methods.insert(method_name.to_string(), key);
Ok(())
}
/**
Inserts a property getter into the instance registry.
# Errors
- If the property already exists in the registry.
*/
pub fn insert_property_getter<'lua>(
lua: &'lua Lua,
class_name: &str,
property_name: &str,
property_getter: LuaFunction<'lua>,
) -> Result<(), InstanceRegistryError> {
let registry = Self::get_or_create(lua);
let mut getters = registry
.getters
.lock()
.expect("Failed to lock instance registry getters");
let class_getters = getters.entry(class_name.to_string()).or_default();
if class_getters.contains_key(property_name) {
return Err(InstanceRegistryError::PropertyAlreadyExists {
class_name: class_name.to_string(),
property_name: property_name.to_string(),
});
}
let key = lua
.create_registry_value(property_getter)
.expect("Failed to store getter in lua registry");
class_getters.insert(property_name.to_string(), key);
Ok(())
}
/**
Inserts a property setter into the instance registry.
# Errors
- If the property already exists in the registry.
*/
pub fn insert_property_setter<'lua>(
lua: &'lua Lua,
class_name: &str,
property_name: &str,
property_setter: LuaFunction<'lua>,
) -> Result<(), InstanceRegistryError> {
let registry = Self::get_or_create(lua);
let mut setters = registry
.setters
.lock()
.expect("Failed to lock instance registry getters");
let class_setters = setters.entry(class_name.to_string()).or_default();
if class_setters.contains_key(property_name) {
return Err(InstanceRegistryError::PropertyAlreadyExists {
class_name: class_name.to_string(),
property_name: property_name.to_string(),
});
}
let key = lua
.create_registry_value(property_setter)
.expect("Failed to store getter in lua registry");
class_setters.insert(property_name.to_string(), key);
Ok(())
}
/**
Finds a method in the instance registry.
Returns `None` if the method is not found.
*/
#[must_use]
pub fn find_method<'lua>(
lua: &'lua Lua,
instance: &Instance,
method_name: &str,
) -> Option<LuaFunction<'lua>> {
let registry = Self::get_or_create(lua);
let methods = registry
.methods
.lock()
.expect("Failed to lock instance registry methods");
class_name_chain(&instance.class_name)
.iter()
.find_map(|&class_name| {
methods
.get(class_name)
.and_then(|class_methods| class_methods.get(method_name))
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
})
}
/**
Finds a property getter in the instance registry.
Returns `None` if the property getter is not found.
*/
#[must_use]
pub fn find_property_getter<'lua>(
lua: &'lua Lua,
instance: &Instance,
property_name: &str,
) -> Option<LuaFunction<'lua>> {
let registry = Self::get_or_create(lua);
let getters = registry
.getters
.lock()
.expect("Failed to lock instance registry getters");
class_name_chain(&instance.class_name)
.iter()
.find_map(|&class_name| {
getters
.get(class_name)
.and_then(|class_getters| class_getters.get(property_name))
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
})
}
/**
Finds a property setter in the instance registry.
Returns `None` if the property setter is not found.
*/
#[must_use]
pub fn find_property_setter<'lua>(
lua: &'lua Lua,
instance: &Instance,
property_name: &str,
) -> Option<LuaFunction<'lua>> {
let registry = Self::get_or_create(lua);
let setters = registry
.setters
.lock()
.expect("Failed to lock instance registry setters");
class_name_chain(&instance.class_name)
.iter()
.find_map(|&class_name| {
setters
.get(class_name)
.and_then(|class_setters| class_setters.get(property_name))
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
})
}
}
/**
Gets the class name chain for a given class name.
The chain starts with the given class name and ends with the root class.
# Panics
Panics if the class name is not valid.
*/
#[must_use]
pub fn class_name_chain(class_name: &str) -> Vec<&str> {
let db = rbx_reflection_database::get();
let mut list = vec![class_name];
let mut current_name = class_name;
loop {
let class_descriptor = db
.classes
.get(current_name)
.expect("Got invalid class name");
if let Some(sup) = &class_descriptor.superclass {
current_name = sup.borrow();
list.push(current_name);
} else {
break;
}
}
list
}

View file

@ -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<'lua, M: LuaUserDataMethods<'lua, 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(())
}

View file

@ -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)?,
export::<BrickColor>(lua)?,
export::<CFrame>(lua)?,
export::<Color3>(lua)?,
export::<ColorSequence>(lua)?,
export::<ColorSequenceKeypoint>(lua)?,
export::<Content>(lua)?,
export::<Faces>(lua)?,
export::<Font>(lua)?,
export::<NumberRange>(lua)?,
export::<NumberSequence>(lua)?,
export::<NumberSequenceKeypoint>(lua)?,
export::<PhysicalProperties>(lua)?,
export::<Ray>(lua)?,
export::<Rect>(lua)?,
export::<UDim>(lua)?,
export::<UDim2>(lua)?,
export::<Region3>(lua)?,
export::<Region3int16>(lua)?,
export::<Vector2>(lua)?,
export::<Vector2int16>(lua)?,
export::<Vector3>(lua)?,
export::<Vector3int16>(lua)?,
// Classes
export::<Instance>(lua)?,
// 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)?;
TableBuilder::new(lua)?
.with_values(exports)?
.build_readonly()
}

View file

@ -1,155 +0,0 @@
use core::fmt;
use std::collections::HashMap;
use mlua::prelude::*;
use rbx_dom_weak::types::Variant as DomVariant;
use rbx_reflection::{ClassDescriptor, DataType};
use super::{property::DatabaseProperty, utils::*};
use crate::datatypes::{
conversion::DomValueToLua, types::EnumItem, userdata_impl_eq, userdata_impl_to_string,
};
type DbClass = &'static ClassDescriptor<'static>;
/**
A wrapper for [`rbx_reflection::ClassDescriptor`] that
also provides access to the class descriptor from lua.
*/
#[derive(Debug, Clone, Copy)]
pub struct DatabaseClass(DbClass);
impl DatabaseClass {
pub(crate) fn new(inner: DbClass) -> Self {
Self(inner)
}
/**
Get the name of this class.
*/
#[must_use]
pub fn get_name(&self) -> String {
self.0.name.to_string()
}
/**
Get the superclass (parent class) of this class.
May be `None` if no parent class exists.
*/
#[must_use]
pub fn get_superclass(&self) -> Option<String> {
let sup = self.0.superclass.as_ref()?;
Some(sup.to_string())
}
/**
Get all known properties for this class.
*/
#[must_use]
pub fn get_properties(&self) -> HashMap<String, DatabaseProperty> {
self.0
.properties
.iter()
.map(|(name, prop)| (name.to_string(), DatabaseProperty::new(self.0, prop)))
.collect()
}
/**
Get all default values for properties of this class.
*/
#[must_use]
pub fn get_defaults(&self) -> HashMap<String, DomVariant> {
self.0
.default_properties
.iter()
.map(|(name, prop)| (name.to_string(), prop.clone()))
.collect()
}
/**
Get all tags describing the class.
These include information such as if the class can be replicated
to players at runtime, and top-level class categories.
*/
pub fn get_tags_str(&self) -> Vec<&'static str> {
self.0
.tags
.iter()
.copied()
.map(class_tag_to_str)
.collect::<Vec<_>>()
}
}
impl LuaUserData for DatabaseClass {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
fields.add_field_method_get("Superclass", |_, this| Ok(this.get_superclass()));
fields.add_field_method_get("Properties", |_, this| Ok(this.get_properties()));
fields.add_field_method_get("DefaultProperties", |lua, this| {
let defaults = this.get_defaults();
let mut map = HashMap::with_capacity(defaults.len());
for (name, prop) in defaults {
let value = if let DomVariant::Enum(enum_value) = prop {
make_enum_value(this.0, &name, enum_value.to_u32())
.and_then(|e| e.into_lua(lua))
} else {
LuaValue::dom_value_to_lua(lua, &prop).into_lua_err()
};
if let Ok(value) = value {
map.insert(name, value);
}
}
Ok(map)
});
fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl PartialEq for DatabaseClass {
fn eq(&self, other: &Self) -> bool {
self.0.name == other.0.name
}
}
impl fmt::Display for DatabaseClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ReflectionDatabaseClass({})", self.0.name)
}
}
fn find_enum_name(inner: DbClass, name: impl AsRef<str>) -> Option<String> {
inner.properties.iter().find_map(|(prop_name, prop_info)| {
if prop_name == name.as_ref() {
if let DataType::Enum(enum_name) = &prop_info.data_type {
Some(enum_name.to_string())
} else {
None
}
} else {
None
}
})
}
fn make_enum_value(inner: DbClass, name: impl AsRef<str>, value: u32) -> LuaResult<EnumItem> {
let name = name.as_ref();
let enum_name = find_enum_name(inner, name).ok_or_else(|| {
LuaError::RuntimeError(format!(
"Failed to get default property '{name}' - No enum descriptor was found",
))
})?;
EnumItem::from_enum_name_and_value(&enum_name, value).ok_or_else(|| {
LuaError::RuntimeError(format!(
"Failed to get default property '{name}' - Enum.{enum_name} does not contain numeric value {value}",
))
})
}

View file

@ -1,69 +0,0 @@
use std::{collections::HashMap, fmt};
use mlua::prelude::*;
use rbx_reflection::EnumDescriptor;
use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string};
type DbEnum = &'static EnumDescriptor<'static>;
/**
A wrapper for [`rbx_reflection::EnumDescriptor`] that
also provides access to the class descriptor from lua.
*/
#[derive(Debug, Clone, Copy)]
pub struct DatabaseEnum(DbEnum);
impl DatabaseEnum {
pub(crate) fn new(inner: DbEnum) -> Self {
Self(inner)
}
/**
Get the name of this enum.
*/
#[must_use]
pub fn get_name(&self) -> String {
self.0.name.to_string()
}
/**
Get all known members of this enum.
Note that this is a direct map of name -> enum values,
and does not actually use the `EnumItem` datatype itself.
*/
#[must_use]
pub fn get_items(&self) -> HashMap<String, u32> {
self.0
.items
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect()
}
}
impl LuaUserData for DatabaseEnum {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
fields.add_field_method_get("Items", |_, this| Ok(this.get_items()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl PartialEq for DatabaseEnum {
fn eq(&self, other: &Self) -> bool {
self.0.name == other.0.name
}
}
impl fmt::Display for DatabaseEnum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ReflectionDatabaseEnum({})", self.0.name)
}
}

View file

@ -1,151 +0,0 @@
use std::fmt;
use mlua::prelude::*;
use rbx_reflection::ReflectionDatabase;
use crate::datatypes::userdata_impl_eq;
mod class;
mod enums;
mod property;
mod utils;
pub use class::DatabaseClass;
pub use enums::DatabaseEnum;
pub use property::DatabaseProperty;
use super::datatypes::userdata_impl_to_string;
type Db = &'static ReflectionDatabase<'static>;
/**
A wrapper for [`rbx_reflection::ReflectionDatabase`] that
also provides access to the reflection database from lua.
*/
#[derive(Debug, Clone, Copy)]
pub struct Database(Db);
impl Database {
/**
Creates a new database struct, referencing the bundled reflection database.
*/
#[must_use]
pub fn new() -> Self {
Self::default()
}
/**
Get the version string of the database.
This will follow the format `x.y.z.w`, which most
commonly looks something like `0.567.0.123456789`.
*/
#[must_use]
pub fn get_version(&self) -> String {
let [x, y, z, w] = self.0.version;
format!("{x}.{y}.{z}.{w}")
}
/**
Retrieves a list of all currently known enum names.
*/
#[must_use]
pub fn get_enum_names(&self) -> Vec<String> {
self.0.enums.keys().map(ToString::to_string).collect()
}
/**
Retrieves a list of all currently known class names.
*/
#[must_use]
pub fn get_class_names(&self) -> Vec<String> {
self.0.classes.keys().map(ToString::to_string).collect()
}
/**
Gets an enum with the exact given name, if one exists.
*/
pub fn get_enum(&self, name: impl AsRef<str>) -> Option<DatabaseEnum> {
let e = self.0.enums.get(name.as_ref())?;
Some(DatabaseEnum::new(e))
}
/**
Gets a class with the exact given name, if one exists.
*/
pub fn get_class(&self, name: impl AsRef<str>) -> Option<DatabaseClass> {
let c = self.0.classes.get(name.as_ref())?;
Some(DatabaseClass::new(c))
}
/**
Finds an enum with the given name.
This will use case-insensitive matching and ignore leading and trailing whitespace.
*/
pub fn find_enum(&self, name: impl AsRef<str>) -> Option<DatabaseEnum> {
let name = name.as_ref().trim().to_lowercase();
let (ename, _) = self
.0
.enums
.iter()
.find(|(ename, _)| ename.trim().to_lowercase() == name)?;
self.get_enum(ename)
}
/**
Finds a class with the given name.
This will use case-insensitive matching and ignore leading and trailing whitespace.
*/
pub fn find_class(&self, name: impl AsRef<str>) -> Option<DatabaseClass> {
let name = name.as_ref().trim().to_lowercase();
let (cname, _) = self
.0
.classes
.iter()
.find(|(cname, _)| cname.trim().to_lowercase() == name)?;
self.get_class(cname)
}
}
impl LuaUserData for Database {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("Version", |_, this| Ok(this.get_version()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
methods.add_method("GetEnumNames", |_, this, (): ()| Ok(this.get_enum_names()));
methods.add_method(
"GetClassNames",
|_, this, (): ()| Ok(this.get_class_names()),
);
methods.add_method("GetEnum", |_, this, name: String| Ok(this.get_enum(name)));
methods.add_method("GetClass", |_, this, name: String| Ok(this.get_class(name)));
methods.add_method("FindEnum", |_, this, name: String| Ok(this.find_enum(name)));
methods.add_method("FindClass", |_, this, name: String| {
Ok(this.find_class(name))
});
}
}
impl Default for Database {
fn default() -> Self {
Self(rbx_reflection_database::get())
}
}
impl PartialEq for Database {
fn eq(&self, _other: &Self) -> bool {
true // All database userdatas refer to the same underlying rbx-dom database
}
}
impl fmt::Display for Database {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ReflectionDatabase")
}
}

View file

@ -1,99 +0,0 @@
use std::fmt;
use mlua::prelude::*;
use rbx_reflection::{ClassDescriptor, PropertyDescriptor};
use super::utils::*;
use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string};
type DbClass = &'static ClassDescriptor<'static>;
type DbProp = &'static PropertyDescriptor<'static>;
/**
A wrapper for [`rbx_reflection::PropertyDescriptor`] that
also provides access to the property descriptor from lua.
*/
#[derive(Debug, Clone, Copy)]
pub struct DatabaseProperty(DbClass, DbProp);
impl DatabaseProperty {
pub(crate) fn new(inner: DbClass, inner_prop: DbProp) -> Self {
Self(inner, inner_prop)
}
/**
Get the name of this property.
*/
#[must_use]
pub fn get_name(&self) -> String {
self.1.name.to_string()
}
/**
Get the datatype name of the property.
For normal datatypes this will be a string such as `string`, `Color3`, ...
For enums this will be a string formatted as `Enum.EnumName`.
*/
#[must_use]
pub fn get_datatype_name(&self) -> String {
data_type_to_str(self.1.data_type.clone())
}
/**
Get the scriptability of this property, meaning if it can be written / read at runtime.
All properties are writable and readable in Lune even if scriptability is not.
*/
#[must_use]
pub fn get_scriptability_str(&self) -> &'static str {
scriptability_to_str(self.1.scriptability)
}
/**
Get all known tags describing the property.
These include information such as if the property can be replicated to players
at runtime, if the property should be hidden in Roblox Studio, and more.
*/
pub fn get_tags_str(&self) -> Vec<&'static str> {
self.1
.tags
.iter()
.copied()
.map(property_tag_to_str)
.collect::<Vec<_>>()
}
}
impl LuaUserData for DatabaseProperty {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
fields.add_field_method_get("Datatype", |_, this| Ok(this.get_datatype_name()));
fields.add_field_method_get("Scriptability", |_, this| Ok(this.get_scriptability_str()));
fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl PartialEq for DatabaseProperty {
fn eq(&self, other: &Self) -> bool {
self.0.name == other.0.name && self.1.name == other.1.name
}
}
impl fmt::Display for DatabaseProperty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ReflectionDatabaseProperty({} > {})",
self.0.name, self.1.name
)
}
}

View file

@ -1,56 +0,0 @@
use rbx_reflection::{ClassTag, DataType, PropertyTag, Scriptability};
use crate::datatypes::extension::DomValueExt;
pub fn data_type_to_str(data_type: DataType) -> String {
match data_type {
DataType::Enum(e) => format!("Enum.{e}"),
DataType::Value(v) => v
.variant_name()
.expect("Encountered unknown data type variant")
.to_string(),
_ => panic!("Encountered unknown data type"),
}
}
/*
NOTE: Remember to add any new strings here to typedefs too!
*/
pub fn scriptability_to_str(scriptability: Scriptability) -> &'static str {
match scriptability {
Scriptability::None => "None",
Scriptability::Custom => "Custom",
Scriptability::Read => "Read",
Scriptability::ReadWrite => "ReadWrite",
Scriptability::Write => "Write",
_ => panic!("Encountered unknown scriptability"),
}
}
pub fn property_tag_to_str(tag: PropertyTag) -> &'static str {
match tag {
PropertyTag::Deprecated => "Deprecated",
PropertyTag::Hidden => "Hidden",
PropertyTag::NotBrowsable => "NotBrowsable",
PropertyTag::NotReplicated => "NotReplicated",
PropertyTag::NotScriptable => "NotScriptable",
PropertyTag::ReadOnly => "ReadOnly",
PropertyTag::WriteOnly => "WriteOnly",
_ => panic!("Encountered unknown property tag"),
}
}
pub fn class_tag_to_str(tag: ClassTag) -> &'static str {
match tag {
ClassTag::Deprecated => "Deprecated",
ClassTag::NotBrowsable => "NotBrowsable",
ClassTag::NotCreatable => "NotCreatable",
ClassTag::NotReplicated => "NotReplicated",
ClassTag::PlayerReplicated => "PlayerReplicated",
ClassTag::Service => "Service",
ClassTag::Settings => "Settings",
ClassTag::UserSettings => "UserSettings",
_ => panic!("Encountered unknown class tag"),
}
}

View file

@ -1,22 +0,0 @@
[package]
name = "lune-std-datetime"
version = "0.1.3"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - DateTime"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
thiserror = "1.0"
chrono = "0.4.38"
chrono_lc = "0.1.6"
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,250 +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 ISO 8601 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_iso_date(iso_date: impl AsRef<str>) -> DateTimeResult<Self> {
let inner = ChronoDateTime::parse_from_rfc3339(iso_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 ISO 8601 format, such as `1996-12-19T16:39:57-08:00`.
See [`chrono::DateTime::to_rfc3339`] for additional details.
*/
#[must_use]
pub fn to_iso_date(self) -> String {
self.inner.to_rfc3339()
}
}
impl LuaUserData for DateTime {
fn add_fields<'lua, F: LuaUserDataFields<'lua, 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<'lua, M: LuaUserDataMethods<'lua, 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_iso_date()));
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()));
}
}

View file

@ -1,36 +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;
/**
Creates the `datetime` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("fromIsoDate", |_, iso_date: String| {
Ok(DateTime::from_iso_date(iso_date)?)
})?
.with_function("fromLocalTime", |_, values| {
Ok(DateTime::from_local_time(&values)?)
})?
.with_function("fromUniversalTime", |_, values| {
Ok(DateTime::from_universal_time(&values)?)
})?
.with_function("fromUnixTimestamp", |_, timestamp| {
Ok(DateTime::from_unix_timestamp_float(timestamp)?)
})?
.with_function("now", |_, ()| Ok(DateTime::now()))?
.build_readonly()
}

View file

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

View file

@ -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",
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",
message: Some(e.to_string()),
}),
}
}
}
impl IntoLua<'_> for DateTimeValues {
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
let tab = TableBuilder::new(lua)?
.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)
}
}

View file

@ -1,23 +0,0 @@
[package]
name = "lune-std-fs"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - FS"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
bstr = "1.9"
tokio = { version = "1", default-features = false, features = ["fs"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-datetime = { version = "0.1.2", path = "../lune-std-datetime" }

View file

@ -1,166 +0,0 @@
use std::collections::VecDeque;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use mlua::prelude::*;
use tokio::fs;
use super::options::FsWriteOptions;
pub struct CopyContents {
// Vec<(relative depth, path)>
pub dirs: Vec<(usize, PathBuf)>,
pub files: Vec<(usize, PathBuf)>,
}
async fn get_contents_at(root: PathBuf, _: FsWriteOptions) -> LuaResult<CopyContents> {
let mut dirs = Vec::new();
let mut files = Vec::new();
let mut queue = VecDeque::new();
let normalized_root = fs::canonicalize(&root).await.map_err(|e| {
LuaError::RuntimeError(format!("Failed to canonicalize root directory path\n{e}"))
})?;
// Push initial children of the root path into the queue
let mut entries = fs::read_dir(&normalized_root).await?;
while let Some(entry) = entries.next_entry().await? {
queue.push_back((1, entry.path()));
}
// Go through the current queue, pushing to it
// when we find any new descendant directories
// FUTURE: Try to do async reading here concurrently to speed it up a bit
while let Some((current_depth, current_path)) = queue.pop_front() {
let meta = fs::metadata(&current_path).await?;
if meta.is_symlink() {
return Err(LuaError::RuntimeError(format!(
"Symlinks are not yet supported, encountered at path '{}'",
current_path.display()
)));
} else if meta.is_dir() {
// FUTURE: Add an option in FsWriteOptions for max depth and limit it here
let mut entries = fs::read_dir(&current_path).await?;
while let Some(entry) = entries.next_entry().await? {
queue.push_back((current_depth + 1, entry.path()));
}
dirs.push((current_depth, current_path));
} else {
files.push((current_depth, current_path));
}
}
// Ensure that all directory and file paths are relative to the root path
// SAFETY: Since we only ever push dirs and files relative to the root, unwrap is safe
for (_, dir) in &mut dirs {
*dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf();
}
for (_, file) in &mut files {
*file = file.strip_prefix(&normalized_root).unwrap().to_path_buf();
}
// FUTURE: Deduplicate paths such that these directories:
// - foo/
// - foo/bar/
// - foo/bar/baz/
// turn into a single foo/bar/baz/ and let create_dir_all do the heavy lifting
Ok(CopyContents { dirs, files })
}
async fn ensure_no_dir_exists(path: impl AsRef<Path>) -> LuaResult<()> {
let path = path.as_ref();
match fs::metadata(&path).await {
Ok(meta) if meta.is_dir() => Err(LuaError::RuntimeError(format!(
"A directory already exists at the path '{}'",
path.display()
))),
_ => Ok(()),
}
}
async fn ensure_no_file_exists(path: impl AsRef<Path>) -> LuaResult<()> {
let path = path.as_ref();
match fs::metadata(&path).await {
Ok(meta) if meta.is_file() => Err(LuaError::RuntimeError(format!(
"A file already exists at the path '{}'",
path.display()
))),
_ => Ok(()),
}
}
pub async fn copy(
source: impl AsRef<Path>,
target: impl AsRef<Path>,
options: FsWriteOptions,
) -> LuaResult<()> {
let source = source.as_ref();
let target = target.as_ref();
// Check if we got a file or directory - we will handle them differently below
let (is_dir, is_file) = match fs::metadata(&source).await {
Ok(meta) => (meta.is_dir(), meta.is_file()),
Err(e) if e.kind() == ErrorKind::NotFound => {
return Err(LuaError::RuntimeError(format!(
"No file or directory exists at the path '{}'",
source.display()
)))
}
Err(e) => return Err(e.into()),
};
if !is_file && !is_dir {
return Err(LuaError::RuntimeError(format!(
"The given path '{}' is not a file or a directory",
source.display()
)));
}
// Perform copying:
//
// 1. If we are not allowed to overwrite, make sure nothing exists at the target path
// 2. If we are allowed to overwrite, remove any previous entry at the path
// 3. Write all directories first
// 4. Write all files
if !options.overwrite {
if is_file {
ensure_no_file_exists(target).await?;
} else if is_dir {
ensure_no_dir_exists(target).await?;
}
}
if is_file {
fs::copy(source, target).await?;
} else if is_dir {
let contents = get_contents_at(source.to_path_buf(), options).await?;
if options.overwrite {
let (is_dir, is_file) = match fs::metadata(&target).await {
Ok(meta) => (meta.is_dir(), meta.is_file()),
Err(e) if e.kind() == ErrorKind::NotFound => (false, false),
Err(e) => return Err(e.into()),
};
if is_dir {
fs::remove_dir_all(target).await?;
} else if is_file {
fs::remove_file(target).await?;
}
}
fs::create_dir_all(target).await?;
// FUTURE: Write dirs / files concurrently
// to potentially speed these operations up
for (_, dir) in &contents.dirs {
fs::create_dir_all(target.join(dir)).await?;
}
for (_, file) in &contents.files {
fs::copy(source.join(file), target.join(file)).await?;
}
}
Ok(())
}

View file

@ -1,126 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::io::ErrorKind as IoErrorKind;
use std::path::PathBuf;
use bstr::{BString, ByteSlice};
use mlua::prelude::*;
use tokio::fs;
use lune_utils::TableBuilder;
mod copy;
mod metadata;
mod options;
use self::copy::copy;
use self::metadata::FsMetadata;
use self::options::FsWriteOptions;
/**
Creates the `fs` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_async_function("readFile", fs_read_file)?
.with_async_function("readDir", fs_read_dir)?
.with_async_function("writeFile", fs_write_file)?
.with_async_function("writeDir", fs_write_dir)?
.with_async_function("removeFile", fs_remove_file)?
.with_async_function("removeDir", fs_remove_dir)?
.with_async_function("metadata", fs_metadata)?
.with_async_function("isFile", fs_is_file)?
.with_async_function("isDir", fs_is_dir)?
.with_async_function("move", fs_move)?
.with_async_function("copy", fs_copy)?
.build_readonly()
}
async fn fs_read_file(lua: &Lua, path: String) -> LuaResult<LuaString> {
let bytes = fs::read(&path).await.into_lua_err()?;
lua.create_string(bytes)
}
async fn fs_read_dir(_: &Lua, path: String) -> LuaResult<Vec<String>> {
let mut dir_strings = Vec::new();
let mut dir = fs::read_dir(&path).await.into_lua_err()?;
while let Some(dir_entry) = dir.next_entry().await.into_lua_err()? {
if let Some(dir_name_str) = dir_entry.file_name().to_str() {
dir_strings.push(dir_name_str.to_owned());
} else {
return Err(LuaError::RuntimeError(format!(
"File name could not be converted into a string: '{}'",
dir_entry.file_name().to_string_lossy()
)));
}
}
Ok(dir_strings)
}
async fn fs_write_file(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> {
fs::write(&path, contents.as_bytes()).await.into_lua_err()
}
async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> {
fs::create_dir_all(&path).await.into_lua_err()
}
async fn fs_remove_file(_: &Lua, path: String) -> LuaResult<()> {
fs::remove_file(&path).await.into_lua_err()
}
async fn fs_remove_dir(_: &Lua, path: String) -> LuaResult<()> {
fs::remove_dir_all(&path).await.into_lua_err()
}
async fn fs_metadata(_: &Lua, path: String) -> LuaResult<FsMetadata> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(FsMetadata::not_found()),
Ok(meta) => Ok(FsMetadata::from(meta)),
Err(e) => Err(e.into()),
}
}
async fn fs_is_file(_: &Lua, path: String) -> LuaResult<bool> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
Ok(meta) => Ok(meta.is_file()),
Err(e) => Err(e.into()),
}
}
async fn fs_is_dir(_: &Lua, path: String) -> LuaResult<bool> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
Ok(meta) => Ok(meta.is_dir()),
Err(e) => Err(e.into()),
}
}
async fn fs_move(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
let path_from = PathBuf::from(from);
if !path_from.exists() {
return Err(LuaError::RuntimeError(format!(
"No file or directory exists at the path '{}'",
path_from.display()
)));
}
let path_to = PathBuf::from(to);
if !options.overwrite && path_to.exists() {
return Err(LuaError::RuntimeError(format!(
"A file or directory already exists at the path '{}'",
path_to.display()
)));
}
fs::rename(path_from, path_to).await.into_lua_err()?;
Ok(())
}
async fn fs_copy(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
copy(from, to, options).await
}

View file

@ -1,154 +0,0 @@
use std::{
fmt,
fs::{FileType as StdFileType, Metadata as StdMetadata, Permissions as StdPermissions},
io::Result as IoResult,
str::FromStr,
time::SystemTime,
};
use mlua::prelude::*;
use lune_std_datetime::DateTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsMetadataKind {
None,
File,
Dir,
Symlink,
}
impl fmt::Display for FsMetadataKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::None => "none",
Self::File => "file",
Self::Dir => "dir",
Self::Symlink => "symlink",
}
)
}
}
impl FromStr for FsMetadataKind {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_ref() {
"none" => Ok(Self::None),
"file" => Ok(Self::File),
"dir" => Ok(Self::Dir),
"symlink" => Ok(Self::Symlink),
_ => Err("Invalid metadata kind"),
}
}
}
impl From<StdFileType> for FsMetadataKind {
fn from(value: StdFileType) -> Self {
if value.is_file() {
Self::File
} else if value.is_dir() {
Self::Dir
} else if value.is_symlink() {
Self::Symlink
} else {
panic!("Encountered unknown filesystem filetype")
}
}
}
impl<'lua> IntoLua<'lua> for FsMetadataKind {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
if self == Self::None {
Ok(LuaValue::Nil)
} else {
self.to_string().into_lua(lua)
}
}
}
#[derive(Debug, Clone)]
pub struct FsPermissions {
pub(crate) read_only: bool,
}
impl From<StdPermissions> for FsPermissions {
fn from(value: StdPermissions) -> Self {
Self {
read_only: value.readonly(),
}
}
}
impl<'lua> IntoLua<'lua> for FsPermissions {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
let tab = lua.create_table_with_capacity(0, 1)?;
tab.set("readOnly", self.read_only)?;
tab.set_readonly(true);
Ok(LuaValue::Table(tab))
}
}
#[derive(Debug, Clone)]
pub struct FsMetadata {
pub(crate) kind: FsMetadataKind,
pub(crate) exists: bool,
pub(crate) created_at: Option<DateTime>,
pub(crate) modified_at: Option<DateTime>,
pub(crate) accessed_at: Option<DateTime>,
pub(crate) permissions: Option<FsPermissions>,
}
impl FsMetadata {
pub fn not_found() -> Self {
Self {
kind: FsMetadataKind::None,
exists: false,
created_at: None,
modified_at: None,
accessed_at: None,
permissions: None,
}
}
}
impl<'lua> IntoLua<'lua> for FsMetadata {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
let tab = lua.create_table_with_capacity(0, 6)?;
tab.set("kind", self.kind)?;
tab.set("exists", self.exists)?;
tab.set("createdAt", self.created_at)?;
tab.set("modifiedAt", self.modified_at)?;
tab.set("accessedAt", self.accessed_at)?;
tab.set("permissions", self.permissions)?;
tab.set_readonly(true);
Ok(LuaValue::Table(tab))
}
}
impl From<StdMetadata> for FsMetadata {
fn from(value: StdMetadata) -> Self {
Self {
kind: value.file_type().into(),
exists: true,
created_at: system_time_to_timestamp(value.created()),
modified_at: system_time_to_timestamp(value.modified()),
accessed_at: system_time_to_timestamp(value.accessed()),
permissions: Some(FsPermissions::from(value.permissions())),
}
}
}
fn system_time_to_timestamp(res: IoResult<SystemTime>) -> Option<DateTime> {
match res {
Ok(t) => match t.duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => DateTime::from_unix_timestamp_float(d.as_secs_f64()).ok(),
Err(_) => None,
},
Err(_) => None,
}
}

View file

@ -1,18 +0,0 @@
[package]
name = "lune-std-luau"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Luau"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau", "luau-jit"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,90 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use lune_utils::{jit::JitStatus, TableBuilder};
mod options;
use self::options::{LuauCompileOptions, LuauLoadOptions};
const BYTECODE_ERROR_BYTE: u8 = 0;
/**
Creates the `luau` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("compile", compile_source)?
.with_function("load", load_source)?
.build_readonly()
}
fn compile_source<'lua>(
lua: &'lua Lua,
(source, options): (LuaString<'lua>, LuauCompileOptions),
) -> LuaResult<LuaString<'lua>> {
let bytecode = options.into_compiler().compile(source);
match bytecode.first() {
Some(&BYTECODE_ERROR_BYTE) => Err(LuaError::RuntimeError(
String::from_utf8_lossy(&bytecode).into_owned(),
)),
Some(_) => lua.create_string(bytecode),
None => panic!("Compiling resulted in empty bytecode"),
}
}
fn load_source<'lua>(
lua: &'lua Lua,
(source, options): (LuaString<'lua>, LuauLoadOptions),
) -> LuaResult<LuaFunction<'lua>> {
let mut chunk = lua.load(source.as_bytes()).set_name(options.debug_name);
let env_changed = options.environment.is_some();
if let Some(custom_environment) = options.environment {
let environment = lua.create_table()?;
// Inject all globals into the environment
if options.inject_globals {
for pair in lua.globals().pairs() {
let (key, value): (LuaValue, LuaValue) = pair?;
environment.set(key, value)?;
}
if let Some(global_metatable) = lua.globals().get_metatable() {
environment.set_metatable(Some(global_metatable));
}
} else if let Some(custom_metatable) = custom_environment.get_metatable() {
// Since we don't need to set the global metatable,
// we can just set a custom metatable if it exists
environment.set_metatable(Some(custom_metatable));
}
// Inject the custom environment
for pair in custom_environment.pairs() {
let (key, value): (LuaValue, LuaValue) = pair?;
environment.set(key, value)?;
}
chunk = chunk.set_environment(environment);
}
// Enable JIT if codegen is enabled and the environment hasn't
// changed, otherwise disable JIT since it'll fall back anyways
lua.enable_jit(options.codegen_enabled && !env_changed);
let function = chunk.into_function()?;
lua.enable_jit(
lua.app_data_ref::<JitStatus>()
.ok_or(LuaError::runtime(
"Failed to get current JitStatus ref from AppData",
))?
.enabled(),
);
Ok(function)
}

View file

@ -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<'lua> FromLua<'lua> for LuauCompileOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua 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",
message: Some(format!(
"Invalid compile options - expected table, got {}",
value.type_name()
)),
})
}
})
}
}
pub struct LuauLoadOptions<'lua> {
pub(crate) debug_name: String,
pub(crate) environment: Option<LuaTable<'lua>>,
pub(crate) inject_globals: bool,
pub(crate) codegen_enabled: bool,
}
impl Default for LuauLoadOptions<'_> {
fn default() -> Self {
Self {
debug_name: DEFAULT_DEBUG_NAME.to_string(),
environment: None,
inject_globals: true,
codegen_enabled: false,
}
}
}
impl<'lua> FromLua<'lua> for LuauLoadOptions<'lua> {
fn from_lua(value: LuaValue<'lua>, _: &'lua 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",
message: Some(format!(
"Invalid load options - expected string or table, got {}",
value.type_name()
)),
})
}
})
}
}

View file

@ -1,39 +0,0 @@
[package]
name = "lune-std-net"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Net"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
bstr = "1.9"
futures-util = "0.3"
hyper = { version = "1.1", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }
http = "1.0"
http-body-util = { version = "0.1" }
hyper-tungstenite = { version = "0.13" }
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
] }
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
urlencoding = "2.1"
tokio = { version = "1", default-features = false, features = [
"sync",
"net",
"macros",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-serde = { version = "0.1.2", path = "../lune-std-serde" }

View file

@ -1,163 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_ENCODING};
use lune_std_serde::{decompress, CompressDecompressFormat};
use lune_utils::TableBuilder;
use super::{config::RequestConfig, util::header_map_to_table};
const REGISTRY_KEY: &str = "NetClient";
pub struct NetClientBuilder {
builder: reqwest::ClientBuilder,
}
impl NetClientBuilder {
pub fn new() -> NetClientBuilder {
Self {
builder: reqwest::ClientBuilder::new(),
}
}
pub fn headers<K, V>(mut self, headers: &[(K, V)]) -> LuaResult<Self>
where
K: AsRef<str>,
V: AsRef<[u8]>,
{
let mut map = HeaderMap::new();
for (key, val) in headers {
let hkey = HeaderName::from_str(key.as_ref()).into_lua_err()?;
let hval = HeaderValue::from_bytes(val.as_ref()).into_lua_err()?;
map.insert(hkey, hval);
}
self.builder = self.builder.default_headers(map);
Ok(self)
}
pub fn build(self) -> LuaResult<NetClient> {
let client = self.builder.build().into_lua_err()?;
Ok(NetClient { inner: client })
}
}
#[derive(Debug, Clone)]
pub struct NetClient {
inner: reqwest::Client,
}
impl NetClient {
pub fn from_registry(lua: &Lua) -> Self {
lua.named_registry_value(REGISTRY_KEY)
.expect("Failed to get NetClient from lua registry")
}
pub fn into_registry(self, lua: &Lua) {
lua.set_named_registry_value(REGISTRY_KEY, self)
.expect("Failed to store NetClient in lua registry");
}
pub async fn request(&self, config: RequestConfig) -> LuaResult<NetClientResponse> {
// Create and send the request
let mut request = self.inner.request(config.method, config.url);
for (query, values) in config.query {
request = request.query(
&values
.iter()
.map(|v| (query.as_str(), v))
.collect::<Vec<_>>(),
);
}
for (header, values) in config.headers {
for value in values {
request = request.header(header.as_str(), value);
}
}
let res = request
.body(config.body.unwrap_or_default())
.send()
.await
.into_lua_err()?;
// Extract status, headers
let res_status = res.status().as_u16();
let res_status_text = res.status().canonical_reason();
let res_headers = res.headers().clone();
// Read response bytes
let mut res_bytes = res.bytes().await.into_lua_err()?.to_vec();
let mut res_decompressed = false;
// Check for extra options, decompression
if config.options.decompress {
let decompress_format = res_headers
.iter()
.find(|(name, _)| {
name.as_str()
.eq_ignore_ascii_case(CONTENT_ENCODING.as_str())
})
.and_then(|(_, value)| value.to_str().ok())
.and_then(CompressDecompressFormat::detect_from_header_str);
if let Some(format) = decompress_format {
res_bytes = decompress(res_bytes, format).await?;
res_decompressed = true;
}
}
Ok(NetClientResponse {
ok: (200..300).contains(&res_status),
status_code: res_status,
status_message: res_status_text.unwrap_or_default().to_string(),
headers: res_headers,
body: res_bytes,
body_decompressed: res_decompressed,
})
}
}
impl LuaUserData for NetClient {}
impl FromLua<'_> for NetClient {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::UserData(ud) = value {
if let Ok(ctx) = ud.borrow::<NetClient>() {
return Ok(ctx.clone());
}
}
unreachable!("NetClient should only be used from registry")
}
}
impl From<&Lua> for NetClient {
fn from(value: &Lua) -> Self {
value
.named_registry_value(REGISTRY_KEY)
.expect("Missing require context in lua registry")
}
}
pub struct NetClientResponse {
ok: bool,
status_code: u16,
status_message: String,
headers: HeaderMap,
body: Vec<u8>,
body_decompressed: bool,
}
impl NetClientResponse {
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_value("ok", self.ok)?
.with_value("statusCode", self.status_code)?
.with_value("statusMessage", self.status_message)?
.with_value(
"headers",
header_map_to_table(lua, self.headers, self.body_decompressed)?,
)?
.with_value("body", lua.create_string(&self.body)?)?
.build_readonly()
}
}

View file

@ -1,231 +0,0 @@
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr},
};
use bstr::{BString, ByteSlice};
use mlua::prelude::*;
use reqwest::Method;
use super::util::table_to_hash_map;
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
return {
status = 426,
body = "Upgrade Required",
headers = {
Upgrade = "websocket",
},
}
"#;
// Net request config
#[derive(Debug, Clone)]
pub struct RequestConfigOptions {
pub decompress: bool,
}
impl Default for RequestConfigOptions {
fn default() -> Self {
Self { decompress: true }
}
}
impl<'lua> FromLua<'lua> for RequestConfigOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::Nil = value {
// Nil means default options
Ok(Self::default())
} else if let LuaValue::Table(tab) = value {
// Table means custom options
let decompress = match tab.get::<_, Option<bool>>("decompress") {
Ok(decomp) => Ok(decomp.unwrap_or(true)),
Err(_) => Err(LuaError::RuntimeError(
"Invalid option value for 'decompress' in request config options".to_string(),
)),
}?;
Ok(Self { decompress })
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestConfigOptions",
message: Some(format!(
"Invalid request config options - expected table or nil, got {}",
value.type_name()
)),
})
}
}
}
#[derive(Debug, Clone)]
pub struct RequestConfig {
pub url: String,
pub method: Method,
pub query: HashMap<String, Vec<String>>,
pub headers: HashMap<String, Vec<String>>,
pub body: Option<Vec<u8>>,
pub options: RequestConfigOptions,
}
impl FromLua<'_> for RequestConfig {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
// If we just got a string we assume its a GET request to a given url
if let LuaValue::String(s) = value {
Ok(Self {
url: s.to_string_lossy().to_string(),
method: Method::GET,
query: HashMap::new(),
headers: HashMap::new(),
body: None,
options: RequestConfigOptions::default(),
})
} else if let LuaValue::Table(tab) = value {
// If we got a table we are able to configure the entire request
// Extract url
let url = match tab.get::<_, LuaString>("url") {
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
Err(_) => Err(LuaError::runtime("Missing 'url' in request config")),
}?;
// Extract method
let method = match tab.get::<_, LuaString>("method") {
Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(),
Err(_) => "GET".to_string(),
};
// Extract query
let query = match tab.get::<_, LuaTable>("query") {
Ok(tab) => table_to_hash_map(tab, "query")?,
Err(_) => HashMap::new(),
};
// Extract headers
let headers = match tab.get::<_, LuaTable>("headers") {
Ok(tab) => table_to_hash_map(tab, "headers")?,
Err(_) => HashMap::new(),
};
// Extract body
let body = match tab.get::<_, BString>("body") {
Ok(config_body) => Some(config_body.as_bytes().to_owned()),
Err(_) => None,
};
// Convert method string into proper enum
let method = method.trim().to_ascii_uppercase();
let method = match method.as_ref() {
"GET" => Ok(Method::GET),
"POST" => Ok(Method::POST),
"PUT" => Ok(Method::PUT),
"DELETE" => Ok(Method::DELETE),
"HEAD" => Ok(Method::HEAD),
"OPTIONS" => Ok(Method::OPTIONS),
"PATCH" => Ok(Method::PATCH),
_ => Err(LuaError::RuntimeError(format!(
"Invalid request config method '{}'",
&method
))),
}?;
// Parse any extra options given
let options = match tab.get::<_, LuaValue>("options") {
Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?,
Err(_) => RequestConfigOptions::default(),
};
// All good, validated and we got what we need
Ok(Self {
url,
method,
query,
headers,
body,
options,
})
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestConfig",
message: Some(format!(
"Invalid request config - expected string or table, got {}",
value.type_name()
)),
})
}
}
}
// Net serve config
#[derive(Debug)]
pub struct ServeConfig<'a> {
pub address: IpAddr,
pub handle_request: LuaFunction<'a>,
pub handle_web_socket: Option<LuaFunction<'a>>,
}
impl<'lua> FromLua<'lua> for ServeConfig<'lua> {
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::Function(f) = &value {
// Single function = request handler, rest is default
Ok(ServeConfig {
handle_request: f.clone(),
handle_web_socket: None,
address: DEFAULT_IP_ADDRESS,
})
} else if let LuaValue::Table(t) = &value {
// Table means custom options
let address: Option<LuaString> = t.get("address")?;
let handle_request: Option<LuaFunction> = t.get("handleRequest")?;
let handle_web_socket: Option<LuaFunction> = t.get("handleWebSocket")?;
if handle_request.is_some() || handle_web_socket.is_some() {
let address: IpAddr = match &address {
Some(addr) => {
let addr_str = addr.to_str()?;
addr_str
.trim_start_matches("http://")
.trim_start_matches("https://")
.parse()
.map_err(|_e| LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig",
message: Some(format!(
"IP address format is incorrect - \
expected an IP in the form 'http://0.0.0.0' or '0.0.0.0', \
got '{addr_str}'"
)),
})?
}
None => DEFAULT_IP_ADDRESS,
};
Ok(Self {
address,
handle_request: handle_request.unwrap_or_else(|| {
lua.load(WEB_SOCKET_UPDGRADE_REQUEST_HANDLER)
.into_function()
.expect("Failed to create default http responder function")
}),
handle_web_socket,
})
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig",
message: Some(String::from(
"Invalid serve config - expected table with 'handleRequest' or 'handleWebSocket' function",
)),
})
}
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig",
message: None,
})
}
}
}

View file

@ -1,102 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use bstr::BString;
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
mod client;
mod config;
mod server;
mod util;
mod websocket;
use lune_utils::TableBuilder;
use self::{
client::{NetClient, NetClientBuilder},
config::{RequestConfig, ServeConfig},
server::serve,
util::create_user_agent_header,
websocket::NetWebSocket,
};
use lune_std_serde::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
/**
Creates the `net` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
NetClientBuilder::new()
.headers(&[("User-Agent", create_user_agent_header(lua)?)])?
.build()?
.into_registry(lua);
TableBuilder::new(lua)?
.with_function("jsonEncode", net_json_encode)?
.with_function("jsonDecode", net_json_decode)?
.with_async_function("request", net_request)?
.with_async_function("socket", net_socket)?
.with_async_function("serve", net_serve)?
.with_function("urlEncode", net_url_encode)?
.with_function("urlDecode", net_url_decode)?
.build_readonly()
}
fn net_json_encode<'lua>(
lua: &'lua Lua,
(val, pretty): (LuaValue<'lua>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let config = EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()));
encode(val, lua, config)
}
fn net_json_decode(lua: &Lua, json: BString) -> LuaResult<LuaValue> {
let config = EncodeDecodeConfig::from(EncodeDecodeFormat::Json);
decode(json, lua, config)
}
async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
let client = NetClient::from_registry(lua);
// NOTE: We spawn the request as a background task to free up resources in lua
let res = lua.spawn(async move { client.request(config).await });
res.await?.into_lua_table(lua)
}
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaValue> {
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
NetWebSocket::new(ws).into_lua(lua)
}
async fn net_serve<'lua>(
lua: &'lua Lua,
(port, config): (u16, ServeConfig<'lua>),
) -> LuaResult<LuaTable<'lua>> {
serve(lua, port, config).await
}
fn net_url_encode<'lua>(
lua: &'lua Lua,
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
) -> LuaResult<LuaValue<'lua>> {
if matches!(as_binary, Some(true)) {
urlencoding::encode_binary(lua_string.as_bytes()).into_lua(lua)
} else {
urlencoding::encode(lua_string.to_str()?).into_lua(lua)
}
}
fn net_url_decode<'lua>(
lua: &'lua Lua,
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
) -> LuaResult<LuaValue<'lua>> {
if matches!(as_binary, Some(true)) {
urlencoding::decode_binary(lua_string.as_bytes()).into_lua(lua)
} else {
urlencoding::decode(lua_string.to_str()?)
.map_err(|e| LuaError::RuntimeError(format!("Encountered invalid encoding - {e}")))?
.into_lua(lua)
}
}

View file

@ -1,61 +0,0 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub(super) struct SvcKeys {
key_request: &'static str,
key_websocket: Option<&'static str>,
}
impl SvcKeys {
pub(super) fn new<'lua>(
lua: &'lua Lua,
handle_request: LuaFunction<'lua>,
handle_websocket: Option<LuaFunction<'lua>>,
) -> LuaResult<Self> {
static SERVE_COUNTER: AtomicUsize = AtomicUsize::new(0);
let count = SERVE_COUNTER.fetch_add(1, Ordering::Relaxed);
// NOTE: We leak strings here, but this is an acceptable tradeoff since programs
// generally only start one or a couple of servers and they are usually never dropped.
// Leaking here lets us keep this struct Copy and access the request handler callbacks
// very performantly, significantly reducing the per-request overhead of the server.
let key_request: &'static str =
Box::leak(format!("__net_serve_request_{count}").into_boxed_str());
let key_websocket: Option<&'static str> = if handle_websocket.is_some() {
Some(Box::leak(
format!("__net_serve_websocket_{count}").into_boxed_str(),
))
} else {
None
};
lua.set_named_registry_value(key_request, handle_request)?;
if let Some(key) = key_websocket {
lua.set_named_registry_value(key, handle_websocket.unwrap())?;
}
Ok(Self {
key_request,
key_websocket,
})
}
pub(super) fn has_websocket_handler(&self) -> bool {
self.key_websocket.is_some()
}
pub(super) fn request_handler<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaFunction<'lua>> {
lua.named_registry_value(self.key_request)
}
pub(super) fn websocket_handler<'lua>(
&self,
lua: &'lua Lua,
) -> LuaResult<Option<LuaFunction<'lua>>> {
self.key_websocket
.map(|key| lua.named_registry_value(key))
.transpose()
}
}

View file

@ -1,105 +0,0 @@
use std::{
net::SocketAddr,
rc::{Rc, Weak},
};
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use tokio::{net::TcpListener, pin};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use lune_utils::TableBuilder;
use super::config::ServeConfig;
mod keys;
mod request;
mod response;
mod service;
use keys::SvcKeys;
use service::Svc;
pub async fn serve<'lua>(
lua: &'lua Lua,
port: u16,
config: ServeConfig<'lua>,
) -> LuaResult<LuaTable<'lua>> {
let addr: SocketAddr = (config.address, port).into();
let listener = TcpListener::bind(addr).await?;
let (lua_svc, lua_inner) = {
let rc = lua
.app_data_ref::<Weak<Lua>>()
.expect("Missing weak lua ref")
.upgrade()
.expect("Lua was dropped unexpectedly");
(Rc::clone(&rc), rc)
};
let keys = SvcKeys::new(lua, config.handle_request, config.handle_web_socket)?;
let svc = Svc {
lua: lua_svc,
addr,
keys,
};
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
lua.spawn_local(async move {
let mut shutdown_rx_outer = shutdown_rx.clone();
loop {
// Create futures for accepting new connections and shutting down
let fut_shutdown = shutdown_rx_outer.changed();
let fut_accept = async {
let stream = match listener.accept().await {
Err(_) => return,
Ok((s, _)) => s,
};
let io = TokioIo::new(stream);
let svc = svc.clone();
let mut shutdown_rx_inner = shutdown_rx.clone();
lua_inner.spawn_local(async move {
let conn = http1::Builder::new()
.keep_alive(true) // Web sockets need this
.serve_connection(io, svc)
.with_upgrades();
// NOTE: Because we need to use keep_alive for websockets, we need to
// also manually poll this future and handle the shutdown signal here
pin!(conn);
tokio::select! {
_ = conn.as_mut() => {}
_ = shutdown_rx_inner.changed() => {
conn.as_mut().graceful_shutdown();
}
}
});
};
// Wait for either a new connection or a shutdown signal
tokio::select! {
() = fut_accept => {}
res = fut_shutdown => {
// NOTE: We will only get a RecvError here if the serve handle is dropped,
// this means lua has garbage collected it and the user does not want
// to manually stop the server using the serve handle. Run forever.
if res.is_ok() {
break;
}
}
}
}
});
TableBuilder::new(lua)?
.with_value("ip", addr.ip().to_string())?
.with_value("port", addr.port())?
.with_function("stop", move |_, (): ()| match shutdown_tx.send(true) {
Ok(()) => Ok(()),
Err(_) => Err(LuaError::runtime("Server already stopped")),
})?
.build_readonly()
}

View file

@ -1,56 +0,0 @@
use std::{collections::HashMap, net::SocketAddr};
use http::request::Parts;
use mlua::prelude::*;
use lune_utils::TableBuilder;
pub(super) struct LuaRequest {
pub(super) _remote_addr: SocketAddr,
pub(super) head: Parts,
pub(super) body: Vec<u8>,
}
impl LuaRequest {
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
let method = self.head.method.as_str().to_string();
let path = self.head.uri.path().to_string();
let body = lua.create_string(&self.body)?;
#[allow(clippy::mutable_key_type)]
let query: HashMap<LuaString, LuaString> = self
.head
.uri
.query()
.unwrap_or_default()
.split('&')
.filter_map(|q| q.split_once('='))
.map(|(k, v)| {
let k = lua.create_string(k)?;
let v = lua.create_string(v)?;
Ok((k, v))
})
.collect::<LuaResult<_>>()?;
#[allow(clippy::mutable_key_type)]
let headers: HashMap<LuaString, LuaString> = self
.head
.headers
.iter()
.map(|(k, v)| {
let k = lua.create_string(k.as_str())?;
let v = lua.create_string(v.as_bytes())?;
Ok((k, v))
})
.collect::<LuaResult<_>>()?;
TableBuilder::new(lua)?
.with_value("method", method)?
.with_value("path", path)?
.with_value("query", query)?
.with_value("headers", headers)?
.with_value("body", body)?
.build()
}
}

View file

@ -1,89 +0,0 @@
use std::str::FromStr;
use bstr::{BString, ByteSlice};
use http_body_util::Full;
use hyper::{
body::Bytes,
header::{HeaderName, HeaderValue},
HeaderMap, Response,
};
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub(super) enum LuaResponseKind {
PlainText,
Table,
}
pub(super) struct LuaResponse {
pub(super) kind: LuaResponseKind,
pub(super) status: u16,
pub(super) headers: HeaderMap,
pub(super) body: Option<Vec<u8>>,
}
impl LuaResponse {
pub(super) fn into_response(self) -> LuaResult<Response<Full<Bytes>>> {
Ok(match self.kind {
LuaResponseKind::PlainText => Response::builder()
.status(200)
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(self.body.unwrap())))
.into_lua_err()?,
LuaResponseKind::Table => {
let mut response = Response::builder()
.status(self.status)
.body(Full::new(Bytes::from(self.body.unwrap_or_default())))
.into_lua_err()?;
response.headers_mut().extend(self.headers);
response
}
})
}
}
impl FromLua<'_> for LuaResponse {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
match value {
// Plain strings from the handler are plaintext responses
LuaValue::String(s) => Ok(Self {
kind: LuaResponseKind::PlainText,
status: 200,
headers: HeaderMap::new(),
body: Some(s.as_bytes().to_vec()),
}),
// Tables are more detailed responses with potential status, headers, body
LuaValue::Table(t) => {
let status: Option<u16> = t.get("status")?;
let headers: Option<LuaTable> = t.get("headers")?;
let body: Option<BString> = t.get("body")?;
let mut headers_map = HeaderMap::new();
if let Some(headers) = headers {
for pair in headers.pairs::<String, LuaString>() {
let (h, v) = pair?;
let name = HeaderName::from_str(&h).into_lua_err()?;
let value = HeaderValue::from_bytes(v.as_bytes()).into_lua_err()?;
headers_map.insert(name, value);
}
}
let body_bytes = body.map(|s| s.as_bytes().to_vec());
Ok(Self {
kind: LuaResponseKind::Table,
status: status.unwrap_or(200),
headers: headers_map,
body: body_bytes,
})
}
// Anything else is an error
value => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "NetServeResponse",
message: None,
}),
}
}
}

View file

@ -1,82 +0,0 @@
use std::{future::Future, net::SocketAddr, pin::Pin, rc::Rc};
use http_body_util::{BodyExt, Full};
use hyper::{
body::{Bytes, Incoming},
service::Service,
Request, Response,
};
use hyper_tungstenite::{is_upgrade_request, upgrade};
use mlua::prelude::*;
use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt};
use super::{
super::websocket::NetWebSocket, keys::SvcKeys, request::LuaRequest, response::LuaResponse,
};
#[derive(Debug, Clone)]
pub(super) struct Svc {
pub(super) lua: Rc<Lua>,
pub(super) addr: SocketAddr,
pub(super) keys: SvcKeys,
}
impl Service<Request<Incoming>> for Svc {
type Response = Response<Full<Bytes>>;
type Error = LuaError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn call(&self, req: Request<Incoming>) -> Self::Future {
let lua = self.lua.clone();
let addr = self.addr;
let keys = self.keys;
if keys.has_websocket_handler() && is_upgrade_request(&req) {
Box::pin(async move {
let (res, sock) = upgrade(req, None).into_lua_err()?;
let lua_inner = lua.clone();
lua.spawn_local(async move {
let sock = sock.await.unwrap();
let lua_sock = NetWebSocket::new(sock);
let lua_val = lua_sock.into_lua(&lua_inner).unwrap();
let handler_websocket: LuaFunction =
keys.websocket_handler(&lua_inner).unwrap().unwrap();
lua_inner
.push_thread_back(handler_websocket, lua_val)
.unwrap();
});
Ok(res)
})
} else {
let (head, body) = req.into_parts();
Box::pin(async move {
let handler_request: LuaFunction = keys.request_handler(&lua).unwrap();
let body = body.collect().await.into_lua_err()?;
let body = body.to_bytes().to_vec();
let lua_req = LuaRequest {
_remote_addr: addr,
head,
body,
};
let lua_req_table = lua_req.into_lua_table(&lua)?;
let thread_id = lua.push_thread_back(handler_request, lua_req_table)?;
lua.track_thread(thread_id);
lua.wait_for_thread(thread_id).await;
let thread_res = lua
.get_thread_result(thread_id)
.expect("Missing handler thread result")?;
LuaResponse::from_lua_multi(thread_res, &lua)?.into_response()
})
}
}
}

View file

@ -1,94 +0,0 @@
use std::collections::HashMap;
use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH};
use reqwest::header::HeaderMap;
use mlua::prelude::*;
use lune_utils::TableBuilder;
pub fn create_user_agent_header(lua: &Lua) -> LuaResult<String> {
let version_global = lua
.globals()
.get::<_, LuaString>("_VERSION")
.expect("Missing _VERSION global");
let version_global_str = version_global
.to_str()
.context("Invalid utf8 found in _VERSION global")?;
let (package_name, full_version) = version_global_str.split_once(' ').unwrap();
Ok(format!("{}/{}", package_name.to_lowercase(), full_version))
}
pub fn header_map_to_table(
lua: &Lua,
headers: HeaderMap,
remove_content_headers: bool,
) -> LuaResult<LuaTable> {
let mut res_headers: HashMap<String, Vec<String>> = HashMap::new();
for (name, value) in &headers {
let name = name.as_str();
let value = value.to_str().unwrap().to_owned();
if let Some(existing) = res_headers.get_mut(name) {
existing.push(value);
} else {
res_headers.insert(name.to_owned(), vec![value]);
}
}
if remove_content_headers {
let content_encoding_header_str = CONTENT_ENCODING.as_str();
let content_length_header_str = CONTENT_LENGTH.as_str();
res_headers.retain(|name, _| {
name != content_encoding_header_str && name != content_length_header_str
});
}
let mut builder = TableBuilder::new(lua)?;
for (name, mut values) in res_headers {
if values.len() == 1 {
let value = values.pop().unwrap().into_lua(lua)?;
builder = builder.with_value(name, value)?;
} else {
let values = TableBuilder::new(lua)?
.with_sequential_values(values)?
.build_readonly()?
.into_lua(lua)?;
builder = builder.with_value(name, values)?;
}
}
builder.build_readonly()
}
pub fn table_to_hash_map(
tab: LuaTable,
tab_origin_key: &'static str,
) -> LuaResult<HashMap<String, Vec<String>>> {
let mut map = HashMap::new();
for pair in tab.pairs::<String, LuaValue>() {
let (key, value) = pair?;
match value {
LuaValue::String(s) => {
map.insert(key, vec![s.to_str()?.to_owned()]);
}
LuaValue::Table(t) => {
let mut values = Vec::new();
for value in t.sequence_values::<LuaString>() {
values.push(value?.to_str()?.to_owned());
}
map.insert(key, values);
}
_ => {
return Err(LuaError::runtime(format!(
"Value for '{tab_origin_key}' must be a string or array of strings",
)))
}
}
}
Ok(map)
}

View file

@ -1,149 +0,0 @@
use std::sync::{
atomic::{AtomicBool, AtomicU16, Ordering},
Arc,
};
use bstr::{BString, ByteSlice};
use mlua::prelude::*;
use futures_util::{
stream::{SplitSink, SplitStream},
SinkExt, StreamExt,
};
use tokio::{
io::{AsyncRead, AsyncWrite},
sync::Mutex as AsyncMutex,
};
use hyper_tungstenite::{
tungstenite::{
protocol::{frame::coding::CloseCode as WsCloseCode, CloseFrame as WsCloseFrame},
Message as WsMessage,
},
WebSocketStream,
};
#[derive(Debug)]
pub struct NetWebSocket<T> {
close_code_exists: Arc<AtomicBool>,
close_code_value: Arc<AtomicU16>,
read_stream: Arc<AsyncMutex<SplitStream<WebSocketStream<T>>>>,
write_stream: Arc<AsyncMutex<SplitSink<WebSocketStream<T>, WsMessage>>>,
}
impl<T> Clone for NetWebSocket<T> {
fn clone(&self) -> Self {
Self {
close_code_exists: Arc::clone(&self.close_code_exists),
close_code_value: Arc::clone(&self.close_code_value),
read_stream: Arc::clone(&self.read_stream),
write_stream: Arc::clone(&self.write_stream),
}
}
}
impl<T> NetWebSocket<T>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
pub fn new(value: WebSocketStream<T>) -> Self {
let (write, read) = value.split();
Self {
close_code_exists: Arc::new(AtomicBool::new(false)),
close_code_value: Arc::new(AtomicU16::new(0)),
read_stream: Arc::new(AsyncMutex::new(read)),
write_stream: Arc::new(AsyncMutex::new(write)),
}
}
fn get_close_code(&self) -> Option<u16> {
if self.close_code_exists.load(Ordering::Relaxed) {
Some(self.close_code_value.load(Ordering::Relaxed))
} else {
None
}
}
fn set_close_code(&self, code: u16) {
self.close_code_exists.store(true, Ordering::Relaxed);
self.close_code_value.store(code, Ordering::Relaxed);
}
pub async fn send(&self, msg: WsMessage) -> LuaResult<()> {
let mut ws = self.write_stream.lock().await;
ws.send(msg).await.into_lua_err()
}
pub async fn next(&self) -> LuaResult<Option<WsMessage>> {
let mut ws = self.read_stream.lock().await;
ws.next().await.transpose().into_lua_err()
}
pub async fn close(&self, code: Option<u16>) -> LuaResult<()> {
if self.close_code_exists.load(Ordering::Relaxed) {
return Err(LuaError::runtime("Socket has already been closed"));
}
self.send(WsMessage::Close(Some(WsCloseFrame {
code: match code {
Some(code) if (1000..=4999).contains(&code) => WsCloseCode::from(code),
Some(code) => {
return Err(LuaError::runtime(format!(
"Close code must be between 1000 and 4999, got {code}"
)))
}
None => WsCloseCode::Normal,
},
reason: "".into(),
})))
.await?;
let mut ws = self.write_stream.lock().await;
ws.close().await.into_lua_err()
}
}
impl<T> LuaUserData for NetWebSocket<T>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("closeCode", |_, this| Ok(this.get_close_code()));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method("close", |_, this, code: Option<u16>| async move {
this.close(code).await
});
methods.add_async_method(
"send",
|_, this, (string, as_binary): (BString, Option<bool>)| async move {
this.send(if as_binary.unwrap_or_default() {
WsMessage::Binary(string.as_bytes().to_vec())
} else {
let s = string.to_str().into_lua_err()?;
WsMessage::Text(s.to_string())
})
.await
},
);
methods.add_async_method("next", |lua, this, (): ()| async move {
let msg = this.next().await?;
if let Some(WsMessage::Close(Some(frame))) = msg.as_ref() {
this.set_close_code(frame.code.into());
}
Ok(match msg {
Some(WsMessage::Binary(bin)) => LuaValue::String(lua.create_string(bin)?),
Some(WsMessage::Text(txt)) => LuaValue::String(lua.create_string(txt)?),
Some(WsMessage::Close(_)) | None => LuaValue::Nil,
// Ignore ping/pong/frame messages, they are handled by tungstenite
msg => unreachable!("Unhandled message: {:?}", msg),
})
});
}
}

View file

@ -1,34 +0,0 @@
[package]
name = "lune-std-process"
version = "0.1.3"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Process"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
directories = "5.0"
pin-project = "1.0"
os_str_bytes = { version = "7.0", features = ["conversions"] }
bstr = "1.9"
bytes = "1.6.0"
tokio = { version = "1", default-features = false, features = [
"io-std",
"io-util",
"process",
"rt",
"sync",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,289 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::{
cell::RefCell,
env::{
self,
consts::{ARCH, OS},
},
path::MAIN_SEPARATOR,
process::Stdio,
rc::Rc,
sync::Arc,
};
use mlua::prelude::*;
use lune_utils::TableBuilder;
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
use options::ProcessSpawnOptionsStdio;
use os_str_bytes::RawOsString;
use stream::{ChildProcessReader, ChildProcessWriter};
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
mod options;
mod stream;
mod tee_writer;
mod wait_for_child;
use self::options::ProcessSpawnOptions;
use self::wait_for_child::wait_for_child;
use lune_utils::path::get_current_dir;
/**
Creates the `process` standard library module.
# Errors
Errors when out of memory.
*/
#[allow(clippy::missing_panics_doc)]
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
let mut cwd_str = get_current_dir()
.to_str()
.expect("cwd should be valid UTF-8")
.to_string();
if !cwd_str.ends_with(MAIN_SEPARATOR) {
cwd_str.push(MAIN_SEPARATOR);
}
// Create constants for OS & processor architecture
let os = lua.create_string(OS.to_lowercase())?;
let arch = lua.create_string(ARCH.to_lowercase())?;
let endianness = lua.create_string(if cfg!(target_endian = "big") {
"big"
} else {
"little"
})?;
// Create readonly args array
let args_vec = lua
.app_data_ref::<Vec<String>>()
.ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))?
.clone();
let args_tab = TableBuilder::new(lua)?
.with_sequential_values(args_vec)?
.build_readonly()?;
// Create proxied table for env that gets & sets real env vars
let env_tab = TableBuilder::new(lua)?
.with_metatable(
TableBuilder::new(lua)?
.with_function(LuaMetaMethod::Index.name(), process_env_get)?
.with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
.with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
.build_readonly()?,
)?
.build_readonly()?;
// Create our process exit function, the scheduler crate provides this
let fns = Functions::new(lua)?;
let process_exit = fns.exit;
// Create the full process table
TableBuilder::new(lua)?
.with_value("os", os)?
.with_value("arch", arch)?
.with_value("endianness", endianness)?
.with_value("args", args_tab)?
.with_value("cwd", cwd_str)?
.with_value("env", env_tab)?
.with_value("exit", process_exit)?
.with_async_function("exec", process_exec)?
.with_function("create", process_create)?
.build_readonly()
}
fn process_env_get<'lua>(
lua: &'lua Lua,
(_, key): (LuaValue<'lua>, String),
) -> LuaResult<LuaValue<'lua>> {
match env::var_os(key) {
Some(value) => {
let raw_value = RawOsString::new(value);
Ok(LuaValue::String(
lua.create_string(raw_value.to_raw_bytes())?,
))
}
None => Ok(LuaValue::Nil),
}
}
fn process_env_set<'lua>(
_: &'lua Lua,
(_, key, value): (LuaValue<'lua>, String, Option<String>),
) -> LuaResult<()> {
// Make sure key is valid, otherwise set_var will panic
if key.is_empty() {
Err(LuaError::RuntimeError("Key must not be empty".to_string()))
} else if key.contains('=') {
Err(LuaError::RuntimeError(
"Key must not contain the equals character '='".to_string(),
))
} else if key.contains('\0') {
Err(LuaError::RuntimeError(
"Key must not contain the NUL character".to_string(),
))
} else if let Some(value) = value {
// Make sure value is valid, otherwise set_var will panic
if value.contains('\0') {
Err(LuaError::RuntimeError(
"Value must not contain the NUL character".to_string(),
))
} else {
env::set_var(&key, &value);
Ok(())
}
} else {
env::remove_var(&key);
Ok(())
}
}
fn process_env_iter<'lua>(
lua: &'lua Lua,
(_, ()): (LuaValue<'lua>, ()),
) -> LuaResult<LuaFunction<'lua>> {
let mut vars = env::vars_os().collect::<Vec<_>>().into_iter();
lua.create_function_mut(move |lua, (): ()| match vars.next() {
Some((key, value)) => {
let raw_key = RawOsString::new(key);
let raw_value = RawOsString::new(value);
Ok((
LuaValue::String(lua.create_string(raw_key.to_raw_bytes())?),
LuaValue::String(lua.create_string(raw_value.to_raw_bytes())?),
))
}
None => Ok((LuaValue::Nil, LuaValue::Nil)),
})
}
async fn process_exec(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
let res = lua
.spawn(async move {
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
})
.await?;
/*
NOTE: If an exit code was not given by the child process,
we default to 1 if it yielded any error output, otherwise 0
An exit code may be missing if the process was terminated by
some external signal, which is the only time we use this default
*/
let code = res
.status
.code()
.unwrap_or(i32::from(!res.stderr.is_empty()));
// Construct and return a readonly lua table with results
TableBuilder::new(lua)?
.with_value("ok", code == 0)?
.with_value("code", code)?
.with_value("stdout", lua.create_string(&res.stdout)?)?
.with_value("stderr", lua.create_string(&res.stderr)?)?
.build_readonly()
}
#[allow(clippy::await_holding_refcell_ref)]
fn process_create(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
// We do not want the user to provide stdio options for process.create,
// so we reset the options, regardless of what the user provides us
let mut spawn_options = options.clone();
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
let code_rx_rc = Rc::new(RefCell::new(code_rx));
let child = spawn_command(program, args, spawn_options)?;
let child_arc = Arc::new(RwLock::new(child));
let child_arc_clone = Arc::clone(&child_arc);
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
let stdin = child_lock.stdin.take().unwrap();
let stdout = child_lock.stdout.take().unwrap();
let stderr = child_lock.stderr.take().unwrap();
let child_arc_inner = Arc::clone(&child_arc);
// Spawn a background task to wait for the child to exit and send the exit code
let status_handle = tokio::spawn(async move {
let res = child_arc_inner.write().await.wait().await;
if let Ok(output) = res {
let code = output.code().unwrap_or_default();
code_tx
.send(code)
.expect("ExitCode receiver was unexpectedly dropped");
}
});
TableBuilder::new(lua)?
.with_value("stdout", ChildProcessReader(stdout))?
.with_value("stderr", ChildProcessReader(stderr))?
.with_value("stdin", ChildProcessWriter(stdin))?
.with_async_function("kill", move |_, ()| {
// First, stop the status task so the RwLock is dropped
status_handle.abort();
let child_arc_clone = Arc::clone(&child_arc);
// Then get another RwLock to write to the child process and kill it
async move { Ok(child_arc_clone.write().await.kill().await?) }
})?
.with_async_function("status", move |lua, ()| {
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
async move {
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
// the receiver gets suddenly dropped
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
TableBuilder::new(lua)?
.with_value("code", code)?
.with_value("ok", code == 0)?
.build_readonly()
}
})?
.build_readonly()
}
async fn spawn_command_with_stdin(
program: String,
args: Option<Vec<String>>,
mut options: ProcessSpawnOptions,
) -> LuaResult<Child> {
let stdin = options.stdio.stdin.take();
let mut child = spawn_command(program, args, options)?;
if let Some(stdin) = stdin {
let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(&stdin).await.into_lua_err()?;
}
Ok(child)
}
fn spawn_command(
program: String,
args: Option<Vec<String>>,
options: ProcessSpawnOptions,
) -> LuaResult<Child> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let child = options
.into_command(program, args)
.stdin(Stdio::piped())
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
Ok(child)
}

View file

@ -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<'lua> FromLua<'lua> for ProcessSpawnOptionsStdioKind {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
LuaValue::Nil => Ok(Self::default()),
LuaValue::String(s) => s.to_str()?.parse(),
_ => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ProcessSpawnOptionsStdioKind",
message: Some(format!(
"Invalid spawn options stdio kind - expected string, got {}",
value.type_name()
)),
}),
}
}
}

View file

@ -1,177 +0,0 @@
use std::{
collections::HashMap,
env::{self},
path::PathBuf,
};
use directories::UserDirs;
use mlua::prelude::*;
use tokio::process::Command;
mod kind;
mod stdio;
pub(super) use kind::*;
pub(super) use stdio::*;
#[derive(Debug, Clone, Default)]
pub(super) struct ProcessSpawnOptions {
pub cwd: Option<PathBuf>,
pub envs: HashMap<String, String>,
pub shell: Option<String>,
pub stdio: ProcessSpawnOptionsStdio,
}
impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua 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",
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()?);
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 - note that
we accept a separate "stdin" value here for compatibility with older
scripts, but the user should preferrably pass it in the stdio table
*/
this.stdio = value.get("stdio")?;
match value.get("stdin")? {
LuaValue::Nil => {}
LuaValue::String(s) => this.stdio.stdin = Some(s.as_bytes().to_vec()),
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'stdin' - expected 'string', got '{}'",
value.type_name()
)))
}
}
Ok(this)
}
}
impl ProcessSpawnOptions {
pub fn into_command(self, program: impl Into<String>, args: Option<Vec<String>>) -> Command {
let mut program = program.into();
// Run a shell using the command param if wanted
let pargs = match self.shell {
None => args,
Some(shell) => {
let shell_args = match args {
Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))],
None => vec!["-c".to_string(), program.to_string()],
};
program = shell.to_string();
Some(shell_args)
}
};
// Create command with the wanted options
let mut cmd = match pargs {
None => Command::new(program),
Some(args) => {
let mut cmd = Command::new(program);
cmd.args(args);
cmd
}
};
// 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
}
}

View file

@ -1,56 +0,0 @@
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<'lua> FromLua<'lua> for ProcessSpawnOptionsStdio {
fn from_lua(value: LuaValue<'lua>, lua: &'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("stdin")? {
this.stdin = stdin;
}
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",
message: Some(format!(
"Invalid spawn options stdio - expected string or table, got {}",
value.type_name()
)),
}),
}
}
}

View file

@ -1,58 +0,0 @@
use bstr::BString;
use bytes::BytesMut;
use mlua::prelude::*;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
const CHUNK_SIZE: usize = 8;
#[derive(Debug, Clone)]
pub struct ChildProcessReader<R: AsyncRead>(pub R);
#[derive(Debug, Clone)]
pub struct ChildProcessWriter<W: AsyncWrite>(pub W);
impl<R: AsyncRead + Unpin> ChildProcessReader<R> {
pub async fn read(&mut self, chunk_size: Option<usize>) -> LuaResult<Vec<u8>> {
let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE));
self.0.read_buf(&mut buf).await?;
Ok(buf.to_vec())
}
pub async fn read_to_end(&mut self) -> LuaResult<Vec<u8>> {
let mut buf = vec![];
self.0.read_to_end(&mut buf).await?;
Ok(buf)
}
}
impl<R: AsyncRead + Unpin + 'static> LuaUserData for ChildProcessReader<R> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("read", |lua, this, chunk_size: Option<usize>| async move {
let buf = this.read(chunk_size).await?;
if buf.is_empty() {
return Ok(LuaValue::Nil);
}
Ok(LuaValue::String(lua.create_string(buf)?))
});
methods.add_async_method_mut("readToEnd", |lua, this, ()| async {
Ok(lua.create_string(this.read_to_end().await?))
});
}
}
impl<W: AsyncWrite + Unpin> ChildProcessWriter<W> {
pub async fn write(&mut self, data: BString) -> LuaResult<()> {
self.0.write_all(data.as_ref()).await?;
Ok(())
}
}
impl<W: AsyncWrite + Unpin + 'static> LuaUserData for ChildProcessWriter<W> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await });
}
}

View file

@ -1,73 +0,0 @@
use std::process::ExitStatus;
use mlua::prelude::*;
use tokio::{
io::{self, AsyncRead, AsyncReadExt},
process::Child,
task,
};
use super::{options::ProcessSpawnOptionsStdioKind, tee_writer::AsyncTeeWriter};
#[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 = io::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 = task::spawn(read_with_stdio_kind(stdout_opt, stdout_kind));
let stderr_task = task::spawn(read_with_stdio_kind(stderr_opt, stderr_kind));
let status = child.wait().await.expect("Child process failed to start");
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,
})
}

View file

@ -1,21 +0,0 @@
[package]
name = "lune-std-regex"
version = "0.1.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.9.9", features = ["luau"] }
regex = "1.10"
self_cell = "1.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -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<'lua, M: LuaUserDataMethods<'lua, 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<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures");
}
}

View file

@ -1,28 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use lune_utils::TableBuilder;
mod captures;
mod matches;
mod regex;
use self::regex::LuaRegex;
/**
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)
}

View file

@ -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<'lua, F: LuaUserDataFields<'lua, 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<'lua, M: LuaUserDataMethods<'lua, 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())
});
}
}

View file

@ -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<'lua, M: LuaUserDataMethods<'lua, 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<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_meta_field(LuaMetaMethod::Type, "Regex");
}
}

View file

@ -1,24 +0,0 @@
[package]
name = "lune-std-roblox"
version = "0.1.4"
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.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
once_cell = "1.17"
rbx_cookie = { version = "0.1.4", default-features = false }
roblox_install = "1.0.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-roblox = { version = "0.1.4", path = "../lune-roblox" }

View file

@ -1,178 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use once_cell::sync::OnceCell;
use lune_roblox::{
document::{Document, DocumentError, DocumentFormat, DocumentKind},
instance::{registry::InstanceRegistry, Instance},
reflection::Database as ReflectionDatabase,
};
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
use lune_utils::TableBuilder;
use roblox_install::RobloxStudio;
/**
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)?;
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: &'lua Lua,
contents: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
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: &'lua Lua,
contents: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
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: &'lua Lua,
(data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
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: &'lua Lua,
(instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
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)
}

View file

@ -1,47 +0,0 @@
[package]
name = "lune-std-serde"
version = "0.1.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.9.9", features = ["luau", "serialize"] }
async-compression = { version = "0.4", features = [
"tokio",
"brotli",
"deflate",
"gzip",
"zlib",
] }
bstr = "1.9"
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"] }
tokio = { version = "1", default-features = false, features = [
"rt",
"io-util",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,158 +0,0 @@
use mlua::prelude::*;
use serde_json::Value as JsonValue;
use serde_yaml::Value as YamlValue;
use toml::Value as TomlValue;
// NOTE: These are options for going from other format -> lua ("serializing" lua values)
const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new()
.set_array_metatable(false)
.serialize_none_to_null(false)
.serialize_unit_to_null(false);
// NOTE: These are options for going from lua -> other format ("deserializing" lua values)
const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new()
.sort_keys(true)
.deny_recursive_tables(false)
.deny_unsupported_types(true);
/**
An encoding and decoding format supported by Lune.
Encode / decode in this case is synonymous with serialize / deserialize.
*/
#[derive(Debug, Clone, Copy)]
pub enum EncodeDecodeFormat {
Json,
Yaml,
Toml,
}
impl<'lua> FromLua<'lua> for EncodeDecodeFormat {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = &value {
match s.to_string_lossy().to_ascii_lowercase().trim() {
"json" => Ok(Self::Json),
"yaml" => Ok(Self::Yaml),
"toml" => Ok(Self::Toml),
kind => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "EncodeDecodeFormat",
message: Some(format!(
"Invalid format '{kind}', valid formats are: json, yaml, toml"
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "EncodeDecodeFormat",
message: None,
})
}
}
}
/**
Configuration for encoding and decoding values.
Encoding / decoding in this case is synonymous with serialize / deserialize.
*/
#[derive(Debug, Clone, Copy)]
pub struct EncodeDecodeConfig {
pub format: EncodeDecodeFormat,
pub pretty: bool,
}
impl From<EncodeDecodeFormat> for EncodeDecodeConfig {
fn from(format: EncodeDecodeFormat) -> Self {
Self {
format,
pretty: false,
}
}
}
impl From<(EncodeDecodeFormat, bool)> for EncodeDecodeConfig {
fn from(value: (EncodeDecodeFormat, bool)) -> Self {
Self {
format: value.0,
pretty: value.1,
}
}
}
/**
Encodes / serializes the given value into a string, using the specified configuration.
# Errors
Errors when the encoding fails.
*/
pub fn encode<'lua>(
value: LuaValue<'lua>,
lua: &'lua Lua,
config: EncodeDecodeConfig,
) -> LuaResult<LuaString<'lua>> {
let bytes = match config.format {
EncodeDecodeFormat::Json => {
let serialized: JsonValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
if config.pretty {
serde_json::to_vec_pretty(&serialized).into_lua_err()?
} else {
serde_json::to_vec(&serialized).into_lua_err()?
}
}
EncodeDecodeFormat::Yaml => {
let serialized: YamlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
let mut writer = Vec::with_capacity(128);
serde_yaml::to_writer(&mut writer, &serialized).into_lua_err()?;
writer
}
EncodeDecodeFormat::Toml => {
let serialized: TomlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
let s = if config.pretty {
toml::to_string_pretty(&serialized).into_lua_err()?
} else {
toml::to_string(&serialized).into_lua_err()?
};
s.as_bytes().to_vec()
}
};
lua.create_string(bytes)
}
/**
Decodes / deserializes the given string into a value, using the specified configuration.
# Errors
Errors when the decoding fails.
*/
pub fn decode(
bytes: impl AsRef<[u8]>,
lua: &Lua,
config: EncodeDecodeConfig,
) -> LuaResult<LuaValue> {
let bytes = bytes.as_ref();
match config.format {
EncodeDecodeFormat::Json => {
let value: JsonValue = serde_json::from_slice(bytes).into_lua_err()?;
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
}
EncodeDecodeFormat::Yaml => {
let value: YamlValue = serde_yaml::from_slice(bytes).into_lua_err()?;
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
}
EncodeDecodeFormat::Toml => {
if let Ok(s) = String::from_utf8(bytes.to_vec()) {
let value: TomlValue = toml::from_str(&s).into_lua_err()?;
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
} else {
Err(LuaError::RuntimeError(
"TOML must be valid utf-8".to_string(),
))
}
}
}
}

View file

@ -1,260 +0,0 @@
use std::fmt::Write;
use bstr::BString;
use md5::Md5;
use mlua::prelude::*;
use blake3::Hasher as Blake3;
use sha1::Sha1;
use sha2::{Sha224, Sha256, Sha384, Sha512};
use sha3::{Sha3_224, Sha3_256, Sha3_384, Sha3_512};
pub struct HashOptions {
algorithm: HashAlgorithm,
message: BString,
secret: Option<BString>,
// seed: Option<BString>,
}
#[derive(Debug, Clone, Copy)]
enum HashAlgorithm {
Md5,
Sha1,
// SHA-2 variants
Sha2_224,
Sha2_256,
Sha2_384,
Sha2_512,
// SHA-3 variants
Sha3_224,
Sha3_256,
Sha3_384,
Sha3_512,
// Blake3
Blake3,
}
impl HashAlgorithm {
pub const ALL: [Self; 11] = [
Self::Md5,
Self::Sha1,
Self::Sha2_224,
Self::Sha2_256,
Self::Sha2_384,
Self::Sha2_512,
Self::Sha3_224,
Self::Sha3_256,
Self::Sha3_384,
Self::Sha3_512,
Self::Blake3,
];
pub const fn name(self) -> &'static str {
match self {
Self::Md5 => "md5",
Self::Sha1 => "sha1",
Self::Sha2_224 => "sha224",
Self::Sha2_256 => "sha256",
Self::Sha2_384 => "sha384",
Self::Sha2_512 => "sha512",
Self::Sha3_224 => "sha3-224",
Self::Sha3_256 => "sha3-256",
Self::Sha3_384 => "sha3-384",
Self::Sha3_512 => "sha3-512",
Self::Blake3 => "blake3",
}
}
}
impl HashOptions {
/**
Computes the hash for the `message` using whatever `algorithm` is
contained within this struct and returns it as a string of hex digits.
*/
#[inline]
#[must_use = "hashing a message is useless without using the resulting hash"]
pub fn hash(self) -> String {
use digest::Digest;
let message = self.message;
let bytes = match self.algorithm {
HashAlgorithm::Md5 => Md5::digest(message).to_vec(),
HashAlgorithm::Sha1 => Sha1::digest(message).to_vec(),
HashAlgorithm::Sha2_224 => Sha224::digest(message).to_vec(),
HashAlgorithm::Sha2_256 => Sha256::digest(message).to_vec(),
HashAlgorithm::Sha2_384 => Sha384::digest(message).to_vec(),
HashAlgorithm::Sha2_512 => Sha512::digest(message).to_vec(),
HashAlgorithm::Sha3_224 => Sha3_224::digest(message).to_vec(),
HashAlgorithm::Sha3_256 => Sha3_256::digest(message).to_vec(),
HashAlgorithm::Sha3_384 => Sha3_384::digest(message).to_vec(),
HashAlgorithm::Sha3_512 => Sha3_512::digest(message).to_vec(),
HashAlgorithm::Blake3 => Blake3::digest(message).to_vec(),
};
// We don't want to return raw binary data generally, since that's not
// what most people want a hash for. So we have to make a hex string.
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
})
}
/**
Computes the HMAC for the `message` using whatever `algorithm` and
`secret` are contained within this struct. The computed value is
returned as a string of hex digits.
# Errors
If the `secret` is not provided or is otherwise invalid.
*/
#[inline]
pub fn hmac(self) -> LuaResult<String> {
use hmac::{Hmac, Mac, SimpleHmac};
let secret = self
.secret
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "string or buffer",
message: Some("Argument #3 missing or nil".to_string()),
})?;
/*
These macros exist to remove what would ultimately be dozens of
repeating lines. Essentially, there's several step to processing
HMacs, which expands into the 3 lines you see below. However,
the Hmac struct is specialized towards eager block-based processes.
In order to support anything else, like blake3, there's a second
type named `SimpleHmac`. This results in duplicate macros like
there are below.
*/
macro_rules! hmac {
($Type:ty) => {{
let mut mac: Hmac<$Type> = Hmac::new_from_slice(&secret).into_lua_err()?;
mac.update(&self.message);
mac.finalize().into_bytes().to_vec()
}};
}
macro_rules! hmac_no_blocks {
($Type:ty) => {{
let mut mac: SimpleHmac<$Type> =
SimpleHmac::new_from_slice(&secret).into_lua_err()?;
mac.update(&self.message);
mac.finalize().into_bytes().to_vec()
}};
}
let bytes = match self.algorithm {
HashAlgorithm::Md5 => hmac!(Md5),
HashAlgorithm::Sha1 => hmac!(Sha1),
HashAlgorithm::Sha2_224 => hmac!(Sha224),
HashAlgorithm::Sha2_256 => hmac!(Sha256),
HashAlgorithm::Sha2_384 => hmac!(Sha384),
HashAlgorithm::Sha2_512 => hmac!(Sha512),
HashAlgorithm::Sha3_224 => hmac!(Sha3_224),
HashAlgorithm::Sha3_256 => hmac!(Sha3_256),
HashAlgorithm::Sha3_384 => hmac!(Sha3_384),
HashAlgorithm::Sha3_512 => hmac!(Sha3_512),
HashAlgorithm::Blake3 => hmac_no_blocks!(Blake3),
};
Ok(bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
}))
}
}
impl<'lua> FromLua<'lua> for HashAlgorithm {
fn from_lua(value: LuaValue<'lua>, _lua: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::String(str) = value {
/*
Casing tends to vary for algorithms, so rather than force
people to remember it we'll just accept any casing.
*/
let str = str.to_str()?.to_ascii_lowercase();
match str.as_str() {
"md5" => Ok(Self::Md5),
"sha1" => Ok(Self::Sha1),
"sha2-224" | "sha2_224" | "sha224" => Ok(Self::Sha2_224),
"sha2-256" | "sha2_256" | "sha256" => Ok(Self::Sha2_256),
"sha2-384" | "sha2_384" | "sha384" => Ok(Self::Sha2_384),
"sha2-512" | "sha2_512" | "sha512" => Ok(Self::Sha2_512),
"sha3-224" | "sha3_224" => Ok(Self::Sha3_224),
"sha3-256" | "sha3_256" => Ok(Self::Sha3_256),
"sha3-384" | "sha3_384" => Ok(Self::Sha3_384),
"sha3-512" | "sha3_512" => Ok(Self::Sha3_512),
"blake3" => Ok(Self::Blake3),
_ => Err(LuaError::FromLuaConversionError {
from: "string",
to: "HashAlgorithm",
message: Some(format!(
"Invalid hashing algorithm '{str}', valid kinds are:\n{}",
HashAlgorithm::ALL
.into_iter()
.map(HashAlgorithm::name)
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "HashAlgorithm",
message: None,
})
}
}
}
impl<'lua> FromLuaMulti<'lua> for HashOptions {
fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let algorithm = values
.pop_front()
.map(|value| HashAlgorithm::from_lua(value, lua))
.transpose()?
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "HashAlgorithm",
message: Some("Argument #1 missing or nil".to_string()),
})?;
let message = values
.pop_front()
.map(|value| BString::from_lua(value, lua))
.transpose()?
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "string or buffer",
message: Some("Argument #2 missing or nil".to_string()),
})?;
let secret = values
.pop_front()
.map(|value| BString::from_lua(value, lua))
.transpose()?;
// let seed = values
// .pop_front()
// .map(|value| BString::from_lua(value, lua))
// .transpose()?;
Ok(HashOptions {
algorithm,
message,
secret,
// seed,
})
}
}

View file

@ -1,69 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use bstr::BString;
use mlua::prelude::*;
use lune_utils::TableBuilder;
mod compress_decompress;
mod encode_decode;
mod hash;
pub use self::compress_decompress::{compress, decompress, CompressDecompressFormat};
pub use self::encode_decode::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
pub use self::hash::HashOptions;
/**
Creates the `serde` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("encode", serde_encode)?
.with_function("decode", serde_decode)?
.with_async_function("compress", serde_compress)?
.with_async_function("decompress", serde_decompress)?
.with_function("hash", hash_message)?
.with_function("hmac", hmac_message)?
.build_readonly()
}
fn serde_encode<'lua>(
lua: &'lua Lua,
(format, value, pretty): (EncodeDecodeFormat, LuaValue<'lua>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default()));
encode(value, lua, config)
}
fn serde_decode(lua: &Lua, (format, bs): (EncodeDecodeFormat, BString)) -> LuaResult<LuaValue> {
let config = EncodeDecodeConfig::from(format);
decode(bs, lua, config)
}
async fn serde_compress(
lua: &Lua,
(format, bs, level): (CompressDecompressFormat, BString, Option<i32>),
) -> LuaResult<LuaString> {
let bytes = compress(bs, format, level).await?;
lua.create_string(bytes)
}
async fn serde_decompress(
lua: &Lua,
(format, bs): (CompressDecompressFormat, BString),
) -> LuaResult<LuaString> {
let bytes = decompress(bs, format).await?;
lua.create_string(bytes)
}
fn hash_message(lua: &Lua, options: HashOptions) -> LuaResult<LuaString> {
lua.create_string(options.hash())
}
fn hmac_message(lua: &Lua, options: HashOptions) -> LuaResult<LuaString> {
lua.create_string(options.hmac()?)
}

View file

@ -1,25 +0,0 @@
[package]
name = "lune-std-stdio"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Stdio"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
dialoguer = "0.11"
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
tokio = { version = "1", default-features = false, features = [
"io-std",
"io-util",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,85 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use lune_utils::fmt::{pretty_format_multi_value, ValueFormatConfig};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use tokio::io::{stderr, stdin, stdout, AsyncReadExt, AsyncWriteExt};
use lune_utils::TableBuilder;
mod prompt;
mod style_and_color;
use self::prompt::{prompt, PromptOptions, PromptResult};
use self::style_and_color::{ColorKind, StyleKind};
const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new()
.with_max_depth(4)
.with_colors_enabled(false);
/**
Creates the `stdio` standard library module.
# Errors
Errors when out of memory.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("color", stdio_color)?
.with_function("style", stdio_style)?
.with_function("format", stdio_format)?
.with_async_function("write", stdio_write)?
.with_async_function("ewrite", stdio_ewrite)?
.with_async_function("readToEnd", stdio_read_to_end)?
.with_async_function("prompt", stdio_prompt)?
.build_readonly()
}
fn stdio_color(lua: &Lua, color: ColorKind) -> LuaResult<LuaValue> {
color.ansi_escape_sequence().into_lua(lua)
}
fn stdio_style(lua: &Lua, style: StyleKind) -> LuaResult<LuaValue> {
style.ansi_escape_sequence().into_lua(lua)
}
fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult<String> {
Ok(pretty_format_multi_value(&args, &FORMAT_CONFIG))
}
async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
let mut stdout = stdout();
stdout.write_all(s.as_bytes()).await?;
stdout.flush().await?;
Ok(())
}
async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
let mut stderr = stderr();
stderr.write_all(s.as_bytes()).await?;
stderr.flush().await?;
Ok(())
}
/*
FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader.
This is a bit tricky since we would want to be able to use **both** readLine and readToEnd
in the same script, doing something like readLine, readLine, readToEnd from lua, and
having that capture the first two lines and then read the rest of the input.
*/
async fn stdio_read_to_end(lua: &Lua, (): ()) -> LuaResult<LuaString> {
let mut input = Vec::new();
let mut stdin = stdin();
stdin.read_to_end(&mut input).await?;
lua.create_string(&input)
}
async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult<PromptResult> {
lua.spawn_blocking(move || prompt(options))
.await
.into_lua_err()
}

View file

@ -1,195 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
const ESCAPE_SEQ_RESET: &str = "\x1b[0m";
/**
A color kind supported by the `stdio` standard library.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorKind {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
impl ColorKind {
pub const ALL: [Self; 9] = [
Self::Reset,
Self::Black,
Self::Red,
Self::Green,
Self::Yellow,
Self::Blue,
Self::Magenta,
Self::Cyan,
Self::White,
];
/**
Returns the human-friendly name of this color kind.
*/
pub fn name(self) -> &'static str {
match self {
Self::Reset => "reset",
Self::Black => "black",
Self::Red => "red",
Self::Green => "green",
Self::Yellow => "yellow",
Self::Blue => "blue",
Self::Magenta => "magenta",
Self::Cyan => "cyan",
Self::White => "white",
}
}
/**
Returns the ANSI escape sequence for the color kind.
*/
pub fn ansi_escape_sequence(self) -> &'static str {
match self {
Self::Reset => ESCAPE_SEQ_RESET,
Self::Black => "\x1b[30m",
Self::Red => "\x1b[31m",
Self::Green => "\x1b[32m",
Self::Yellow => "\x1b[33m",
Self::Blue => "\x1b[34m",
Self::Magenta => "\x1b[35m",
Self::Cyan => "\x1b[36m",
Self::White => "\x1b[37m",
}
}
}
impl FromStr for ColorKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.trim().to_ascii_lowercase().as_str() {
"reset" => Self::Reset,
"black" => Self::Black,
"red" => Self::Red,
"green" => Self::Green,
"yellow" => Self::Yellow,
"blue" => Self::Blue,
// NOTE: Previous versions of Lune had this color as "purple" instead
// of "magenta", so we keep this here for backwards compatibility.
"magenta" | "purple" => Self::Magenta,
"cyan" => Self::Cyan,
"white" => Self::White,
_ => return Err(()),
})
}
}
impl FromLua<'_> for ColorKind {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = value {
let s = s.to_str()?;
match s.parse() {
Ok(color) => Ok(color),
Err(()) => Err(LuaError::FromLuaConversionError {
from: "string",
to: "ColorKind",
message: Some(format!(
"Invalid color kind '{s}'\nValid kinds are: {}",
Self::ALL
.iter()
.map(|kind| kind.name())
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ColorKind",
message: None,
})
}
}
}
/**
A style kind supported by the `stdio` standard library.
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StyleKind {
Reset,
Bold,
Dim,
}
impl StyleKind {
pub const ALL: [Self; 3] = [Self::Reset, Self::Bold, Self::Dim];
/**
Returns the human-friendly name for this style kind.
*/
pub fn name(self) -> &'static str {
match self {
Self::Reset => "reset",
Self::Bold => "bold",
Self::Dim => "dim",
}
}
/**
Returns the ANSI escape sequence for this style kind.
*/
pub fn ansi_escape_sequence(self) -> &'static str {
match self {
Self::Reset => ESCAPE_SEQ_RESET,
Self::Bold => "\x1b[1m",
Self::Dim => "\x1b[2m",
}
}
}
impl FromStr for StyleKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.trim().to_ascii_lowercase().as_str() {
"reset" => Self::Reset,
"bold" => Self::Bold,
"dim" => Self::Dim,
_ => return Err(()),
})
}
}
impl FromLua<'_> for StyleKind {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = value {
let s = s.to_str()?;
match s.parse() {
Ok(style) => Ok(style),
Err(()) => Err(LuaError::FromLuaConversionError {
from: "string",
to: "StyleKind",
message: Some(format!(
"Invalid style kind '{s}'\nValid kinds are: {}",
Self::ALL
.iter()
.map(|kind| kind.name())
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "StyleKind",
message: None,
})
}
}
}

View file

@ -1,21 +0,0 @@
[package]
name = "lune-std-task"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library - Task"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
tokio = { version = "1", default-features = false, features = ["time"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,60 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
use std::time::Duration;
use mlua::prelude::*;
use mlua_luau_scheduler::Functions;
use tokio::time::{sleep, Instant};
use lune_utils::TableBuilder;
/**
Creates the `task` standard library module.
# Errors
Errors when out of memory, or if default Lua globals are missing.
*/
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
let fns = Functions::new(lua)?;
// Create wait & delay functions
let task_wait = lua.create_async_function(wait)?;
let task_delay_env = TableBuilder::new(lua)?
.with_value("select", lua.globals().get::<_, LuaFunction>("select")?)?
.with_value("spawn", fns.spawn.clone())?
.with_value("defer", fns.defer.clone())?
.with_value("wait", task_wait.clone())?
.build_readonly()?;
let task_delay = lua
.load(DELAY_IMPL_LUA)
.set_name("task.delay")
.set_environment(task_delay_env)
.into_function()?;
TableBuilder::new(lua)?
.with_value("cancel", fns.cancel)?
.with_value("defer", fns.defer)?
.with_value("delay", task_delay)?
.with_value("spawn", fns.spawn)?
.with_value("wait", task_wait)?
.build_readonly()
}
const DELAY_IMPL_LUA: &str = r"
return defer(function(...)
wait(select(1, ...))
spawn(select(2, ...))
end, ...)
";
async fn wait(_: &Lua, secs: Option<f64>) -> LuaResult<f64> {
let duration = Duration::from_secs_f64(secs.unwrap_or_default());
let before = Instant::now();
sleep(duration).await;
let after = Instant::now();
Ok((after - before).as_secs_f64())
}

View file

@ -1,59 +0,0 @@
[package]
name = "lune-std"
version = "0.1.5"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Lune standard library"
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[features]
default = [
"datetime",
"fs",
"luau",
"net",
"process",
"regex",
"roblox",
"serde",
"stdio",
"task",
]
datetime = ["dep:lune-std-datetime"]
fs = ["dep:lune-std-fs"]
luau = ["dep:lune-std-luau"]
net = ["dep:lune-std-net"]
process = ["dep:lune-std-process"]
regex = ["dep:lune-std-regex"]
roblox = ["dep:lune-std-roblox"]
serde = ["dep:lune-std-serde"]
stdio = ["dep:lune-std-stdio"]
task = ["dep:lune-std-task"]
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", default-features = false, features = ["fs", "sync"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-datetime = { optional = true, version = "0.1.3", path = "../lune-std-datetime" }
lune-std-fs = { optional = true, version = "0.1.2", path = "../lune-std-fs" }
lune-std-luau = { optional = true, version = "0.1.2", path = "../lune-std-luau" }
lune-std-net = { optional = true, version = "0.1.2", path = "../lune-std-net" }
lune-std-process = { optional = true, version = "0.1.3", path = "../lune-std-process" }
lune-std-regex = { optional = true, version = "0.1.2", path = "../lune-std-regex" }
lune-std-roblox = { optional = true, version = "0.1.4", path = "../lune-std-roblox" }
lune-std-serde = { optional = true, version = "0.1.2", path = "../lune-std-serde" }
lune-std-stdio = { optional = true, version = "0.1.2", path = "../lune-std-stdio" }
lune-std-task = { optional = true, version = "0.1.2", path = "../lune-std-task" }

View file

@ -1,92 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
/**
A standard global provided by Lune.
*/
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum LuneStandardGlobal {
GTable,
Print,
Require,
Version,
Warn,
}
impl LuneStandardGlobal {
/**
All available standard globals.
*/
pub const ALL: &'static [Self] = &[
Self::GTable,
Self::Print,
Self::Require,
Self::Version,
Self::Warn,
];
/**
Gets the name of the global, such as `_G` or `require`.
*/
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::GTable => "_G",
Self::Print => "print",
Self::Require => "require",
Self::Version => "_VERSION",
Self::Warn => "warn",
}
}
/**
Creates the Lua value for the global.
# Errors
If the global could not be created.
*/
#[rustfmt::skip]
#[allow(unreachable_patterns)]
pub fn create<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
let res = match self {
Self::GTable => crate::globals::g_table::create(lua),
Self::Print => crate::globals::print::create(lua),
Self::Require => crate::globals::require::create(lua),
Self::Version => crate::globals::version::create(lua),
Self::Warn => crate::globals::warn::create(lua),
};
match res {
Ok(v) => Ok(v),
Err(e) => Err(e.context(format!(
"Failed to create standard global '{}'",
self.name()
))),
}
}
}
impl FromStr for LuneStandardGlobal {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let low = s.trim().to_ascii_lowercase();
Ok(match low.as_str() {
"_g" => Self::GTable,
"print" => Self::Print,
"require" => Self::Require,
"_version" => Self::Version,
"warn" => Self::Warn,
_ => {
return Err(format!(
"Unknown standard global '{low}'\nValid globals are: {}",
Self::ALL
.iter()
.map(Self::name)
.collect::<Vec<_>>()
.join(", ")
))
}
})
}
}

View file

@ -1,5 +0,0 @@
use mlua::prelude::*;
pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
lua.create_table()?.into_lua(lua)
}

View file

@ -1,5 +0,0 @@
pub mod g_table;
pub mod print;
pub mod require;
pub mod version;
pub mod warn;

View file

@ -1,19 +0,0 @@
use std::io::Write;
use lune_utils::fmt::{pretty_format_multi_value, ValueFormatConfig};
use mlua::prelude::*;
const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new()
.with_max_depth(4)
.with_colors_enabled(true);
pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
let f = lua.create_function(|_, args: LuaMultiValue| {
let formatted = format!("{}\n", pretty_format_multi_value(&args, &FORMAT_CONFIG));
let mut stdout = std::io::stdout();
stdout.write_all(formatted.as_bytes())?;
stdout.flush()?;
Ok(())
})?;
f.into_lua(lua)
}

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