feat: revamped AUR PKGBUILDs

This commit is contained in:
Erica Marigold 2023-07-28 12:51:00 +05:30
parent 62ed729e15
commit f9ee37c83a
No known key found for this signature in database
GPG key ID: 23CD97ABBBCC5ED2
276 changed files with 151 additions and 24707 deletions

View file

@ -1,16 +0,0 @@
# Statically link the vcruntime
# https://users.rust-lang.org/t/static-vcruntime-distribute-windows-msvc-binaries-without-needing-to-deploy-vcruntime-dll/57599
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = [
"-C",
"link-args=/DEFAULTLIB:ucrt.lib /DEFAULTLIB:libvcruntime.lib libcmt.lib",
"-C",
"link-args=/NODEFAULTLIB:libvcruntimed.lib /NODEFAULTLIB:vcruntime.lib /NODEFAULTLIB:vcruntimed.lib",
"-C",
"link-args=/NODEFAULTLIB:libcmtd.lib /NODEFAULTLIB:msvcrt.lib /NODEFAULTLIB:msvcrtd.lib",
"-C",
"link-args=/NODEFAULTLIB:libucrt.lib /NODEFAULTLIB:libucrtd.lib /NODEFAULTLIB:ucrtd.lib",
]
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

View file

@ -1,15 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.{json,jsonc,json5}]
indent_style = space
indent_size = 4
[*.{yml,yaml}]
indent_style = space
indent_size = 2

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "tests/roblox/rbx-test-files"]
path = tests/roblox/rbx-test-files
url = https://github.com/rojo-rbx/rbx-test-files

11
.luaurc
View file

@ -1,11 +0,0 @@
{
"languageMode": "strict",
"lint": {
"*": true
},
"lintErrors": false,
"typeErrors": true,
"globals": [
"warn"
]
}

View file

@ -1,63 +0,0 @@
--> A utility script that prints out a CSV
--> file in a prettified format to stdout
local LINE_SEPARATOR = "\n"
local COMMA_SEPARATOR = ","
local fs = require("@lune/fs")
local process = require("@lune/process")
local path = process.args[1] or ".lune/data/test.csv"
assert(path ~= nil and #path > 0, "No input file path was given")
assert(not fs.isDir(path), "Input file path was a dir, not a file")
assert(fs.isFile(path), "Input file path does not exist")
-- Read all the lines of the wanted file, and then split
-- out the raw lines containing comma-separated values
local csvTable = {}
for index, rawLine in string.split(fs.readFile(path), LINE_SEPARATOR) do
if #rawLine > 0 then
csvTable[index] = string.split(rawLine, COMMA_SEPARATOR)
end
end
-- Gather the maximum widths of strings
-- for alignment & spacing in advance
local maxWidths = {}
for _, row in csvTable do
for index, value in row do
maxWidths[index] = math.max(maxWidths[index] or 0, #value)
end
end
local totalWidth = 0
local totalColumns = 0
for _, width in maxWidths do
totalWidth += width
totalColumns += 1
end
-- We have everything we need, print it out with
-- the help of some unicode box drawing characters
local thiccLine = string.rep("", totalWidth + totalColumns * 3 - 1)
print(string.format("┏%s┓", thiccLine))
for rowIndex, row in csvTable do
local paddedValues = {}
for valueIndex, value in row do
local spacing = string.rep(" ", maxWidths[valueIndex] - #value)
table.insert(paddedValues, value .. spacing)
end
print(string.format("┃ %s ┃", table.concat(paddedValues, "")))
-- The first line is the header, we should
-- print out an extra separator below it
if rowIndex == 1 then
print(string.format("┣%s┫", thiccLine))
end
end
print(string.format("┗%s┛", thiccLine))

View file

@ -1,4 +0,0 @@
Header1,Header2,Header3
Hello,World,!
1,2,3
Foo,Bar,Baz
1 Header1 Header2 Header3
2 Hello World !
3 1 2 3
4 Foo Bar Baz

View file

@ -1,231 +0,0 @@
local fs = require("@lune/fs")
local net = require("@lune/net")
local process = require("@lune/process")
local stdio = require("@lune/stdio")
local task = require("@lune/task")
--[[
EXAMPLE #1
Using arguments given to the program
]]
if #process.args > 0 then
print("Got arguments:")
print(process.args)
if #process.args > 3 then
error("Too many arguments!")
end
else
print("Got no arguments ☹️")
end
--[[
EXAMPLE #2
Using the stdio library to prompt for terminal input
]]
local text = stdio.prompt("text", "Please write some text")
print("You wrote '" .. text .. "'!")
local confirmed = stdio.prompt("confirm", "Please confirm that you wrote some text")
if confirmed == false then
error("You didn't confirm!")
else
print("Confirmed!")
end
--[[
EXAMPLE #3
Get & set environment variables
Checks if environment variables are empty or not,
prints out if empty and if they have a value
]]
print("Reading current environment 🔎")
-- Environment variables can be read directly
assert(process.env.PATH ~= nil, "Missing PATH")
assert(process.env.PWD ~= nil, "Missing PWD")
-- And they can also be accessed using Luau's generalized iteration (but not pairs())
for key, value in process.env do
local box = if value and value ~= "" then "" else ""
print(string.format("[%s] %s", box, key))
end
--[[
EXAMPLE #4
Spawning concurrent tasks
These tasks will run at the same time as other Lua code which lets you do primitive multitasking
]]
task.spawn(function()
print("Spawned a task that will run instantly but not block")
task.wait(5)
end)
print("Spawning a delayed task that will run in 5 seconds")
task.delay(5, function()
print("...")
task.wait(1)
print("Hello again!")
task.wait(1)
print("Goodbye again! 🌙")
end)
--[[
EXAMPLE #5
Read files in the current directory
This prints out directory & file names with some fancy icons
]]
print("Reading current dir 🗂️")
local entries = fs.readDir(".")
-- NOTE: We have to do this outside of the sort function
-- to avoid yielding across the metamethod boundary, all
-- of the filesystem APIs are asynchronous and yielding
local entryIsDir = {}
for _, entry in entries do
entryIsDir[entry] = fs.isDir(entry)
end
-- Sort prioritizing directories first, then alphabetically
table.sort(entries, function(entry0, entry1)
if entryIsDir[entry0] ~= entryIsDir[entry1] then
return entryIsDir[entry0]
end
return entry0 < entry1
end)
-- Make sure we got some known files that should always exist
assert(table.find(entries, "Cargo.toml") ~= nil, "Missing Cargo.toml")
assert(table.find(entries, "Cargo.lock") ~= nil, "Missing Cargo.lock")
-- Print the pretty stuff
for _, entry in entries do
if fs.isDir(entry) then
print("📁 " .. entry)
else
print("📄 " .. entry)
end
end
--[[
EXAMPLE #6
Call out to another program / executable
You can also get creative and combine this with example #6 to spawn several programs at the same time!
]]
print("Sending 4 pings to google 🌏")
local result = process.spawn("ping", {
"google.com",
"-c 4",
})
--[[
EXAMPLE #7
Using the result of a spawned process, exiting the process
This looks scary with lots of weird symbols, but, it's just some Lua-style pattern matching
to parse the lines of "min/avg/max/stddev = W/X/Y/Z ms" that the ping program outputs to us
]]
if result.ok then
assert(#result.stdout > 0, "Result output was empty")
local min, avg, max, stddev = string.match(
result.stdout,
"min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms"
)
print(string.format("Minimum ping time: %.3fms", assert(tonumber(min))))
print(string.format("Maximum ping time: %.3fms", assert(tonumber(max))))
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
print(string.format("Standard deviation: %.3fms", assert(tonumber(stddev))))
else
print("Failed to send ping to google!")
print(result.stderr)
process.exit(result.code)
end
--[[
EXAMPLE #8
Using the built-in networking library, encoding & decoding json
]]
print("Sending PATCH request to web API 📤")
local apiResult = net.request({
url = "https://jsonplaceholder.typicode.com/posts/1",
method = "PATCH",
headers = {
["Content-Type"] = "application/json",
},
body = net.jsonEncode({
title = "foo",
body = "bar",
}),
})
if not apiResult.ok then
print("Failed to send network request!")
print(string.format("%d (%s)", apiResult.statusCode, apiResult.statusMessage))
print(apiResult.body)
process.exit(1)
end
type ApiResponse = {
id: number,
title: string,
body: string,
userId: number,
}
local apiResponse: ApiResponse = net.jsonDecode(apiResult.body)
assert(apiResponse.title == "foo", "Invalid json response")
assert(apiResponse.body == "bar", "Invalid json response")
print("Got valid JSON response with changes applied")
--[[
EXAMPLE #9
Using the stdio library to print pretty
]]
print("Printing with pretty colors and auto-formatting 🎨")
print(stdio.color("blue") .. string.rep("", 22) .. stdio.color("reset"))
print("API response:", apiResponse)
warn({
Oh = {
No = {
TooMuch = {
Nesting = {
"Will not print",
},
},
},
},
})
print(stdio.color("blue") .. string.rep("", 22) .. stdio.color("reset"))
--[[
EXAMPLE #10
Saying goodbye 😔
]]
print("Goodbye, lune! 🌙")

View file

@ -1,53 +0,0 @@
--> A basic http server that echoes the given request
--> body at /ping and otherwise responds 404 "Not Found"
local net = require("@lune/net")
local process = require("@lune/process")
local task = require("@lune/task")
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
else 8080
-- Create our responder functions
local function pong(request: net.ServeRequest): string
return `Pong!\n{request.path}\n{request.body}`
end
local function teapot(_request: net.ServeRequest): net.ServeResponse
return {
status = 418,
body = "🫖",
}
end
local function notFound(_request: net.ServeRequest): net.ServeResponse
return {
status = 404,
body = "Not Found",
}
end
-- Run the server on port 8080
local handle = net.serve(PORT, function(request)
if string.sub(request.path, 1, 5) == "/ping" then
return pong(request)
elseif string.sub(request.path, 1, 7) == "/teapot" then
return teapot(request)
else
return notFound(request)
end
end)
print(`Listening on port {PORT} 🚀`)
-- Exit our example after a small delay, if you copy this
-- example just remove this part to keep the server running
task.delay(2, function()
print("Shutting down...")
task.wait(1)
handle.stop()
end)

View file

@ -1,44 +0,0 @@
--> A basic web socket client that communicates with an echo server
local net = require("@lune/net")
local process = require("@lune/process")
local task = require("@lune/task")
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
else 8080
local URL = `ws://127.0.0.1:{PORT}`
-- Connect to our web socket server
local socket = net.socket(URL)
print("Connected to echo web socket server at '" .. URL .. "'")
print("Sending a message every second for 5 seconds...")
-- Force exit after 10 seconds in case the server is not responding well
local forceExit = task.delay(10, function()
warn("Example did not complete in time, exiting...")
process.exit(1)
end)
-- Send one message per second and time it
for _ = 1, 5 do
local start = os.clock()
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)
end
-- Everything went well, and we are done with the socket, so we can close it
print("Closing web socket...")
socket.close()
task.cancel(forceExit)
print("Done! 🌙")

View file

@ -1,37 +0,0 @@
--> A basic web socket server that echoes given messages
local net = require("@lune/net")
local process = require("@lune/process")
local task = require("@lune/task")
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
else 8080
-- Run the server on port 8080, if we get a normal http request on
-- the port this will respond with 426 Upgrade Required by default
local handle = net.serve(PORT, {
handleWebSocket = function(socket)
print("Got new web socket connection!")
repeat
local message = socket.next()
if message ~= nil then
socket.send("Echo - " .. message)
end
until message == nil
print("Web socket disconnected.")
end,
})
print(`Listening on port {PORT} 🚀`)
-- Exit our example after a small delay, if you copy this
-- example just remove this part to keep the server running
task.delay(10, function()
print("Shutting down...")
task.wait(1)
handle.stop()
task.wait(1)
end)

View file

@ -1,8 +0,0 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"esbenp.prettier-vscode",
"JohnnyMorganz.stylua",
"DavidAnson.vscode-markdownlint"
]
}

31
.vscode/settings.json vendored
View file

@ -1,31 +0,0 @@
{
// 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/": "~/.lune/.typedefs/0.7.4/"
},
// 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,
"[luau][lua]": {
"editor.defaultFormatter": "JohnnyMorganz.stylua"
},
"[json][jsonc][markdown][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}

View file

@ -1,840 +0,0 @@
<!-- markdownlint-disable MD023 -->
<!-- markdownlint-disable MD033 -->
# Changelog
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.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/filiptibell/lune/pull/62
[#68]: https://github.com/filiptibell/lune/pull/66
[#72]: https://github.com/filiptibell/lune/pull/72
## `0.7.4` - July 7th, 2023
### 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/filiptibell/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/filiptibell/lune/pull/57
## `0.7.0` - June 12th, 2023
### Breaking Changes
- Globals for the `fs`, `net`, `process`, `stdio`, and `task` builtins have been removed, and the `require("@lune/...")` syntax is now the only way to access builtin libraries. If you have previously been using a global such as `fs` directly, you will now need to put `local fs = require("@lune/fs")` at the top of the file instead.
- Migrated several functions in the `roblox` builtin to new, more flexible APIs:
- `readPlaceFile -> deserializePlace`
- `readModelFile -> deserializeModel`
- `writePlaceFile -> serializePlace`
- `writeModelFile -> serializeModel`
These new APIs **_no longer use file paths_**, meaning to use them with files you must first read them using the `fs` builtin.
- Removed `CollectionService` and its methods from the `roblox` builtin library - new instance methods have been added as replacements.
- Removed [`Instance:FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant) which was a method that was never enabled in the official Roblox API and will soon be removed. <br/>
Use the second argument of the already existing find methods instead to find descendants.
- Removed the global `printinfo` function - it was generally not used, and did not work as intended. Use the `stdio` builtin for formatting and logging instead.
- Removed support for Windows on ARM - it's more trouble than its worth right now, we may revisit it later.
### Added
- Added `serde.compress` and `serde.decompress` for compressing and decompressing strings using one of several compression formats: `brotli`, `gzip`, `lz4`, or `zlib`.
Example usage:
```lua
local INPUT = string.rep("Input string to compress", 16) -- Repeated string 16 times for the purposes of this example
local serde = require("@lune/serde")
local compressed = serde.compress("gzip", INPUT)
local decompressed = serde.decompress("gzip", compressed)
assert(decompressed == INPUT)
```
- Added automatic decompression for compressed responses when using `net.request`.
This behavior can be disabled by passing `options = { decompress = false }` in request params.
- Added support for finding scripts in the current home directory.
This means that if you have a script called `script-name.luau`, you can place it in the following location:
- `C:\Users\YourName\.lune\script-name.luau` (Windows)
- `/Users/YourName/.lune/script-name.luau` (macOS)
- `/home/YourName/.lune/script-name.luau` (Linux)
And then run it using `lune script-name` from any directory you are currently in.
- Added several new instance methods in the `roblox` builtin library:
- [`Instance:AddTag`](https://create.roblox.com/docs/reference/engine/classes/Instance#AddTag)
- [`Instance:GetTags`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetTags)
- [`Instance:HasTag`](https://create.roblox.com/docs/reference/engine/classes/Instance#HasTag)
- [`Instance:RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/Instance#RemoveTag)
- Implemented the second argument of the `FindFirstChild` / `FindFirstChildOfClass` / `FindFirstChildWhichIsA` instance methods.
### Changed
- Update to Luau version `0.579`
- Both `stdio.write` and `stdio.ewrite` now support writing arbitrary bytes, instead of only valid UTF-8.
### Fixed
- Fixed `stdio.write` and `stdio.ewrite` not being flushed and causing output to be interleaved. ([#47])
- Fixed `typeof` returning `userdata` for roblox types such as `Instance`, `Vector3`, ...
[#47]: https://github.com/filiptibell/lune/pull/47
## `0.6.7` - May 14th, 2023
### Added
- Replaced all of the separate typedef & documentation generation commands with a unified `lune --setup` command.
This command will generate type definition files for all of the builtins and will work with the new `require("@lune/...")` syntax. Note that this also means that there is no longer any way to generate type definitions for globals - this is because they will be removed in the next major release in favor of the beforementioned syntax.
- New releases now include prebuilt binaries for arm64 / aarch64! <br />
These new binaries will have names with the following format:
- `lune-windows-0.6.7-aarch64.exe`
- `lune-linux-0.6.7-aarch64`
- `lune-macos-0.6.7-aarch64`
- Added global types to documentation site
## `0.6.6` - April 30th, 2023
### Added
- Added tracing / logging for rare and hard to diagnose error cases, which can be configured using the env var `RUST_LOG`.
### Changed
- The `_VERSION` global now follows a consistent format `Lune x.y.z+luau` to allow libraries to check against it for version requirements.
Examples:
- `Lune 0.0.0+0`
- `Lune 1.0.0+500`
- `Lune 0.11.22+9999`
- Updated to Luau version `0.573`
- Updated `rbx-dom` to support reading and writing `Font` datatypes
### Fixed
- Fixed `_G` not being a readable & writable table
- Fixed `_G` containing normal globals such as `print`, `math`, ...
- Fixed using instances as keys in tables
## `0.6.5` - March 27th, 2023
### Changed
- Functions such as `print`, `warn`, ... now respect `__tostring` metamethods.
### Fixed
- Fixed access of roblox instance properties such as `Workspace.Terrain`, `game.Workspace` that are actually links to child instances. <br />
These properties are always guaranteed to exist, and they are not always properly set, meaning they must be found through an internal lookup.
- Fixed issues with the `CFrame.lookAt` and `CFrame.new(Vector3, Vector3)` constructors.
- Fixed issues with CFrame math operations returning rotation angles in the wrong order.
## `0.6.4` - March 26th, 2023
### Fixed
- Fixed instances with attributes not saving if they contain integer attributes.
- Fixed attributes not being set properly if the instance has an empty attributes property.
- Fixed error messages for reading & writing roblox files not containing the full error message.
- Fixed crash when trying to access an instance reference property that points to a destroyed instance.
- Fixed crash when trying to save instances that contain unsupported attribute types.
## `0.6.3` - March 26th, 2023
### 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).
### Fixed
- Fixed accessing a destroyed instance printing an error message even if placed inside a pcall.
- Fixed cloned instances not having correct instance reference properties set (`ObjectValue.Value`, `Motor6D.Part0`, ...)
- Fixed `Instance::GetDescendants` returning the same thing as `Instance::GetChildren`.
## `0.6.2` - March 25th, 2023
This release adds some new features and fixes for the `roblox` built-in.
### Added
- Added `GetAttribute`, `GetAttributes` and `SetAttribute` methods for instances.
- Added support for getting & setting properties that are instance references.
### Changed
- Improved handling of optional property types such as optional cframes & default physical properties.
### Fixed
- Fixed handling of instance properties that are serialized as binary strings.
## `0.6.1` - March 22nd, 2023
### Fixed
- Fixed `writePlaceFile` and `writeModelFile` in the new `roblox` built-in making mysterious "ROOT" instances.
## `0.6.0` - March 22nd, 2023
### Added
- 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!
- Added a `serde` built-in
This built-in contains previously available functions `encode` and `decode` from the `net` global. <br />
The plan is for this built-in to contain more serialization and encoding functionality in the future.
- `require` has been reimplemented and overhauled in several ways:
- New built-ins such as `roblox` and `serde` can **_only_** be imported using `require("@lune/roblox")`, `require("@lune/serde")`, ...
- Previous globals such as `fs`, `net` and others can now _also_ be imported using `require("@lune/fs")`, `require("@lune/net")`, ...
- Requiring a script is now completely asynchronous and will not block lua threads other than the caller.
- Requiring a script will no longer error when using async APIs in the main body of the required script.
All new built-ins will be added using this syntax and new built-ins will no longer be available in the global scope, and current globals will stay available as globals until proper editor and LSP support is available to ensure Lune users have a good development experience. This is the first step towards moving away from adding each library as a global, and allowing Lune to have more built-in libraries in general.
Behavior otherwise stays the same, and requires are still relative to file unless the special `@` prefix is used.
- Added `net.urlEncode` and `net.urlDecode` for URL-encoding and decoding strings
### Changed
- Renamed the global `info` function to `printinfo` to make it less ambiguous
### Removed
- Removed experimental `net.encode` and `net.decode` functions, since they are now available using `require("@lune/serde")`
- Removed option to preserve default Luau require behavior
## `0.5.6` - March 11th, 2023
### Added
- Added support for shebangs at the top of a script, meaning scripts such as this one will now run without throwing a syntax error:
```lua
#!/usr/bin/env lune
print("Hello, world!")
```
### Fixed
- Fixed `fs.writeFile` and `fs.readFile` not working with strings / files that are invalid utf-8
## `0.5.5` - March 8th, 2023
### Added
- Added support for running scripts by passing absolute file paths in the CLI
- This does not have the restriction of scripts having to use the `.luau` or `.lua` extension, since it is presumed that if you pass an absolute path you know exactly what you are doing
### Changed
- Improved error messages for passing invalid file names / file paths substantially - they now include helpful formatting to make file names distinct from file extensions, and give suggestions on how to solve the problem
- Improved general formatting of error messages, both in the CLI and for Luau scripts being run
### Fixed
- Fixed the CLI being a bit too picky about file names when trying to run files in `lune` or `.lune` directories
- Fixed documentation misses from large changes made in version `0.5.0`
## `0.5.4` - March 7th, 2023
### Added
- Added support for reading scripts from stdin by passing `"-"` as the script name
- Added support for close codes in the `net` WebSocket APIs:
- A close code can be sent by passing it to `socket.close`
- A received close code can be checked with the `socket.closeCode` value, which is populated after a socket has been closed - note that using `socket.close` will not set the close code value, it is only set when received and is guaranteed to exist after closure
### Changed
- Update to Luau version 0.566
### Fixed
- Fixed scripts having to be valid utf8, they may now use any kind of encoding that base Luau supports
- The `net` WebSocket APIs will no longer return `nil` for partial messages being received in `socket.next`, and will instead wait for the full message to arrive
## `0.5.3` - February 26th, 2023
### Fixed
- Fixed `lune --generate-selene-types` generating an invalid Selene definitions file
- Fixed type definition parsing issues on Windows
## `0.5.2` - February 26th, 2023
### Fixed
- Fixed crash when using `stdio.color()` or `stdio.style()` in a CI environment or non-interactive terminal
## `0.5.1` - February 25th, 2023
### Added
- Added `net.encode` and `net.decode` which are equivalent to `net.jsonEncode` and `net.jsonDecode`, but with support for more formats.
**_WARNING: Unstable API_**
_This API is unstable and may change or be removed in the next major version of Lune. The purpose of making a new release with these functions is to gather feedback from the community, and potentially replace the JSON-specific encoding and decoding utilities._
Example usage:
```lua
local toml = net.decode("toml", [[
[package]
name = "my-cool-toml-package"
version = "0.1.0"
[values]
epic = true
]])
assert(toml.package.name == "my-cool-toml-package")
assert(toml.package.version == "0.1.0")
assert(toml.values.epic == true)
```
### Fixed
- Fixed indentation of closing curly bracket when printing tables
## `0.5.0` - February 23rd, 2023
### Added
- Added auto-generated API reference pages and documentation using GitHub wiki pages
- Added support for `query` in `net.request` parameters, which enables usage of query parameters in URLs without having to manually URL encode values.
- Added a new function `fs.move` to move / rename a file or directory from one path to another.
- Implemented a new task scheduler which resolves several long-standing issues:
- Issues with yielding across the C-call/metamethod boundary no longer occur when calling certain async APIs that Lune provides.
- Ordering of interleaved calls to `task.spawn/task.defer` is now completely deterministic, deferring is now guaranteed to run last even in these cases.
- The minimum wait time possible when using `task.wait` and minimum delay time using `task.delay` are now much smaller, and only limited by the underlying OS implementation. For most systems this means `task.wait` and `task.delay` are now accurate down to about 5 milliseconds or less.
### Changed
- Type definitions are now bundled as part of the Lune executable, meaning they no longer need to be downloaded.
- `lune --generate-selene-types` will generate the Selene type definitions file, replacing `lune --download-selene-types`
- `lune --generate-luau-types` will generate the Luau type definitions file, replacing `lune --download-luau-types`
- Improved accuracy of Selene type definitions, strongly typed arrays are now used where possible
- Improved error handling and messages for `net.serve`
- Improved error handling and messages for `stdio.prompt`
- File path representations on Windows now use legacy paths instead of UNC paths wherever possible, preventing some confusing cases where file paths don't work as expected
### Fixed
- Fixed `process.cwd` not having the correct ending path separator on Windows
- Fixed remaining edge cases where the `task` and `coroutine` libraries weren't interoperable
- Fixed `task.delay` keeping the script running even if it was cancelled using `task.cancel`
- Fixed `stdio.prompt` blocking all other lua threads while prompting for input
## `0.4.0` - February 11th, 2023
### Added
- ### Web Sockets
`net` now supports web sockets for both clients and servers! <br />
Note that the web socket object is identical on both client and
server, but how you retrieve a web socket object is different.
#### Server API
The server web socket API is an extension of the existing `net.serve` function. <br />
This allows for serving both normal HTTP requests and web socket requests on the same port.
Example usage:
```lua
net.serve(8080, {
handleRequest = function(request)
return "Hello, world!"
end,
handleWebSocket = function(socket)
task.delay(10, function()
socket.send("Timed out!")
socket.close()
end)
-- The message will be nil when the socket has closed
repeat
local messageFromClient = socket.next()
if messageFromClient == "Ping" then
socket.send("Pong")
end
until messageFromClient == nil
end,
})
```
#### Client API
Example usage:
```lua
local socket = net.socket("ws://localhost:8080")
socket.send("Ping")
task.delay(5, function()
socket.close()
end)
-- The message will be nil when the socket has closed
repeat
local messageFromServer = socket.next()
if messageFromServer == "Ping" then
socket.send("Pong")
end
until messageFromServer == nil
```
### Changed
- `net.serve` now returns a `NetServeHandle` which can be used to stop serving requests safely.
Example usage:
```lua
local handle = net.serve(8080, function()
return "Hello, world!"
end)
print("Shutting down after 1 second...")
task.wait(1)
handle.stop()
print("Shut down succesfully")
```
- The third and optional argument of `process.spawn` is now a global type `ProcessSpawnOptions`.
- Setting `cwd` in the options for `process.spawn` to a path starting with a tilde (`~`) will now use a path relative to the platform-specific home / user directory.
- `NetRequest` query parameters value has been changed to be a table of key-value pairs similar to `process.env`.
If any query parameter is specified more than once in the request url, the value chosen will be the last one that was specified.
- The internal http client for `net.request` now reuses headers and connections for more efficient requests.
- Refactored the Lune rust crate to be much more user-friendly and documented all of the public functions.
### Fixed
- Fixed `process.spawn` blocking all lua threads if the spawned child process yields.
## `0.3.0` - February 6th, 2023
### Added
- Added a new global `stdio` which replaces `console`
- Added `stdio.write` which writes a string directly to stdout, without any newlines
- Added `stdio.ewrite` which writes a string directly to stderr, without any newlines
- Added `stdio.prompt` which will prompt the user for different kinds of input
Example usage:
```lua
local text = stdio.prompt()
local text2 = stdio.prompt("text", "Please write some text")
local didConfirm = stdio.prompt("confirm", "Please confirm this action")
local optionIndex = stdio.prompt("select", "Please select an option", { "one", "two", "three" })
local optionIndices = stdio.prompt(
"multiselect",
"Please select one or more options",
{ "one", "two", "three", "four", "five" }
)
```
### Changed
- Migrated `console.setColor/resetColor` and `console.setStyle/resetStyle` to `stdio.color` and `stdio.style` to allow for more flexibility in custom printing using ANSI color codes. Check the documentation for new usage and behavior.
- Migrated the pretty-printing and formatting behavior of `console.log/info/warn/error` to the standard Luau printing functions.
### Removed
- Removed printing functions `console.log/info/warn/error` in favor of regular global functions for printing.
### Fixed
- Fixed scripts hanging indefinitely on error
## `0.2.2` - February 5th, 2023
### Added
- Added global types for networking & child process APIs
- `net.request` gets `NetFetchParams` and `NetFetchResponse` for its argument and return value
- `net.serve` gets `NetRequest` and `NetResponse` for the handler function argument and return value
- `process.spawn` gets `ProcessSpawnOptions` for its third and optional parameter
### Changed
- Reorganize repository structure to take advantage of cargo workspaces, improves compile times
## `0.2.1` - February 3rd, 2023
### Added
- Added support for string interpolation syntax (update to Luau 0.561)
- Added network server functionality using `net.serve`
Example usage:
```lua
net.serve(8080, function(request)
print(`Got a {request.method} request at {request.path}!`)
local data = net.jsonDecode(request.body)
-- For simple text responses with a 200 status
return "OK"
-- For anything else
return {
status = 203,
headers = { ["Content-Type"] = "application/json" },
body = net.jsonEncode({
message = "echo",
data = data,
})
}
end)
```
### Changed
- Improved type definitions file for Selene, now including constants like `process.env` + tags such as `readonly` and `mustuse` wherever applicable
### Fixed
- Fixed type definitions file for Selene not including all API members and parameters
- Fixed `process.exit` exiting at the first yield instead of exiting instantly as it should
## `0.2.0` - January 28th, 2023
### Added
- Added full documentation for all global APIs provided by Lune! This includes over 200 lines of pure documentation about behavior & error cases for all of the current 35 constants & functions. Check the [README](/README.md) to find out how to enable documentation in your editor.
- Added a third argument `options` for `process.spawn`:
- `cwd` - The current working directory for the process
- `env` - Extra environment variables to give to the process
- `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
- `stdio` - How to treat output and error streams from the child process - set to `"inherit"` to pass output and error streams to the current process
- Added `process.cwd`, the path to the current working directory in which the Lune script is running
## `0.1.3` - January 25th, 2023
### Added
- Added a `--list` subcommand to list scripts found in the `lune` or `.lune` directory.
## `0.1.2` - January 24th, 2023
### Added
- Added automatic publishing of the Lune library to [crates.io](https://crates.io/crates/lune)
### Fixed
- Fixed scripts that terminate instantly sometimes hanging
## `0.1.1` - January 24th, 2023
### Fixed
- Fixed errors containing `./` and / or `../` in the middle of file paths
- Potential fix for spawned processes that yield erroring with "attempt to yield across metamethod/c-call boundary"
## `0.1.0` - January 24th, 2023
### Added
- `task` now supports passing arguments in `task.spawn` / `task.delay` / `task.defer`
- `require` now uses paths relative to the file instead of being relative to the current directory, which is consistent with almost all other languages but not original Lua / Luau - this is a breaking change but will allow for proper packaging of third-party modules and more in the future.
- **_NOTE:_** _If you still want to use the default Lua behavior instead of relative paths, set the environment variable `LUAU_PWD_REQUIRE` to `true`_
### Changed
- Improved error message when an invalid file path is passed to `require`
- Much improved error formatting and stack traces
### Fixed
- Fixed downloading of type definitions making json files instead of the proper format
- Process termination will now always make sure all lua state is cleaned up before exiting, in all cases
## `0.0.6` - January 23rd, 2023
### Added
- Initial implementation of [Roblox's task library](https://create.roblox.com/docs/reference/engine/libraries/task), with some caveats:
- Minimum wait / delay time is currently set to 10ms, subject to change
- It is not yet possible to pass arguments to tasks created using `task.spawn` / `task.delay` / `task.defer`
- Timings for `task.defer` are flaky and deferred tasks are not (yet) guaranteed to run after spawned tasks
With all that said, everything else should be stable!
- Mixing and matching the `coroutine` library with `task` works in all cases
- `process.exit()` will stop all spawned / delayed / deferred threads and exit the process
- Lune is guaranteed to keep running until there are no longer any waiting threads
If any of the abovementioned things do not work as expected, it is a bug, please file an issue!
### Fixed
- Potential fix for spawned processes that yield erroring with "attempt to yield across metamethod/c-call boundary"
## `0.0.5` - January 22nd, 2023
### Added
- Added full test suites for all Lune globals to ensure correct behavior
- Added library version of Lune that can be used from other Rust projects
### Changed
- Large internal changes to allow for implementing the `task` library.
- Improved general formatting of errors to make them more readable & glanceable
- Improved output formatting of non-primitive types
- Improved output formatting of empty tables
### Fixed
- Fixed double stack trace for certain kinds of errors
## `0.0.4` - January 21st, 2023
### Added
- Added `process.args` for inspecting values given to Lune when running (read only)
- Added `process.env` which is a plain table where you can get & set environment variables
### Changed
- Improved error formatting & added proper file name to stack traces
### Removed
- Removed `...` for process arguments, use `process.args` instead
- Removed individual functions for getting & setting environment variables, use `process.env` instead
## `0.0.3` - January 20th, 2023
### Added
- Added networking functions under `net`
Example usage:
```lua
local apiResult = net.request({
url = "https://jsonplaceholder.typicode.com/posts/1",
method = "PATCH",
headers = {
["Content-Type"] = "application/json",
},
body = net.jsonEncode({
title = "foo",
body = "bar",
}),
})
local apiResponse = net.jsonDecode(apiResult.body)
assert(apiResponse.title == "foo", "Invalid json response")
assert(apiResponse.body == "bar", "Invalid json response")
```
- Added console logging & coloring functions under `console`
This piece of code:
```lua
local tab = { Integer = 1234, Hello = { "World" } }
console.log(tab)
```
Will print the following formatted text to the console, **_with syntax highlighting_**:
```lua
{
Integer = 1234,
Hello = {
"World",
}
}
```
Additional utility functions exist with the same behavior but that also print out a colored
tag together with any data given to them: `console.info`, `console.warn`, `console.error` -
These print out prefix tags `[INFO]`, `[WARN]`, `[ERROR]` in blue, orange, and red, respectively.
### Changed
- The `json` api is now part of `net`
- `json.encode` becomes `net.jsonEncode`
- `json.decode` become `net.jsonDecode`
### Fixed
- Fixed JSON decode not working properly
## `0.0.2` - January 19th, 2023
### Added
- Added support for command-line parameters to scripts
These can be accessed as a vararg in the root of a script:
```lua
local firstArg: string, secondArg: string = ...
print(firstArg, secondArg)
```
- Added CLI parameters for downloading type definitions:
- `lune --download-selene-types` to download Selene types to the current directory
- `lune --download-luau-types` to download Luau types to the current directory
These files will be downloaded as `lune.yml` and `luneTypes.d.luau`
respectively and are also available in each release on GitHub.
## `0.0.1` - January 18th, 2023
Initial Release

2717
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,130 +0,0 @@
[package]
name = "lune"
version = "0.7.5"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/filiptibell/lune"
description = "A Luau script runner"
readme = "README.md"
keywords = ["cli", "lua", "luau", "scripts"]
categories = ["command-line-interface"]
[[bin]]
name = "lune"
path = "src/main.rs"
[lib]
name = "lune"
path = "src/lib.rs"
[features]
default = ["cli", "roblox"]
cli = [
"dep:anyhow",
"dep:env_logger",
"dep:itertools",
"dep:clap",
"dep:include_dir",
"dep:regex",
]
roblox = [
"dep:glam",
"dep:rand",
"dep:rbx_cookie",
"dep:rbx_binary",
"dep:rbx_dom_weak",
"dep:rbx_reflection",
"dep:rbx_reflection_database",
"dep:rbx_xml",
]
# Profile for building the release binary, with the following options set:
#
# 1. Optimize for size
# 2. Automatically strip symbols from the binary
# 3. Enable link-time optimization
#
# Note that we could abort instead of panicking to cut down on size
# even more, but because we use the filesystem & some other APIs we
# need the panic unwinding to properly handle usage of said APIs
#
[profile.release]
opt-level = "z"
strip = true
lto = true
# All of the dependencies for Lune.
#
# Dependencies are categorized as following:
#
# 1. General dependencies with no specific features set
# 2. Large / core dependencies that have many different crates and / or features set
# 3. Dependencies for specific features of Lune, eg. the CLI or massive Roblox builtin library
#
[dependencies]
console = "0.15"
directories = "5.0"
futures-util = "0.3"
once_cell = "1.17"
thiserror = "1.0"
async-trait = "0.1"
dialoguer = "0.10"
dunce = "1.0"
lz4_flex = "0.10"
pin-project = "1.0"
os_str_bytes = "6.4"
urlencoding = "2.1.2"
### RUNTIME
mlua = { version = "0.9.0-beta.3", features = ["luau", "serialize"] }
tokio = { version = "1.24", features = ["full"] }
### SERDE
async-compression = { version = "0.4", features = [
"tokio",
"brotli",
"deflate",
"gzip",
"zlib",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_yaml = "0.9"
toml = { version = "0.7", features = ["preserve_order"] }
### NET
hyper = { version = "0.14", features = ["full"] }
hyper-tungstenite = { version = "0.10" }
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
] }
tokio-tungstenite = { version = "0.19", features = ["rustls-tls-webpki-roots"] }
### CLI
anyhow = { optional = true, version = "1.0" }
env_logger = { optional = true, version = "0.10" }
itertools = { optional = true, version = "0.10" }
clap = { optional = true, version = "4.1", features = ["derive"] }
include_dir = { optional = true, version = "0.7.3", features = ["glob"] }
regex = { optional = true, version = "1.7", default-features = false, features = [
"std",
"unicode-perl",
] }
### ROBLOX
glam = { optional = true, version = "0.24" }
rand = { optional = true, version = "0.8" }
rbx_cookie = { optional = true, version = "0.1.2" }
rbx_binary = { optional = true, git = "https://github.com/rojo-rbx/rbx-dom", rev = "e7a813d569c3f8a54be8a8873c33f8976c37b8b1" }
rbx_dom_weak = { optional = true, git = "https://github.com/rojo-rbx/rbx-dom", rev = "e7a813d569c3f8a54be8a8873c33f8976c37b8b1" }
rbx_reflection = { optional = true, git = "https://github.com/rojo-rbx/rbx-dom", rev = "e7a813d569c3f8a54be8a8873c33f8976c37b8b1" }
rbx_reflection_database = { optional = true, git = "https://github.com/rojo-rbx/rbx-dom", rev = "e7a813d569c3f8a54be8a8873c33f8976c37b8b1" }
rbx_xml = { optional = true, git = "https://github.com/rojo-rbx/rbx-dom", rev = "e7a813d569c3f8a54be8a8873c33f8976c37b8b1" }

View file

@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View file

@ -1,45 +0,0 @@
<!-- markdownlint-disable MD033 -->
<!-- markdownlint-disable MD041 -->
<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/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/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/filiptibell/lune/blob/main/LICENSE.txt">
<img src="https://img.shields.io/github/license/filiptibell/lune.svg?label=License&color=informational" alt="Lune license" />
</a>
</div>
</div>
---
A standalone [Luau](https://luau-lang.org) script runtime.
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 optimal safety and correctness.
## Features
- 🌙 A strictly minimal but powerful interface that is easy to read and remember, just like Luau itself
- 🧰 Fully featured APIs for the filesystem, networking, stdio, all included in the small (~4mb) executable
- 📚 World-class documentation, on the web _or_ directly in your editor, no network connection necessary
- 🏡 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 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!

View file

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

View file

@ -0,0 +1,16 @@
pkgbase = lune-bin
pkgdesc = [Precompiled Binaries] A standalone Luau script runtime
pkgver = 0.7.5
pkgrel = 1
url = https://lune-org.github.io/docs
arch = x86_64
arch = aarch64
license = MPL2
provides = lune
conflicts = lune
source_x86_64 = https://github.com/filiptibell/lune/releases/download/v0.7.5/lune-0.7.5-linux-x86_64.zip
sha256sums_x86_64 = eaec8e6361c8f9b4e63f756cc9b83a94bbbba28b060e5338a144e499aae2881c
source_aarch64 = https://github.com/filiptibell/lune/releases/download/v0.7.5/lune-0.7.5-linux-aarch64.zip
sha256sums_aarch64 = dad5292299db3359c8676c8e294cb9b30105ad1a47f9d96ee99fa34f2684f4fd
pkgname = lune-bin

View file

@ -0,0 +1,20 @@
# Maintainer: Erica Marigold <hi@devcomp.xyz>
pkgname=lune-bin
pkgver=0.7.5
pkgrel=1
pkgdesc="[Precompiled Binaries] A standalone Luau script runtime"
arch=('x86_64' 'aarch64')
conflicts=(lune lune-git)
url="https://lune-org.github.io/docs"
license=('MPL2')
provides=('lune')
conflicts=('lune')
source_x86_64=("https://github.com/filiptibell/lune/releases/download/v$pkgver/lune-$pkgver-linux-x86_64.zip")
source_aarch64=("https://github.com/filiptibell/lune/releases/download/v$pkgver/lune-$pkgver-linux-aarch64.zip")
sha256sums_x86_64=('eaec8e6361c8f9b4e63f756cc9b83a94bbbba28b060e5338a144e499aae2881c')
sha256sums_aarch64=('dad5292299db3359c8676c8e294cb9b30105ad1a47f9d96ee99fa34f2684f4fd')
package() {
install -Dm755 -t "$pkgdir/usr/bin" lune
}

View file

@ -0,0 +1,17 @@
pkgbase = lune-git
pkgdesc = [Latest Git Commit] A standalone Luau script runtime
pkgver = 0.7.5.r0.g4c876cb
pkgrel = 1
url = https://lune-org.github.io/docs
arch = x86_64
arch = aarch64
license = MPL2
makedepends = cargo
makedepends = git
provides = lune
conflicts = lune
options = !lto
source = git+https://github.com/filiptibell/lune.git
sha256sums = SKIP
pkgname = lune-git

View file

@ -0,0 +1,45 @@
# Maintainer: Erica Marigold <hi@devcomp.xyz>
pkgname=lune-git
pkgver=0.7.5.r0.g4c876cb
pkgrel=1
pkgdesc="[Latest Git Commit] A standalone Luau script runtime"
arch=(x86_64 aarch64)
conflicts=(lune lune-bin)
url="https://lune-org.github.io/docs"
license=(MPL2)
makedepends=(cargo git)
provides=(lune)
conflicts=(lune)
options=(!lto)
source=("git+https://github.com/filiptibell/lune.git")
sha256sums=('SKIP')
pkgver() {
cd lune
git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g'
}
prepare() {
cd lune
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
}
build() {
cd lune
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build --frozen --release --all-features
}
check() {
cd lune
export RUSTUP_TOOLCHAIN=stable
cargo test --frozen --all-features -- --test-threads 1 || (EC=$?; if [ $EC -ne 0 ]; then exit 0; fi)
}
package() {
cd lune
install -Dm755 -t ${pkgdir}/usr/bin target/release/lune
}

16
package/aur/lune/.SRCINFO Normal file
View file

@ -0,0 +1,16 @@
pkgbase = lune
pkgdesc = [Latest Stable Source] A standalone Luau script runtime
pkgver = 0.7.5
pkgrel = 1
url = https://lune-org.github.io/docs
arch = x86_64
arch = aarch64
license = MPL2
makedepends = cargo
conflicts = lune-git
conflicts = lune-bin
options = !lto
source = lune-0.7.5.tar.gz::https://github.com/filiptibell/lune/archive/refs/tags/v0.7.5.tar.gz
sha256sums = e8191df5d6844026772cc7afab1083235a265c506474c4c4dee0a7724b04f775
pkgname = lune

37
package/aur/lune/PKGBUILD Normal file
View file

@ -0,0 +1,37 @@
# Maintainer: Erica Marigold <hi@devcomp.xyz>
pkgname=lune
pkgver=0.7.5
pkgrel=1
pkgdesc="[Latest Stable Source] A standalone Luau script runtime"
arch=(x86_64 aarch64)
conflicts=(lune-git lune-bin)
url="https://lune-org.github.io/docs"
license=(MPL2)
makedepends=(cargo)
options=(!lto)
source=("${pkgname}-${pkgver}.tar.gz::https://github.com/filiptibell/lune/archive/refs/tags/v${pkgver}.tar.gz")
sha256sums=('e8191df5d6844026772cc7afab1083235a265c506474c4c4dee0a7724b04f775')
prepare() {
cd "lune-${pkgver}"
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
}
build() {
cd "lune-${pkgver}"
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build --frozen --release --all-features
}
check() {
cd lune-${pkgver}
export RUSTUP_TOOLCHAIN=stable
cargo test --frozen --all-features -- --test-threads 1 || (EC=$?; if [ $EC -ne 0 ]; then exit 0; fi)
}
package() {
cd "lune-${pkgver}"
install -Dm755 -t ${pkgdir}/usr/bin target/release/lune
}

View file

@ -1,38 +0,0 @@
local fs = require("@lune/fs")
local net = require("@lune/net")
local URL =
"https://gist.githubusercontent.com/Anaminus/49ac255a68e7a7bc3cdd72b602d5071f/raw/f1534dcae312dbfda716b7677f8ac338b565afc3/BrickColor.json"
local json = net.jsonDecode(net.request(URL).body)
local contents = ""
contents ..= "const BRICK_COLOR_DEFAULT: u16 = "
contents ..= tostring(json.Default)
contents ..= ";\n"
contents ..= "\nconst BRICK_COLOR_VALUES: &[(u16, &str, (u8, u8, u8))] = &[\n"
for _, color in json.BrickColors do
contents ..= string.format(
' (%d, "%s", (%d, %d, %d)),\n',
color.Number,
color.Name,
color.Color8[1],
color.Color8[2],
color.Color8[3]
)
end
contents ..= "];\n"
contents ..= "\nconst BRICK_COLOR_PALETTE: &[u16] = &["
contents ..= table.concat(json.Palette, ", ")
contents ..= "];\n"
contents ..= "\nconst BRICK_COLOR_CONSTRUCTORS: &[(&str, u16)] = &["
for key, number in json.Constructors do
contents ..= string.format(' ("%s", %d),\n', key, number)
end
contents ..= "];\n"
fs.writeFile("packages/lib-roblox/scripts/brick_color.rs", contents)

View file

@ -1,193 +0,0 @@
--!nocheck
-- NOTE: This must be ran in Roblox Studio to get up-to-date font values
local contents = ""
contents ..= "\ntype FontData = (&'static str, FontWeight, FontStyle);\n"
local ENUM_IMPLEMENTATION = [[
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum <<ENUM_NAME>> {
<<ENUM_NAMES>>
}
impl <<ENUM_NAME>> {
pub(crate) fn as_<<NUMBER_TYPE>>(&self) -> <<NUMBER_TYPE>> {
match self {
<<ENUM_TO_NUMBERS>>
}
}
pub(crate) fn from_<<NUMBER_TYPE>>(n: <<NUMBER_TYPE>>) -> Option<Self> {
match n {
<<NUMBERS_TO_ENUM>>
_ => None,
}
}
}
impl Default for <<ENUM_NAME>> {
fn default() -> Self {
Self::<<DEFAULT_NAME>>
}
}
impl std::str::FromStr for <<ENUM_NAME>> {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
<<STRINGS_TO_ENUM>>
_ => Err("Unknown <<ENUM_NAME>>"),
}
}
}
impl std::fmt::Display for <<ENUM_NAME>> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
<<ENUM_TO_STRINGS>>
}
)
}
}
impl<'lua> FromLua<'lua> for <<ENUM_NAME>> {
fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
let mut message = None;
if let LuaValue::UserData(ud) = &lua_value {
let value = ud.borrow::<EnumItem>()?;
if value.parent.desc.name == "<<ENUM_NAME>>" {
if let Ok(value) = <<ENUM_NAME>>::from_str(&value.name) {
return Ok(value);
} else {
message = Some(format!(
"Found unknown Enum.<<ENUM_NAME>> value '{}'",
value.name
));
}
} else {
message = Some(format!(
"Expected Enum.<<ENUM_NAME>>, got Enum.{}",
value.parent.desc.name
));
}
}
Err(LuaError::FromLuaConversionError {
from: lua_value.type_name(),
to: "Enum.<<ENUM_NAME>>",
message,
})
}
}
impl<'lua> ToLua<'lua> for <<ENUM_NAME>> {
fn to_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
match EnumItem::from_enum_name_and_name("<<ENUM_NAME>>", self.to_string()) {
Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)),
None => Err(LuaError::ToLuaConversionError {
from: "<<ENUM_NAME>>",
to: "EnumItem",
message: Some(format!("Found unknown Enum.<<ENUM_NAME>> value '{}'", self)),
}),
}
}
}
]]
-- FontWeight enum and implementation
local function makeRustEnum(enum, default, numType: string)
local name = tostring(enum)
name = string.gsub(name, "^Enum.", "")
local defaultName = tostring(default)
defaultName = string.gsub(defaultName, "^Enum.", "")
defaultName = string.gsub(defaultName, "^" .. name .. ".", "")
local enumNames = ""
local enumToNumbers = ""
local numbersToEnum = ""
local stringsToEnum = ""
local enumToStrings = ""
for _, enum in enum:GetEnumItems() do
enumNames ..= `\n{enum.Name},`
enumToNumbers ..= `\nSelf::{enum.Name} => {enum.Value},`
numbersToEnum ..= `\n{enum.Value} => Some(Self::{enum.Name}),`
stringsToEnum ..= `\n"{enum.Name}" => Ok(Self::{enum.Name}),`
enumToStrings ..= `\nSelf::{enum.Name} => "{enum.Name}",`
end
local mappings: { [string]: string } = {
["<<ENUM_NAME>>"] = name,
["<<DEFAULT_NAME>>"] = defaultName,
["<<NUMBER_TYPE>>"] = numType,
["<<ENUM_NAMES>>"] = enumNames,
["<<ENUM_TO_NUMBERS>>"] = enumToNumbers,
["<<ENUM_TO_STRINGS>>"] = enumToStrings,
["<<NUMBERS_TO_ENUM>>"] = numbersToEnum,
["<<STRINGS_TO_ENUM>>"] = stringsToEnum,
}
local result = ENUM_IMPLEMENTATION
for key, replacement in mappings do
result = string.gsub(result, "(\t*)" .. key, function(tabbing)
local spacing = string.gsub(tabbing, "\t", " ")
local inner = string.gsub(replacement, "\n", "\n" .. spacing)
inner = string.gsub(inner, "^\n+", "")
return inner
end)
end
return result
end
contents ..= makeRustEnum(Enum.FontWeight, Enum.FontWeight.Regular, "u16")
contents ..= "\n"
contents ..= makeRustEnum(Enum.FontStyle, Enum.FontStyle.Normal, "u8")
contents ..= "\n"
-- Font constant map from enum to font data
local longestNameLen = 0
local longestFamilyLen = 0
local longestWeightLen = 0
for _, enum in Enum.Font:GetEnumItems() do
longestNameLen = math.max(longestNameLen, #enum.Name)
if enum ~= Enum.Font.Unknown then
local font = Font.fromEnum(enum)
longestFamilyLen = math.max(longestFamilyLen, #font.Family)
longestWeightLen = math.max(longestWeightLen, #font.Weight.Name)
end
end
contents ..= "\n#[rustfmt::skip]\nconst FONT_ENUM_MAP: &[(&str, Option<FontData>)] = &[\n"
for _, enum in Enum.Font:GetEnumItems() do
if enum == Enum.Font.Unknown then
contents ..= string.format(
' ("Unknown",%s None),\n',
string.rep(" ", longestNameLen - #enum.Name)
)
else
local font = Font.fromEnum(enum)
contents ..= string.format(
' ("%s",%s Some(("%s",%s FontWeight::%s,%s FontStyle::%s))),\n',
enum.Name,
string.rep(" ", longestNameLen - #enum.Name),
font.Family,
string.rep(" ", longestFamilyLen - #font.Family),
font.Weight.Name,
string.rep(" ", longestWeightLen - #font.Weight.Name),
font.Style.Name
)
end
end
contents ..= "];\n"
print(contents)

View file

@ -1,28 +0,0 @@
--!nocheck
-- NOTE: This must be ran in Roblox Studio to get up-to-date enum values
local contents = ""
local longestNameLen = 0
for _, enum in Enum.Material:GetEnumItems() do
longestNameLen = math.max(longestNameLen, #enum.Name)
end
contents ..= "\n#[rustfmt::skip]\nconst MATERIAL_ENUM_MAP: &[(&str, f32, f32, f32, f32, f32)] = &[\n"
for _, enum in Enum.Material:GetEnumItems() do
local props = PhysicalProperties.new(enum)
contents ..= string.format(
' ("%s",%s %.2f, %.2f, %.2f, %.2f, %.2f),\n',
enum.Name,
string.rep(" ", longestNameLen - #enum.Name),
props.Density,
props.Friction,
props.Elasticity,
props.FrictionWeight,
props.ElasticityWeight
)
end
contents ..= "];\n"
print(contents)

View file

@ -1,6 +0,0 @@
std = "luau+lune"
exclude = ["luneTypes.d.luau"]
[lints]
high_cyclomatic_complexity = "warn"

View file

@ -1,61 +0,0 @@
use std::collections::HashMap;
use anyhow::{Context, Result};
use directories::UserDirs;
use futures_util::future::try_join_all;
use include_dir::Dir;
use tokio::fs::{create_dir_all, write};
pub async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result<String> {
let contents = read_typedefs_dir_contents(dir);
write_typedef_files(contents).await
}
fn read_typedefs_dir_contents(dir: &Dir<'_>) -> HashMap<String, Vec<u8>> {
let mut definitions = HashMap::new();
for entry in dir.find("*.luau").unwrap() {
let entry_file = entry.as_file().unwrap();
let entry_name = entry_file.path().file_name().unwrap().to_string_lossy();
let typedef_name = entry_name.trim_end_matches(".luau");
let typedef_contents = entry_file.contents().to_vec();
definitions.insert(typedef_name.to_string(), typedef_contents);
}
definitions
}
async fn write_typedef_files(typedef_files: HashMap<String, Vec<u8>>) -> Result<String> {
let version_string = env!("CARGO_PKG_VERSION");
let mut dirs_to_write = Vec::new();
let mut files_to_write = Vec::new();
// Create the typedefs dir in the users cache dir
let cache_dir = UserDirs::new()
.context("Failed to find user home directory")?
.home_dir()
.join(".lune")
.join(".typedefs")
.join(version_string);
dirs_to_write.push(cache_dir.clone());
// Make typedef files
for (builtin_name, builtin_typedef) in typedef_files {
let path = cache_dir
.join(builtin_name.to_ascii_lowercase())
.with_extension("luau");
files_to_write.push((builtin_name.to_lowercase(), path, builtin_typedef));
}
// Write all dirs and files only when we know generation was successful
let futs_dirs = dirs_to_write
.drain(..)
.map(create_dir_all)
.collect::<Vec<_>>();
let futs_files = files_to_write
.iter()
.map(|(_, path, contents)| write(path, contents))
.collect::<Vec<_>>();
try_join_all(futs_dirs).await?;
try_join_all(futs_files).await?;
Ok(version_string.to_string())
}

View file

@ -1,187 +0,0 @@
use std::{fmt::Write as _, process::ExitCode};
use anyhow::{Context, Result};
use clap::{CommandFactory, Parser};
use lune::Lune;
use tokio::{
fs::read as read_to_vec,
io::{stdin, AsyncReadExt},
};
pub(crate) mod gen;
pub(crate) mod setup;
pub(crate) mod utils;
use setup::run_setup;
use utils::{
files::{discover_script_path_including_lune_dirs, strip_shebang},
listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list},
};
/// A Luau script runner
#[derive(Parser, Debug, Default, Clone)]
#[command(version, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
/// Script name or full path to the file to run
script_path: Option<String>,
/// Arguments to pass to the script, stored in process.args
script_args: Vec<String>,
/// List scripts found inside of a nearby `lune` directory
#[clap(long, short = 'l')]
list: bool,
/// Set up type definitions and settings for development
#[clap(long)]
setup: bool,
/// Generate a Luau type definitions file in the current dir
#[clap(long, hide = true)]
generate_luau_types: bool,
/// Generate a Selene type definitions file in the current dir
#[clap(long, hide = true)]
generate_selene_types: bool,
/// Generate a Lune documentation file for Luau LSP
#[clap(long, hide = true)]
generate_docs_file: bool,
}
#[allow(dead_code)]
impl Cli {
pub fn new() -> Self {
Self::default()
}
pub fn with_path<S>(mut self, path: S) -> Self
where
S: Into<String>,
{
self.script_path = Some(path.into());
self
}
pub fn with_args<A>(mut self, args: A) -> Self
where
A: Into<Vec<String>>,
{
self.script_args = args.into();
self
}
pub fn setup(mut self) -> Self {
self.setup = true;
self
}
pub fn list(mut self) -> Self {
self.list = true;
self
}
#[allow(clippy::too_many_lines)]
pub async fn run(self) -> Result<ExitCode> {
// List files in `lune` and `.lune` directories, if wanted
// This will also exit early and not run anything else
if self.list {
let sorted_relative = match find_lune_scripts(false).await {
Ok(scripts) => sort_lune_scripts(scripts),
Err(e) => {
eprintln!("{e}");
return Ok(ExitCode::FAILURE);
}
};
let sorted_home_dir = match find_lune_scripts(true).await {
Ok(scripts) => sort_lune_scripts(scripts),
Err(e) => {
eprintln!("{e}");
return Ok(ExitCode::FAILURE);
}
};
let mut buffer = String::new();
if !sorted_relative.is_empty() {
if sorted_home_dir.is_empty() {
write!(&mut buffer, "Available scripts:")?;
} else {
write!(&mut buffer, "Available scripts in current directory:")?;
}
write_lune_scripts_list(&mut buffer, sorted_relative)?;
}
if !sorted_home_dir.is_empty() {
write!(&mut buffer, "Available global scripts:")?;
write_lune_scripts_list(&mut buffer, sorted_home_dir)?;
}
if buffer.is_empty() {
println!("No scripts found.");
} else {
print!("{buffer}");
}
return Ok(ExitCode::SUCCESS);
}
// Generate (save) definition files, if wanted
let generate_file_requested = self.setup
|| self.generate_luau_types
|| self.generate_selene_types
|| self.generate_docs_file;
if generate_file_requested {
if (self.generate_luau_types || self.generate_selene_types || self.generate_docs_file)
&& !self.setup
{
eprintln!(
"\
Typedef & docs generation commands have been superseded by the setup command.\
Run `lune --setup` in your terminal to configure your editor and type definitions.
"
);
return Ok(ExitCode::FAILURE);
}
if self.setup {
run_setup().await;
}
}
if self.script_path.is_none() {
// Only generating typedefs without running a script is completely
// fine, and we should just exit the program normally afterwards
if generate_file_requested {
return Ok(ExitCode::SUCCESS);
}
// HACK: We know that we didn't get any arguments here but since
// script_path is optional clap will not error on its own, to fix
// we will duplicate the cli command and make arguments required,
// which will then fail and print out the normal help message
let cmd = Cli::command();
cmd.arg_required_else_help(true).get_matches();
}
// Figure out if we should read from stdin or from a file,
// reading from stdin is marked by passing a single "-"
// (dash) as the script name to run to the cli
let script_path = self.script_path.unwrap();
let (script_display_name, script_contents) = if script_path == "-" {
let mut stdin_contents = Vec::new();
stdin()
.read_to_end(&mut stdin_contents)
.await
.context("Failed to read script contents from stdin")?;
("stdin".to_string(), stdin_contents)
} else {
let file_path = discover_script_path_including_lune_dirs(&script_path)?;
let file_contents = read_to_vec(&file_path).await?;
// NOTE: We skip the extension here to remove it from stack traces
let file_display_name = file_path.with_extension("").display().to_string();
(file_display_name, file_contents)
};
// Create a new lune object with all globals & run the script
let result = Lune::new()
.with_args(self.script_args)
.run(&script_display_name, strip_shebang(script_contents))
.await;
Ok(match result {
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Ok(code) => code,
})
}
}

View file

@ -1,128 +0,0 @@
use std::{borrow::BorrowMut, env::current_dir, io::ErrorKind, path::PathBuf};
use anyhow::Result;
use include_dir::{include_dir, Dir};
use thiserror::Error;
use tokio::fs;
// TODO: Use a library that supports json with comments since VSCode settings may contain comments
use serde_json::Value as JsonValue;
use super::gen::generate_typedef_files_from_definitions;
pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("types");
pub(crate) static SETTING_NAME_MODE: &str = "luau-lsp.require.mode";
pub(crate) static SETTING_NAME_ALIASES: &str = "luau-lsp.require.directoryAliases";
#[derive(Debug, Clone, Copy, Error)]
enum SetupError {
#[error("Failed to read settings")]
Read,
#[error("Failed to write settings")]
Write,
#[error("Failed to parse settings")]
Deserialize,
#[error("Failed to create settings")]
Serialize,
}
fn lune_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn vscode_path() -> PathBuf {
current_dir()
.expect("No current dir")
.join(".vscode")
.join("settings.json")
}
async fn read_or_create_vscode_settings_json() -> Result<JsonValue, SetupError> {
let path_file = vscode_path();
let mut path_dir = path_file.clone();
path_dir.pop();
match fs::read(&path_file).await {
Err(e) if e.kind() == ErrorKind::NotFound => {
// TODO: Make sure that VSCode is actually installed, or
// let the user choose their editor for interactive setup
match fs::create_dir_all(path_dir).await {
Err(_) => Err(SetupError::Write),
Ok(_) => match fs::write(path_file, "{}").await {
Err(_) => Err(SetupError::Write),
Ok(_) => Ok(JsonValue::Object(serde_json::Map::new())),
},
}
}
Err(_) => Err(SetupError::Read),
Ok(contents) => match serde_json::from_slice(&contents) {
Err(_) => Err(SetupError::Deserialize),
Ok(json) => Ok(json),
},
}
}
async fn write_vscode_settings_json(value: JsonValue) -> Result<(), SetupError> {
match serde_json::to_vec_pretty(&value) {
Err(_) => Err(SetupError::Serialize),
Ok(json) => match fs::write(vscode_path(), json).await {
Err(_) => Err(SetupError::Write),
Ok(_) => Ok(()),
},
}
}
fn add_values_to_vscode_settings_json(value: JsonValue) -> JsonValue {
let mut settings_json = value;
if let JsonValue::Object(settings) = settings_json.borrow_mut() {
// Set require mode
let mode_val = "relativeToFile".to_string();
settings.insert(SETTING_NAME_MODE.to_string(), JsonValue::String(mode_val));
// Set require alias to our typedefs
let aliases_key = "@lune/".to_string();
let aliases_val = format!("~/.lune/.typedefs/{}/", lune_version());
if let Some(JsonValue::Object(aliases)) = settings.get_mut(SETTING_NAME_ALIASES) {
if aliases.contains_key(&aliases_key) {
if aliases.get(&aliases_key).unwrap() != &JsonValue::String(aliases_val.to_string())
{
aliases.insert(aliases_key, JsonValue::String(aliases_val));
}
} else {
aliases.insert(aliases_key, JsonValue::String(aliases_val));
}
} else {
let mut map = serde_json::Map::new();
map.insert(aliases_key, JsonValue::String(aliases_val));
settings.insert(SETTING_NAME_ALIASES.to_string(), JsonValue::Object(map));
}
}
settings_json
}
pub async fn run_setup() {
generate_typedef_files_from_definitions(&TYPEDEFS_DIR)
.await
.expect("Failed to generate typedef files");
// TODO: Let the user interactively choose what editor to set up
let res = async {
let settings = read_or_create_vscode_settings_json().await?;
let modified = add_values_to_vscode_settings_json(settings);
write_vscode_settings_json(modified).await?;
Ok::<_, SetupError>(())
}
.await;
let message = match res {
Ok(_) => "These settings have been added to your workspace for Visual Studio Code:",
Err(_) => "To finish setting up your editor, add these settings to your workspace:",
};
let version_string = lune_version();
println!(
"Lune has now been set up and editor type definitions have been generated.\
\n{message}\
\n\
\n\"{SETTING_NAME_MODE}\": \"relativeToFile\",\
\n\"{SETTING_NAME_ALIASES}\": {{\
\n \"@lune/\": \"~/.lune/.typedefs/{version_string}/\"\
\n}}",
);
}

View file

@ -1,206 +0,0 @@
use std::{
fs::Metadata,
path::{PathBuf, MAIN_SEPARATOR},
};
use anyhow::{anyhow, bail, Result};
use console::style;
use directories::UserDirs;
use itertools::Itertools;
use once_cell::sync::Lazy;
const LUNE_COMMENT_PREFIX: &str = "-->";
static ERR_MESSAGE_HELP_NOTE: Lazy<String> = Lazy::new(|| {
format!(
"To run this file, either:\n{}\n{}",
format_args!(
"{} rename it to use a {} or {} extension",
style("-").dim(),
style(".luau").blue(),
style(".lua").blue()
),
format_args!(
"{} pass it as an absolute path instead of relative",
style("-").dim()
),
)
});
/**
Discovers a script file path based on a given script name.
Script discovery is done in several steps here for the best possible user experience:
1. If we got a file that definitely exists, make sure it is either
- using an absolute path
- has the lua or luau extension
2. If we got a directory, check if it has an `init` file to use, and if it doesn't, let the user know
3. If we got an absolute path, don't check any extensions, just let the user know it didn't exist
4. If we got a relative path with no extension, also look for a file with a lua or luau extension
5. No other options left, the file simply did not exist
This behavior ensures that users can do pretty much whatever they want if they pass in an absolute
path, and that they then have control over script discovery behavior, whereas if they pass in
a relative path we will instead try to be as permissive as possible for user-friendliness
*/
pub fn discover_script_path(path: impl AsRef<str>, in_home_dir: bool) -> Result<PathBuf> {
// NOTE: We don't actually support any platforms without home directories,
// but just in case the user has some strange configuration and it cannot
// be found we should at least throw a nice error instead of panicking
let path = path.as_ref();
let file_path = if in_home_dir {
match UserDirs::new() {
Some(dirs) => dirs.home_dir().join(path),
None => {
bail!(
"No file was found at {}\nThe home directory does not exist",
style(path).yellow()
)
}
}
} else {
PathBuf::from(path)
};
// NOTE: We use metadata directly here to try to
// avoid accessing the file path more than once
let file_meta = file_path.metadata();
let is_file = file_meta.as_ref().map_or(false, Metadata::is_file);
let is_dir = file_meta.as_ref().map_or(false, Metadata::is_dir);
let is_abs = file_path.is_absolute();
let ext = file_path.extension();
if is_file {
if is_abs {
Ok(file_path)
} else if let Some(ext) = file_path.extension() {
match ext {
e if e == "lua" || e == "luau" => Ok(file_path),
_ => Err(anyhow!(
"A file was found at {} but it uses the '{}' file extension\n{}",
style(file_path.display()).green(),
style(ext.to_string_lossy()).blue(),
*ERR_MESSAGE_HELP_NOTE
)),
}
} else {
Err(anyhow!(
"A file was found at {} but it has no file extension\n{}",
style(file_path.display()).green(),
*ERR_MESSAGE_HELP_NOTE
))
}
} else if is_dir {
match (
discover_script_path(format!("{path}/init.luau"), in_home_dir),
discover_script_path(format!("{path}/init.lua"), in_home_dir),
) {
(Ok(path), _) | (_, Ok(path)) => Ok(path),
_ => Err(anyhow!(
"No file was found at {}, found a directory without an init file",
style(file_path.display()).yellow()
)),
}
} else if is_abs && !in_home_dir {
Err(anyhow!(
"No file was found at {}",
style(file_path.display()).yellow()
))
} else if ext.is_none() {
let file_path_lua = file_path.with_extension("lua");
let file_path_luau = file_path.with_extension("luau");
if file_path_lua.is_file() {
Ok(file_path_lua)
} else if file_path_luau.is_file() {
Ok(file_path_luau)
} else {
Err(anyhow!(
"No file was found at {}",
style(file_path.display()).yellow()
))
}
} else {
Err(anyhow!(
"No file was found at {}",
style(file_path.display()).yellow()
))
}
}
/**
Discovers a script file path based on a given script name, and tries to
find scripts in `lune` and `.lune` folders if one was not directly found.
Note that looking in `lune` and `.lune` folders is automatically
disabled if the given script name is an absolute path.
Behavior is otherwise exactly the same as for `discover_script_file_path`.
*/
pub fn discover_script_path_including_lune_dirs(path: &str) -> Result<PathBuf> {
match discover_script_path(path, false) {
Ok(path) => Ok(path),
Err(e) => {
// If we got any absolute path it means the user has also
// told us to not look in any special relative directories
// so we should error right away with the first err message
if PathBuf::from(path).is_absolute() {
return Err(e);
}
// Otherwise we take a look in relative lune and .lune
// directories + the home directory for the current user
let res = discover_script_path(format!("lune{MAIN_SEPARATOR}{path}"), false)
.or_else(|_| discover_script_path(format!(".lune{MAIN_SEPARATOR}{path}"), false))
.or_else(|_| discover_script_path(format!("lune{MAIN_SEPARATOR}{path}"), true))
.or_else(|_| discover_script_path(format!(".lune{MAIN_SEPARATOR}{path}"), true));
match res {
// NOTE: The first error message is generally more
// descriptive than the ones for the lune subfolders
Err(_) => Err(e),
Ok(path) => Ok(path),
}
}
}
}
pub fn parse_lune_description_from_file(contents: &str) -> Option<String> {
let mut comment_lines = Vec::new();
for line in contents.lines() {
if let Some(stripped) = line.strip_prefix(LUNE_COMMENT_PREFIX) {
comment_lines.push(stripped);
} else {
break;
}
}
if comment_lines.is_empty() {
None
} else {
let shortest_indent = comment_lines.iter().fold(usize::MAX, |acc, line| {
let first_alphanumeric = line.find(char::is_alphanumeric).unwrap();
acc.min(first_alphanumeric)
});
let unindented_lines = comment_lines
.iter()
.map(|line| &line[shortest_indent..])
// Replace newlines with a single space inbetween instead
.interleave(std::iter::repeat(" ").take(comment_lines.len() - 1))
.collect();
Some(unindented_lines)
}
}
pub fn strip_shebang(mut contents: Vec<u8>) -> Vec<u8> {
if contents.starts_with(b"#!") {
if let Some(first_newline_idx) =
contents
.iter()
.enumerate()
.find_map(|(idx, c)| if *c == b'\n' { Some(idx) } else { None })
{
// NOTE: We keep the newline here on purpose to preserve
// correct line numbers in stack traces, the only reason
// we strip the shebang is to get the lua script to parse
// and the extra newline is not really a problem for that
contents.drain(..first_newline_idx);
}
}
contents
}

View file

@ -1,121 +0,0 @@
use std::{cmp::Ordering, ffi::OsStr, fmt::Write as _, path::PathBuf};
use anyhow::{bail, Result};
use console::Style;
use directories::UserDirs;
use once_cell::sync::Lazy;
use tokio::{fs, io};
use super::files::parse_lune_description_from_file;
pub static COLOR_BLUE: Lazy<Style> = Lazy::new(|| Style::new().blue());
pub static STYLE_DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
pub async fn find_lune_scripts(in_home_dir: bool) -> Result<Vec<(String, String)>> {
let base_path = if in_home_dir {
UserDirs::new().unwrap().home_dir().to_path_buf()
} else {
PathBuf::new()
};
let mut lune_dir = fs::read_dir(base_path.join("lune")).await;
if lune_dir.is_err() {
lune_dir = fs::read_dir(base_path.join(".lune")).await;
}
match lune_dir {
Ok(mut dir) => {
let mut files = Vec::new();
while let Some(entry) = dir.next_entry().await? {
let meta = entry.metadata().await?;
if meta.is_file() {
let contents = fs::read(entry.path()).await?;
files.push((entry, meta, contents));
}
}
let parsed: Vec<_> = files
.iter()
.filter(|(entry, _, _)| {
matches!(
entry.path().extension().and_then(OsStr::to_str),
Some("lua" | "luau")
)
})
.map(|(entry, _, contents)| {
let contents_str = String::from_utf8_lossy(contents);
let file_path = entry.path().with_extension("");
let file_name = file_path.file_name().unwrap().to_string_lossy();
let description = parse_lune_description_from_file(&contents_str);
(file_name.to_string(), description.unwrap_or_default())
})
.collect();
Ok(parsed)
}
Err(e) if matches!(e.kind(), io::ErrorKind::NotFound) => {
bail!("No lune directory was found.")
}
Err(e) => {
bail!("Failed to read lune files!\n{e}")
}
}
}
pub fn sort_lune_scripts(scripts: Vec<(String, String)>) -> Vec<(String, String)> {
let mut sorted = scripts;
sorted.sort_by(|left, right| {
// Prefer scripts that have a description
let left_has_desc = !left.1.is_empty();
let right_has_desc = !right.1.is_empty();
if left_has_desc == right_has_desc {
// If both have a description or both
// have no description, we sort by name
left.0.cmp(&right.0)
} else if left_has_desc {
Ordering::Less
} else {
Ordering::Greater
}
});
sorted
}
pub fn write_lune_scripts_list(buffer: &mut String, scripts: Vec<(String, String)>) -> Result<()> {
let longest_file_name_len = scripts
.iter()
.fold(0, |acc, (file_name, _)| acc.max(file_name.len()));
let script_with_description_exists = scripts.iter().any(|(_, desc)| !desc.is_empty());
// Pre-calculate some strings that will be used often
let prefix = format!("{} ", COLOR_BLUE.apply_to('>'));
let separator = format!("{}", STYLE_DIM.apply_to('-'));
// Write the entire output to a buffer, doing this instead of using individual
// writeln! calls will ensure that no output get mixed up in between these lines
if script_with_description_exists {
for (file_name, description) in scripts {
if description.is_empty() {
write!(buffer, "\n{prefix}{file_name}")?;
} else {
let mut lines = description.lines();
let first_line = lines.next().unwrap_or_default();
let file_spacing = " ".repeat(file_name.len());
let line_spacing = " ".repeat(longest_file_name_len - file_name.len());
write!(
buffer,
"\n{prefix}{file_name}{line_spacing} {separator} {}",
COLOR_BLUE.apply_to(first_line)
)?;
for line in lines {
write!(
buffer,
"\n{prefix}{file_spacing}{line_spacing} {}",
COLOR_BLUE.apply_to(line)
)?;
}
}
}
} else {
for (file_name, _) in scripts {
write!(buffer, "\n{prefix}{file_name}")?;
}
}
// Finally, write an ending newline
writeln!(buffer)?;
Ok(())
}

View file

@ -1,2 +0,0 @@
pub mod files;
pub mod listing;

View file

@ -1,7 +0,0 @@
mod lune;
mod roblox;
#[cfg(test)]
mod tests;
pub use crate::lune::{Lune, LuneError};

View file

@ -1,136 +0,0 @@
use std::io::ErrorKind as IoErrorKind;
use std::path::{PathBuf, MAIN_SEPARATOR};
use mlua::prelude::*;
use tokio::fs;
use crate::lune::lua::{
fs::{copy, FsMetadata, FsWriteOptions},
table::TableBuilder,
};
pub fn create(lua: &'static 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: &'static Lua, path: String) -> LuaResult<LuaString> {
let bytes = fs::read(&path).await.map_err(LuaError::external)?;
lua.create_string(bytes)
}
async fn fs_read_dir(_: &'static Lua, path: String) -> LuaResult<Vec<String>> {
let mut dir_strings = Vec::new();
let mut dir = fs::read_dir(&path).await.map_err(LuaError::external)?;
while let Some(dir_entry) = dir.next_entry().await.map_err(LuaError::external)? {
if let Some(dir_path_str) = dir_entry.path().to_str() {
dir_strings.push(dir_path_str.to_owned());
} else {
return Err(LuaError::RuntimeError(format!(
"File path could not be converted into a string: '{}'",
dir_entry.path().display()
)));
}
}
let mut dir_string_prefix = path;
if !dir_string_prefix.ends_with(MAIN_SEPARATOR) {
dir_string_prefix.push(MAIN_SEPARATOR);
}
let dir_strings_no_prefix = dir_strings
.iter()
.map(|inner_path| {
inner_path
.trim()
.trim_start_matches(&dir_string_prefix)
.to_owned()
})
.collect::<Vec<_>>();
Ok(dir_strings_no_prefix)
}
async fn fs_write_file(
_: &'static Lua,
(path, contents): (String, LuaString<'_>),
) -> LuaResult<()> {
fs::write(&path, &contents.as_bytes())
.await
.map_err(LuaError::external)
}
async fn fs_write_dir(_: &'static Lua, path: String) -> LuaResult<()> {
fs::create_dir_all(&path).await.map_err(LuaError::external)
}
async fn fs_remove_file(_: &'static Lua, path: String) -> LuaResult<()> {
fs::remove_file(&path).await.map_err(LuaError::external)
}
async fn fs_remove_dir(_: &'static Lua, path: String) -> LuaResult<()> {
fs::remove_dir_all(&path).await.map_err(LuaError::external)
}
async fn fs_metadata(_: &'static 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(_: &'static 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(_: &'static 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(
_: &'static 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
.map_err(LuaError::external)?;
Ok(())
}
async fn fs_copy(
_: &'static Lua,
(from, to, options): (String, String, FsWriteOptions),
) -> LuaResult<()> {
copy(from, to, options).await
}

View file

@ -1,10 +0,0 @@
pub mod fs;
pub mod net;
pub mod process;
pub mod serde;
pub mod stdio;
pub mod task;
pub mod top_level;
#[cfg(feature = "roblox")]
pub mod roblox;

View file

@ -1,215 +0,0 @@
use std::collections::HashMap;
use mlua::prelude::*;
use console::style;
use hyper::{
header::{CONTENT_ENCODING, CONTENT_LENGTH},
Server,
};
use tokio::{sync::mpsc, task};
use crate::lune::lua::{
net::{
NetClient, NetClientBuilder, NetLocalExec, NetService, NetWebSocket, RequestConfig,
ServeConfig,
},
serde::{decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat},
table::TableBuilder,
task::{TaskScheduler, TaskSchedulerAsyncExt},
};
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
// Create a reusable client for performing our
// web requests and store it in the lua registry,
// allowing us to reuse headers and internal structs
let client = NetClientBuilder::new()
.headers(&[("User-Agent", create_user_agent_header())])?
.build()?;
lua.set_named_registry_value("net.client", client)?;
// Create the global table for net
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 create_user_agent_header() -> String {
let (github_owner, github_repo) = env!("CARGO_PKG_REPOSITORY")
.trim_start_matches("https://github.com/")
.split_once('/')
.unwrap();
format!("{github_owner}-{github_repo}-cli")
}
fn net_json_encode<'a>(
lua: &'static Lua,
(val, pretty): (LuaValue<'a>, Option<bool>),
) -> LuaResult<LuaString<'a>> {
EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()))
.serialize_to_string(lua, val)
}
fn net_json_decode<'a>(lua: &'static Lua, json: LuaString<'a>) -> LuaResult<LuaValue<'a>> {
EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json)
}
async fn net_request<'a>(lua: &'static Lua, config: RequestConfig<'a>) -> LuaResult<LuaTable<'a>> {
// Create and send the request
let client: LuaUserDataRef<NetClient> = lua.named_registry_value("net.client")?;
let mut request = client.request(config.method, &config.url);
for (query, value) in config.query {
request = request.query(&[(query.to_str()?, value.to_str()?)]);
}
for (header, value) in config.headers {
request = request.header(header.to_str()?, value.to_str()?);
}
let res = request
.body(config.body.unwrap_or_default())
.send()
.await
.map_err(LuaError::external)?;
// Extract status, headers
let res_status = res.status().as_u16();
let res_status_text = res.status().canonical_reason();
let mut res_headers = res
.headers()
.iter()
.map(|(name, value)| {
(
name.as_str().to_string(),
value.to_str().unwrap().to_owned(),
)
})
.collect::<HashMap<String, String>>();
// Read response bytes
let mut res_bytes = res.bytes().await.map_err(LuaError::external)?.to_vec();
// Check for extra options, decompression
if config.options.decompress {
// NOTE: Header names are guaranteed to be lowercase because of the above
// transformations of them into the hashmap, so we can compare directly
let format = res_headers.iter().find_map(|(name, val)| {
if name == CONTENT_ENCODING.as_str() {
CompressDecompressFormat::detect_from_header_str(val)
} else {
None
}
});
if let Some(format) = format {
res_bytes = decompress(format, res_bytes).await?;
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
});
}
}
// Construct and return a readonly lua table with results
TableBuilder::new(lua)?
.with_value("ok", (200..300).contains(&res_status))?
.with_value("statusCode", res_status)?
.with_value("statusMessage", res_status_text)?
.with_value("headers", res_headers)?
.with_value("body", lua.create_string(&res_bytes)?)?
.build_readonly()
}
async fn net_socket<'a>(lua: &'static Lua, url: String) -> LuaResult<LuaTable> {
let (ws, _) = tokio_tungstenite::connect_async(url)
.await
.map_err(LuaError::external)?;
NetWebSocket::new(ws).into_lua_table(lua)
}
async fn net_serve<'a>(
lua: &'static Lua,
(port, config): (u16, ServeConfig<'a>),
) -> LuaResult<LuaTable<'a>> {
// Note that we need to use a mpsc here and not
// a oneshot channel since we move the sender
// into our table with the stop function
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
let server_request_callback = lua.create_registry_value(config.handle_request)?;
let server_websocket_callback = config.handle_web_socket.map(|handler| {
lua.create_registry_value(handler)
.expect("Failed to store websocket handler")
});
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler - make sure it is added as a lua app data before the first scheduler resumption");
// Bind first to make sure that we can bind to this address
let bound = match Server::try_bind(&([127, 0, 0, 1], port).into()) {
Err(e) => {
return Err(LuaError::external(format!(
"Failed to bind to localhost on port {port}\n{}",
format!("{e}").replace(
"error creating server listener: ",
&format!("{}", style("> ").dim())
)
)));
}
Ok(bound) => bound,
};
// Register a background task to prevent the task scheduler from
// exiting early and start up our web server on the bound address
let task = sched.register_background_task();
let server = bound
.http1_only(true) // Web sockets can only use http1
.http1_keepalive(true) // Web sockets must be kept alive
.executor(NetLocalExec)
.serve(NetService::new(
lua,
server_request_callback,
server_websocket_callback,
))
.with_graceful_shutdown(async move {
task.unregister(Ok(()));
shutdown_rx
.recv()
.await
.expect("Server was stopped instantly");
shutdown_rx.close();
});
// Spawn a new tokio task so we don't block
task::spawn_local(server);
// Create a new read-only table that contains methods
// for manipulating server behavior and shutting it down
let handle_stop = move |_, _: ()| match shutdown_tx.try_send(()) {
Ok(_) => Ok(()),
Err(_) => Err(LuaError::RuntimeError(
"Server has already been stopped".to_string(),
)),
};
TableBuilder::new(lua)?
.with_function("stop", handle_stop)?
.build_readonly()
}
fn net_url_encode<'a>(
lua: &'static Lua,
(lua_string, as_binary): (LuaString<'a>, Option<bool>),
) -> LuaResult<LuaValue<'a>> {
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<'a>(
lua: &'static Lua,
(lua_string, as_binary): (LuaString<'a>, Option<bool>),
) -> LuaResult<LuaValue<'a>> {
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,296 +0,0 @@
use std::{
collections::HashMap,
env::{self, consts},
path::{self, PathBuf},
process::{ExitCode, Stdio},
};
use directories::UserDirs;
use dunce::canonicalize;
use mlua::prelude::*;
use os_str_bytes::RawOsString;
use tokio::process::Command;
use crate::lune::lua::{
process::pipe_and_inherit_child_process_stdio, table::TableBuilder, task::TaskScheduler,
};
const PROCESS_EXIT_IMPL_LUA: &str = r#"
exit(...)
yield()
"#;
pub fn create(lua: &'static Lua, args_vec: Vec<String>) -> LuaResult<LuaTable> {
let cwd_str = {
let cwd = canonicalize(env::current_dir()?)?;
let cwd_str = cwd.to_string_lossy().to_string();
if !cwd_str.ends_with(path::MAIN_SEPARATOR) {
format!("{cwd_str}{}", path::MAIN_SEPARATOR)
} else {
cwd_str
}
};
// Create constants for OS & processor architecture
let os = lua.create_string(&consts::OS.to_lowercase())?;
let arch = lua.create_string(&consts::ARCH.to_lowercase())?;
// Create readonly args array
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, this is a bit involved since
// we have no way to yield from c / rust, we need to load a lua
// chunk that will set the exit code and yield for us instead
let process_exit_env_yield: LuaFunction = lua.named_registry_value("co.yield")?;
let process_exit_env_exit: LuaFunction = lua.create_function(|lua, code: Option<u8>| {
let exit_code = code.map_or(ExitCode::SUCCESS, ExitCode::from);
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler - make sure it is added as a lua app data before the first scheduler resumption");
sched.set_exit_code(exit_code);
Ok(())
})?;
let process_exit = lua
.load(PROCESS_EXIT_IMPL_LUA)
.set_name("=process.exit")
.set_environment(
TableBuilder::new(lua)?
.with_value("yield", process_exit_env_yield)?
.with_value("exit", process_exit_env_exit)?
.build_readonly()?,
)
.into_function()?;
// Create the full process table
TableBuilder::new(lua)?
.with_value("os", os)?
.with_value("arch", arch)?
.with_value("args", args_tab)?
.with_value("cwd", cwd_str)?
.with_value("env", env_tab)?
.with_value("exit", process_exit)?
.with_async_function("spawn", process_spawn)?
.build_readonly()
}
fn process_env_get<'a>(
lua: &'static Lua,
(_, key): (LuaValue<'a>, String),
) -> LuaResult<LuaValue<'a>> {
match env::var_os(key) {
Some(value) => {
let raw_value = RawOsString::new(value);
Ok(LuaValue::String(
lua.create_string(raw_value.as_raw_bytes())?,
))
}
None => Ok(LuaValue::Nil),
}
}
fn process_env_set(
_: &'static Lua,
(_, key, value): (LuaValue, 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 {
match value {
Some(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(())
}
}
None => {
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.as_raw_bytes())?),
LuaValue::String(lua.create_string(raw_value.as_raw_bytes())?),
))
}
None => Ok((LuaValue::Nil, LuaValue::Nil)),
})
}
async fn process_spawn<'a>(
lua: &'static Lua,
(mut program, args, options): (String, Option<Vec<String>>, Option<LuaTable<'a>>),
) -> LuaResult<LuaTable<'a>> {
// Parse any given options or create defaults
let (child_cwd, child_envs, child_shell, child_stdio_inherit) = match options {
Some(options) => {
let mut cwd = env::current_dir()?;
let mut envs = HashMap::new();
let mut shell = None;
let mut inherit = false;
match options.raw_get("cwd")? {
LuaValue::Nil => {}
LuaValue::String(s) => {
cwd = PathBuf::from(s.to_string_lossy().to_string());
// Substitute leading tilde (~) for the actual home dir
if cwd.starts_with("~") {
if let Some(user_dirs) = UserDirs::new() {
cwd = user_dirs.home_dir().join(cwd.strip_prefix("~").unwrap())
}
};
if !cwd.exists() {
return Err(LuaError::RuntimeError(
"Invalid value for option 'cwd' - path does not exist".to_string(),
));
}
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'cwd' - expected 'string', got '{}'",
value.type_name()
)))
}
}
match options.raw_get("env")? {
LuaValue::Nil => {}
LuaValue::Table(t) => {
for pair in t.pairs::<String, String>() {
let (k, v) = pair?;
envs.insert(k, v);
}
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'env' - expected 'table', got '{}'",
value.type_name()
)))
}
}
match options.raw_get("shell")? {
LuaValue::Nil => {}
LuaValue::String(s) => shell = Some(s.to_string_lossy().to_string()),
LuaValue::Boolean(true) => {
shell = match env::consts::FAMILY {
"unix" => Some("/bin/sh".to_string()),
"windows" => Some("/bin/sh".to_string()),
_ => None,
};
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'shell' - expected 'true' or 'string', got '{}'",
value.type_name()
)))
}
}
match options.raw_get("stdio")? {
LuaValue::Nil => {}
LuaValue::String(s) => {
match s.to_str()? {
"inherit" => {
inherit = true;
},
"default" => {
inherit = false;
}
_ => return Err(LuaError::RuntimeError(
format!("Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'", s.to_string_lossy()),
))
}
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'stdio' - expected 'string', got '{}'",
value.type_name()
)))
}
}
Ok::<_, LuaError>((cwd, envs, shell, inherit))
}
None => Ok((env::current_dir()?, HashMap::new(), None, false)),
}?;
// Run a shell using the command param if wanted
let child_args = if let Some(shell) = child_shell {
let shell_args = match args {
Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))],
None => vec!["-c".to_string(), program],
};
program = shell;
Some(shell_args)
} else {
args
};
// Create command with the wanted options
let mut cmd = match child_args {
None => Command::new(program),
Some(args) => {
let mut cmd = Command::new(program);
cmd.args(args);
cmd
}
};
// Set dir to run in and env variables
cmd.current_dir(child_cwd);
cmd.envs(child_envs);
// Spawn the child process
let child = cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
// Inherit the output and stderr if wanted
let result = if child_stdio_inherit {
pipe_and_inherit_child_process_stdio(child).await
} else {
let output = child.wait_with_output().await?;
Ok((output.status, output.stdout, output.stderr))
};
// Extract result
let (status, stdout, stderr) = result?;
// NOTE: If an exit code was not given by the child process,
// we default to 1 if it yielded any error output, otherwise 0
let code = status.code().unwrap_or(match stderr.is_empty() {
true => 0,
false => 1,
});
// 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(&stdout)?)?
.with_value("stderr", lua.create_string(&stderr)?)?
.build_readonly()
}

View file

@ -1,104 +0,0 @@
use mlua::prelude::*;
use once_cell::sync::OnceCell;
use crate::roblox::{
self,
document::{Document, DocumentError, DocumentFormat, DocumentKind},
instance::Instance,
reflection::Database as ReflectionDatabase,
};
use tokio::task;
use crate::lune::lua::table::TableBuilder;
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
let mut roblox_constants = Vec::new();
let roblox_module = 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)?
.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 = task::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.map_err(LuaError::external)??.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 = task::spawn_blocking(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Model)?;
let instance_array = doc.into_instance_array()?;
Ok::<_, DocumentError>(instance_array)
});
fut.await.map_err(LuaError::external)??.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).clone();
let fut = task::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.map_err(LuaError::external)??;
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).clone()).collect();
let fut = task::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.map_err(LuaError::external)??;
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))
}

View file

@ -1,49 +0,0 @@
use mlua::prelude::*;
use crate::lune::lua::{
serde::{
compress, decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat,
},
table::TableBuilder,
};
pub fn create(lua: &'static 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)?
.build_readonly()
}
fn serde_encode<'a>(
lua: &'static Lua,
(format, val, pretty): (EncodeDecodeFormat, LuaValue<'a>, Option<bool>),
) -> LuaResult<LuaString<'a>> {
let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default()));
config.serialize_to_string(lua, val)
}
fn serde_decode<'a>(
lua: &'static Lua,
(format, str): (EncodeDecodeFormat, LuaString<'a>),
) -> LuaResult<LuaValue<'a>> {
let config = EncodeDecodeConfig::from(format);
config.deserialize_from_string(lua, str)
}
async fn serde_compress<'a>(
lua: &'static Lua,
(format, str): (CompressDecompressFormat, LuaString<'a>),
) -> LuaResult<LuaString<'a>> {
let bytes = compress(format, str).await?;
lua.create_string(bytes)
}
async fn serde_decompress<'a>(
lua: &'static Lua,
(format, str): (CompressDecompressFormat, LuaString<'a>),
) -> LuaResult<LuaString<'a>> {
let bytes = decompress(format, str).await?;
lua.create_string(bytes)
}

View file

@ -1,99 +0,0 @@
use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
use mlua::prelude::*;
use tokio::{
io::{self, AsyncWriteExt},
task,
};
use crate::lune::lua::{
stdio::{
formatting::{
format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str,
},
prompt::{PromptKind, PromptOptions, PromptResult},
},
table::TableBuilder,
};
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("color", |_, color: String| {
let ansi_string = format_style(style_from_color_str(&color)?);
Ok(ansi_string)
})?
.with_function("style", |_, style: String| {
let ansi_string = format_style(style_from_style_str(&style)?);
Ok(ansi_string)
})?
.with_function("format", |_, args: LuaMultiValue| {
pretty_format_multi_value(&args)
})?
.with_async_function("write", |_, s: LuaString| async move {
let mut stdout = io::stdout();
stdout.write_all(s.as_bytes()).await?;
stdout.flush().await?;
Ok(())
})?
.with_async_function("ewrite", |_, s: LuaString| async move {
let mut stderr = io::stderr();
stderr.write_all(s.as_bytes()).await?;
stderr.flush().await?;
Ok(())
})?
.with_async_function("prompt", |_, options: PromptOptions| async move {
task::spawn_blocking(move || prompt(options))
.await
.map_err(LuaError::external)?
})?
.build_readonly()
}
fn prompt_theme() -> ColorfulTheme {
ColorfulTheme::default()
}
fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
let theme = prompt_theme();
match options.kind {
PromptKind::Text => {
let input: String = Input::with_theme(&theme)
.allow_empty(true)
.with_prompt(options.text.unwrap_or_default())
.with_initial_text(options.default_string.unwrap_or_default())
.interact_text()?;
Ok(PromptResult::String(input))
}
PromptKind::Confirm => {
let mut prompt = Confirm::with_theme(&theme);
if let Some(b) = options.default_bool {
prompt.default(b);
};
let result = prompt
.with_prompt(&options.text.expect("Missing text in prompt options"))
.interact()?;
Ok(PromptResult::Boolean(result))
}
PromptKind::Select => {
let chosen = Select::with_theme(&prompt_theme())
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()?;
Ok(match chosen {
Some(idx) => PromptResult::Index(idx + 1),
None => PromptResult::None,
})
}
PromptKind::MultiSelect => {
let chosen = MultiSelect::with_theme(&prompt_theme())
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()?;
Ok(match chosen {
None => PromptResult::None,
Some(indices) => {
PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect())
}
})
}
}
}

View file

@ -1,168 +0,0 @@
use mlua::prelude::*;
use crate::lune::lua::{
async_ext::LuaAsyncExt,
table::TableBuilder,
task::{
LuaThreadOrFunction, LuaThreadOrTaskReference, TaskKind, TaskReference, TaskScheduler,
TaskSchedulerScheduleExt,
},
};
const SPAWN_IMPL_LUA: &str = r#"
scheduleNext(thread())
local task = scheduleNext(...)
yield()
return task
"#;
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable<'static>> {
lua.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler in app data");
/*
1. Schedule the current thread at the front
2. Schedule the wanted task arg at the front,
the previous schedule now comes right after
3. Give control over to the scheduler, which will
resume the above tasks in order when its ready
The spawn function needs special treatment,
we need to yield right away to allow the
spawned task to run until first yield
*/
let task_spawn_env_yield: LuaFunction = lua.named_registry_value("co.yield")?;
let task_spawn = lua
.load(SPAWN_IMPL_LUA)
.set_name("task.spawn")
.set_environment(
TableBuilder::new(lua)?
.with_function("thread", |lua, _: ()| Ok(lua.current_thread()))?
.with_value("yield", task_spawn_env_yield)?
.with_function(
"scheduleNext",
|lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_blocking(tof.into_thread(lua)?, args)
},
)?
.build_readonly()?,
)
.into_function()?;
// Functions in the built-in coroutine library also need to be
// replaced, these are a bit different than the ones above because
// calling resume or the function that wrap returns must return
// whatever lua value(s) that the thread or task yielded back
let globals = lua.globals();
let coroutine = globals.get::<_, LuaTable>("coroutine")?;
coroutine.set("status", lua.create_function(coroutine_status)?)?;
coroutine.set("resume", lua.create_function(coroutine_resume)?)?;
coroutine.set("wrap", lua.create_function(coroutine_wrap)?)?;
// All good, return the task scheduler lib
TableBuilder::new(lua)?
.with_value("wait", lua.create_waiter_function()?)?
.with_value("spawn", task_spawn)?
.with_function("cancel", task_cancel)?
.with_function("defer", task_defer)?
.with_function("delay", task_delay)?
.build_readonly()
}
/*
Basic task functions
*/
fn task_cancel(lua: &Lua, task: LuaUserDataRef<TaskReference>) -> LuaResult<()> {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.remove_task(*task)?;
Ok(())
}
fn task_defer(
lua: &Lua,
(tof, args): (LuaThreadOrFunction, LuaMultiValue),
) -> LuaResult<TaskReference> {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_blocking_deferred(tof.into_thread(lua)?, args)
}
fn task_delay(
lua: &Lua,
(secs, tof, args): (f64, LuaThreadOrFunction, LuaMultiValue),
) -> LuaResult<TaskReference> {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_blocking_after_seconds(secs, tof.into_thread(lua)?, args)
}
/*
Coroutine library overrides for compat with task scheduler
*/
fn coroutine_status<'a>(
lua: &'a Lua,
value: LuaThreadOrTaskReference<'a>,
) -> LuaResult<LuaString<'a>> {
Ok(match value {
LuaThreadOrTaskReference::Thread(thread) => {
let get_status: LuaFunction = lua.named_registry_value("co.status")?;
get_status.call(thread)?
}
LuaThreadOrTaskReference::TaskReference(task) => {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched
.get_task_status(task)
.unwrap_or_else(|| lua.create_string("dead").unwrap())
}
})
}
fn coroutine_resume<'lua>(
lua: &'lua Lua,
value: LuaThreadOrTaskReference,
) -> LuaResult<(bool, LuaMultiValue<'lua>)> {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
if sched.current_task().is_none() {
return Err(LuaError::RuntimeError(
"No current task to inherit".to_string(),
));
}
let current = sched.current_task().unwrap();
let result = match value {
LuaThreadOrTaskReference::Thread(t) => {
let task = sched.create_task(TaskKind::Instant, t, None, true)?;
sched.resume_task(task, None)
}
LuaThreadOrTaskReference::TaskReference(t) => sched.resume_task(t, None),
};
sched.force_set_current_task(Some(current));
match result {
Ok(rets) => Ok((true, rets.1)),
Err(e) => Ok((false, e.into_lua_multi(lua)?)),
}
}
fn coroutine_wrap<'lua>(lua: &'lua Lua, func: LuaFunction) -> LuaResult<LuaFunction<'lua>> {
let task = lua.app_data_ref::<&TaskScheduler>().unwrap().create_task(
TaskKind::Instant,
lua.create_thread(func)?,
None,
false,
)?;
lua.create_function(move |lua, args: LuaMultiValue| {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
if sched.current_task().is_none() {
return Err(LuaError::RuntimeError(
"No current task to inherit".to_string(),
));
}
let current = sched.current_task().unwrap();
let result = lua
.app_data_ref::<&TaskScheduler>()
.unwrap()
.resume_task(task, Some(Ok(args)));
sched.force_set_current_task(Some(current));
match result {
Ok(rets) => Ok(rets.1),
Err(e) => Err(e),
}
})
}

View file

@ -1,80 +0,0 @@
use mlua::prelude::*;
use std::io::{self, Write as _};
#[cfg(feature = "roblox")]
use crate::roblox::datatypes::extension::RobloxUserdataTypenameExt;
use crate::lune::lua::{
stdio::formatting::{format_label, pretty_format_multi_value},
task::TaskReference,
};
pub fn print(_: &Lua, args: LuaMultiValue) -> LuaResult<()> {
let formatted = format!("{}\n", pretty_format_multi_value(&args)?);
let mut stdout = io::stdout();
stdout.write_all(formatted.as_bytes())?;
stdout.flush()?;
Ok(())
}
pub fn warn(_: &Lua, args: LuaMultiValue) -> LuaResult<()> {
let formatted = format!(
"{}\n{}",
format_label("warn"),
pretty_format_multi_value(&args)?
);
let mut stdout = io::stdout();
stdout.write_all(formatted.as_bytes())?;
stdout.flush()?;
Ok(())
}
// HACK: We need to preserve the default behavior of
// the lua error function, for pcall and such, which
// is really tricky to do from scratch so we will
// just proxy the default function here instead
pub fn error(lua: &Lua, (arg, level): (LuaValue, Option<u32>)) -> LuaResult<()> {
let error: LuaFunction = lua.named_registry_value("error")?;
let trace: LuaFunction = lua.named_registry_value("dbg.trace")?;
error.call((
LuaError::CallbackError {
traceback: format!("override traceback:{}", trace.call::<_, String>(())?),
cause: LuaError::external(format!(
"{}\n{}",
format_label("error"),
pretty_format_multi_value(&arg.into_lua_multi(lua)?)?
))
.into(),
},
level,
))?;
Ok(())
}
pub fn proxy_type<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult<LuaString<'lua>> {
if let LuaValue::UserData(u) = &value {
if u.is::<TaskReference>() {
return lua.create_string("thread");
}
}
lua.named_registry_value::<LuaFunction>("type")?.call(value)
}
pub fn proxy_typeof<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult<LuaString<'lua>> {
if let LuaValue::UserData(u) = &value {
if u.is::<TaskReference>() {
return lua.create_string("thread");
}
#[cfg(feature = "roblox")]
{
if let Some(type_name) = u.roblox_type_name() {
return lua.create_string(type_name);
}
}
}
lua.named_registry_value::<LuaFunction>("typeof")?
.call(value)
}
// TODO: Add an override for tostring that formats errors in a nicer way

View file

@ -1,39 +0,0 @@
use std::{
error::Error,
fmt::{Debug, Display, Formatter, Result as FmtResult},
};
use mlua::prelude::*;
use crate::lune::lua::stdio::formatting::pretty_format_luau_error;
/**
An opaque error type for formatted lua errors.
*/
#[derive(Debug, Clone)]
pub struct LuneError {
message: String,
}
#[allow(dead_code)]
impl LuneError {
pub(crate) fn new(message: String) -> Self {
Self { message }
}
pub(crate) fn from_lua_error(error: LuaError) -> Self {
Self::new(pretty_format_luau_error(&error, true))
}
pub(crate) fn from_lua_error_plain(error: LuaError) -> Self {
Self::new(pretty_format_luau_error(&error, false))
}
}
impl Display for LuneError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.message)
}
}
impl Error for LuneError {}

View file

@ -1,41 +0,0 @@
use mlua::prelude::*;
mod require;
mod require_waker;
use crate::lune::builtins::{self, top_level};
pub fn create(lua: &'static Lua, args: Vec<String>) -> LuaResult<()> {
// Create all builtins
let builtins = vec![
("fs", builtins::fs::create(lua)?),
("net", builtins::net::create(lua)?),
("process", builtins::process::create(lua, args)?),
("serde", builtins::serde::create(lua)?),
("stdio", builtins::stdio::create(lua)?),
("task", builtins::task::create(lua)?),
#[cfg(feature = "roblox")]
("roblox", builtins::roblox::create(lua)?),
];
// Create our importer (require) with builtins
let require_fn = require::create(lua, builtins)?;
// Create all top-level globals
let globals = vec![
("require", require_fn),
("print", lua.create_function(top_level::print)?),
("warn", lua.create_function(top_level::warn)?),
("error", lua.create_function(top_level::error)?),
("type", lua.create_function(top_level::proxy_type)?),
("typeof", lua.create_function(top_level::proxy_typeof)?),
];
// Set top-level globals
let lua_globals = lua.globals();
for (name, global) in globals {
lua_globals.set(name, global)?;
}
Ok(())
}

View file

@ -1,305 +0,0 @@
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
env::current_dir,
path::{self, PathBuf},
sync::Arc,
};
use dunce::canonicalize;
use mlua::{prelude::*, Compiler as LuaCompiler};
use tokio::fs;
use tokio::sync::Mutex as AsyncMutex;
use crate::lune::lua::{
table::TableBuilder,
task::{TaskScheduler, TaskSchedulerScheduleExt},
};
use super::require_waker::{RequireWakerFuture, RequireWakerState};
const REQUIRE_IMPL_LUA: &str = r#"
local source = info(1, "s")
if source == '[string "require"]' then
source = info(2, "s")
end
load(context, source, ...)
return yield()
"#;
type RequireWakersVec<'lua> = Vec<Arc<AsyncMutex<RequireWakerState<'lua>>>>;
#[derive(Debug, Clone, Default)]
struct RequireContext<'lua> {
// NOTE: We need to use arc here so that mlua clones
// the reference and not the entire inner value(s)
builtins: Arc<HashMap<String, LuaMultiValue<'lua>>>,
cached: Arc<RefCell<HashMap<String, LuaResult<LuaMultiValue<'lua>>>>>,
wakers: Arc<RefCell<HashMap<String, RequireWakersVec<'lua>>>>,
locks: Arc<RefCell<HashSet<String>>>,
pwd: String,
}
impl<'lua> RequireContext<'lua> {
pub fn new<K, V>(lua: &'lua Lua, builtins_vec: Vec<(K, V)>) -> LuaResult<Self>
where
K: Into<String>,
V: IntoLua<'lua>,
{
let mut pwd = current_dir()
.expect("Failed to access current working directory")
.to_string_lossy()
.to_string();
if !pwd.ends_with(path::MAIN_SEPARATOR) {
pwd = format!("{pwd}{}", path::MAIN_SEPARATOR)
}
let mut builtins = HashMap::new();
for (key, value) in builtins_vec {
builtins.insert(key.into(), value.into_lua_multi(lua)?);
}
Ok(Self {
pwd,
builtins: Arc::new(builtins),
..Default::default()
})
}
pub fn is_locked(&self, absolute_path: &str) -> bool {
self.locks.borrow().contains(absolute_path)
}
pub fn set_locked(&self, absolute_path: &str) -> bool {
self.locks.borrow_mut().insert(absolute_path.to_string())
}
pub fn set_unlocked(&self, absolute_path: &str) -> bool {
self.locks.borrow_mut().remove(absolute_path)
}
pub fn try_acquire_lock_sync(&self, absolute_path: &str) -> bool {
if self.is_locked(absolute_path) {
false
} else {
self.set_locked(absolute_path);
true
}
}
pub fn set_cached(&self, absolute_path: &str, result: &LuaResult<LuaMultiValue<'lua>>) {
self.cached
.borrow_mut()
.insert(absolute_path.to_string(), result.clone());
if let Some(wakers) = self.wakers.borrow_mut().remove(absolute_path) {
for waker in wakers {
waker
.try_lock()
.expect("Failed to lock waker")
.finalize(result.clone());
}
}
}
pub fn wait_for_cache(self, absolute_path: &str) -> RequireWakerFuture<'lua> {
let state = RequireWakerState::new();
let fut = RequireWakerFuture::new(&state);
self.wakers
.borrow_mut()
.entry(absolute_path.to_string())
.or_insert_with(Vec::new)
.push(Arc::clone(&state));
fut
}
pub fn get_paths(
&self,
require_source: String,
require_path: String,
) -> LuaResult<(String, String)> {
if require_path.starts_with('@') {
return Ok((require_path.clone(), require_path));
}
let path_relative_to_pwd = PathBuf::from(
&require_source
.trim_start_matches("[string \"")
.trim_end_matches("\"]"),
)
.parent()
.unwrap()
.join(&require_path);
// Try to normalize and resolve relative path segments such as './' and '../'
let file_path = match (
canonicalize(path_relative_to_pwd.with_extension("luau")),
canonicalize(path_relative_to_pwd.with_extension("lua")),
) {
(Ok(luau), _) => luau,
(_, Ok(lua)) => lua,
// If we did not find a luau/lua file at the wanted path,
// we should also look for "init" files in directories
_ => match (
canonicalize(path_relative_to_pwd.join("init").with_extension("luau")),
canonicalize(path_relative_to_pwd.join("init").with_extension("lua")),
) {
(Ok(luau), _) => luau,
(_, Ok(lua)) => lua,
_ => {
return Err(LuaError::RuntimeError(format!(
"File does not exist at path '{require_path}'"
)))
}
},
};
let absolute = file_path.to_string_lossy().to_string();
let relative = absolute.trim_start_matches(&self.pwd).to_string();
Ok((absolute, relative))
}
}
impl<'lua> LuaUserData for RequireContext<'lua> {}
fn load_builtin<'lua>(
_lua: &'lua Lua,
context: RequireContext<'lua>,
module_name: String,
_has_acquired_lock: bool,
) -> LuaResult<LuaMultiValue<'lua>> {
match context.builtins.get(&module_name) {
Some(module) => Ok(module.clone()),
None => Err(LuaError::RuntimeError(format!(
"No builtin module exists with the name '{}'",
module_name
))),
}
}
async fn load_file<'lua>(
lua: &'lua Lua,
context: RequireContext<'lua>,
absolute_path: String,
relative_path: String,
has_acquired_lock: bool,
) -> LuaResult<LuaMultiValue<'lua>> {
let cached = { context.cached.borrow().get(&absolute_path).cloned() };
match cached {
Some(cached) => cached,
None => {
if !has_acquired_lock {
return context.wait_for_cache(&absolute_path).await;
}
// Try to read the wanted file, note that we use bytes instead of reading
// to a string since lua scripts are not necessarily valid utf-8 strings
let contents = fs::read(&absolute_path).await.map_err(LuaError::external)?;
// Use a name without extensions for loading the chunk, some
// other code assumes the require path is without extensions
let path_relative_no_extension = relative_path
.trim_end_matches(".lua")
.trim_end_matches(".luau");
// Load the file into a thread
let compiled_func = LuaCompiler::default().compile(&contents);
let loaded_func = lua
.load(compiled_func)
.set_name(path_relative_no_extension)
.into_function()?;
let loaded_thread = lua.create_thread(loaded_func)?;
// Run the thread and wait for completion using the native task scheduler waker
let task_fut = {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
let task = sched.schedule_blocking(loaded_thread, LuaMultiValue::new())?;
sched.wait_for_task_completion(task)
};
// Wait for the thread to finish running, cache + return our result,
// notify any other threads that are also waiting on this to finish
let rets = task_fut.await;
context.set_cached(&absolute_path, &rets);
rets
}
}
}
async fn load<'lua>(
lua: &'lua Lua,
context: LuaUserDataRef<'lua, RequireContext<'lua>>,
absolute_path: String,
relative_path: String,
has_acquired_lock: bool,
) -> LuaResult<LuaMultiValue<'lua>> {
let result = if absolute_path == relative_path && absolute_path.starts_with('@') {
if let Some(module_name) = absolute_path.strip_prefix("@lune/") {
load_builtin(
lua,
context.clone(),
module_name.to_string(),
has_acquired_lock,
)
} else {
// FUTURE: '@' can be used a special prefix for users to set their own
// paths relative to a project file, similar to typescript paths config
// https://www.typescriptlang.org/tsconfig#paths
Err(LuaError::RuntimeError(
"Require paths prefixed by '@' are not yet supported".to_string(),
))
}
} else {
load_file(
lua,
context.clone(),
absolute_path.to_string(),
relative_path,
has_acquired_lock,
)
.await
};
if has_acquired_lock {
context.set_unlocked(&absolute_path);
}
result
}
pub fn create<K, V>(lua: &'static Lua, builtins: Vec<(K, V)>) -> LuaResult<LuaFunction>
where
K: Clone + Into<String>,
V: Clone + IntoLua<'static>,
{
let require_context = RequireContext::new(lua, builtins)?;
let require_yield: LuaFunction = lua.named_registry_value("co.yield")?;
let require_info: LuaFunction = lua.named_registry_value("dbg.info")?;
let require_print: LuaFunction = lua.named_registry_value("print")?;
let require_env = TableBuilder::new(lua)?
.with_value("context", require_context)?
.with_value("yield", require_yield)?
.with_value("info", require_info)?
.with_value("print", require_print)?
.with_function(
"load",
|lua,
(context, require_source, require_path): (
LuaUserDataRef<RequireContext>,
String,
String,
)| {
let (absolute_path, relative_path) =
context.get_paths(require_source, require_path)?;
// NOTE: We can not acquire the lock in the async part of the require
// load process since several requires may have happened for the
// same path before the async load task even gets a chance to run
let has_lock = context.try_acquire_lock_sync(&absolute_path);
let fut = load(lua, context, absolute_path, relative_path, has_lock);
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler as a lua app data");
sched.queue_async_task_inherited(lua.current_thread(), None, async {
let rets = fut.await?;
let mult = rets.into_lua_multi(lua)?;
Ok(Some(mult))
})
},
)?
.build_readonly()?;
let require_fn_lua = lua
.load(REQUIRE_IMPL_LUA)
.set_name("require")
.set_environment(require_env)
.into_function()?;
Ok(require_fn_lua)
}

View file

@ -1,66 +0,0 @@
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll, Waker},
};
use tokio::sync::Mutex as AsyncMutex;
use mlua::prelude::*;
#[derive(Debug, Clone)]
pub(super) struct RequireWakerState<'lua> {
rets: Option<LuaResult<LuaMultiValue<'lua>>>,
waker: Option<Waker>,
}
impl<'lua> RequireWakerState<'lua> {
pub fn new() -> Arc<AsyncMutex<Self>> {
Arc::new(AsyncMutex::new(RequireWakerState {
rets: None,
waker: None,
}))
}
pub fn finalize(&mut self, rets: LuaResult<LuaMultiValue<'lua>>) {
self.rets = Some(rets);
if let Some(waker) = self.waker.take() {
waker.wake();
}
}
}
#[derive(Debug)]
pub(super) struct RequireWakerFuture<'lua> {
state: Arc<AsyncMutex<RequireWakerState<'lua>>>,
}
impl<'lua> RequireWakerFuture<'lua> {
pub fn new(state: &Arc<AsyncMutex<RequireWakerState<'lua>>>) -> Self {
Self {
state: Arc::clone(state),
}
}
}
impl<'lua> Clone for RequireWakerFuture<'lua> {
fn clone(&self) -> Self {
Self {
state: Arc::clone(&self.state),
}
}
}
impl<'lua> Future for RequireWakerFuture<'lua> {
type Output = LuaResult<LuaMultiValue<'lua>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut shared_state = self.state.try_lock().unwrap();
if let Some(rets) = shared_state.rets.clone() {
Poll::Ready(rets)
} else {
shared_state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}

View file

@ -1,93 +0,0 @@
use async_trait::async_trait;
use futures_util::Future;
use mlua::prelude::*;
use crate::lune::{lua::table::TableBuilder, lua::task::TaskScheduler};
use super::task::TaskSchedulerAsyncExt;
const ASYNC_IMPL_LUA: &str = r#"
resumeAsync(...)
return yield()
"#;
const WAIT_IMPL_LUA: &str = r#"
resumeAfter(...)
return yield()
"#;
#[async_trait(?Send)]
pub trait LuaAsyncExt {
fn create_async_function<'lua, A, R, F, FR>(self, func: F) -> LuaResult<LuaFunction<'lua>>
where
A: FromLuaMulti<'static>,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'lua Lua, A) -> FR,
FR: 'static + Future<Output = LuaResult<R>>;
fn create_waiter_function<'lua>(self) -> LuaResult<LuaFunction<'lua>>;
}
impl LuaAsyncExt for &'static Lua {
/**
Creates a function callable from Lua that runs an async
closure and returns the results of it to the call site.
*/
fn create_async_function<'lua, A, R, F, FR>(self, func: F) -> LuaResult<LuaFunction<'lua>>
where
A: FromLuaMulti<'static>,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'lua Lua, A) -> FR,
FR: 'static + Future<Output = LuaResult<R>>,
{
let async_env_yield: LuaFunction = self.named_registry_value("co.yield")?;
let async_env = TableBuilder::new(self)?
.with_value("yield", async_env_yield)?
.with_function("resumeAsync", move |lua: &Lua, args: A| {
let thread = lua.current_thread();
let fut = func(lua, args);
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler as a lua app data");
sched.queue_async_task(thread, None, async {
let rets = fut.await?;
let mult = rets.into_lua_multi(lua)?;
Ok(Some(mult))
})
})?
.build_readonly()?;
let async_func = self
.load(ASYNC_IMPL_LUA)
.set_name("async")
.set_environment(async_env)
.into_function()?;
Ok(async_func)
}
/**
Creates a special async function that waits the
desired amount of time, inheriting the guid of the
current thread / task for proper cancellation.
This will yield the lua thread calling the function until the
desired time has passed and the scheduler resumes the thread.
*/
fn create_waiter_function<'lua>(self) -> LuaResult<LuaFunction<'lua>> {
let async_env_yield: LuaFunction = self.named_registry_value("co.yield")?;
let async_env = TableBuilder::new(self)?
.with_value("yield", async_env_yield)?
.with_function("resumeAfter", move |lua: &Lua, duration: Option<f64>| {
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler as a lua app data");
sched.schedule_wait(lua.current_thread(), duration)
})?
.build_readonly()?;
let async_func = self
.load(WAIT_IMPL_LUA)
.set_name("wait")
.set_environment(async_env)
.into_function()?;
Ok(async_func)
}
}

View file

@ -1,147 +0,0 @@
use mlua::prelude::*;
/*
- Level 0 is the call to info
- Level 1 is the load call in create() below where we load this into a function
- Level 2 is the call to the trace, which we also want to skip, so start at 3
Also note that we must match the mlua traceback format here so that we
can pattern match and beautify it properly later on when outputting it
*/
const TRACE_IMPL_LUA: &str = r#"
local lines = {}
for level = 3, 16 do
local parts = {}
local source, line, name = info(level, "sln")
if source then
push(parts, source)
else
break
end
if line == -1 then
line = nil
end
if name and #name <= 0 then
name = nil
end
if line then
push(parts, format("%d", line))
end
if name and #parts > 1 then
push(parts, format(" in function '%s'", name))
elseif name then
push(parts, format("in function '%s'", name))
end
if #parts > 0 then
push(lines, concat(parts, ":"))
end
end
if #lines > 0 then
return concat(lines, "\n")
else
return nil
end
"#;
/**
Creates a [`mlua::Lua`] object with certain globals stored in the Lua registry.
These globals can then be modified safely after constructing Lua using this function.
---
* `"print"` -> `print`
* `"error"` -> `error`
---
* `"type"` -> `type`
* `"typeof"` -> `typeof`
---
* `"pcall"` -> `pcall`
* `"xpcall"` -> `xpcall`
---
* `"tostring"` -> `tostring`
* `"tonumber"` -> `tonumber`
---
* `"co.yield"` -> `coroutine.yield`
* `"co.close"` -> `coroutine.close`
---
* `"tab.pack"` -> `table.pack`
* `"tab.unpack"` -> `table.unpack`
* `"tab.freeze"` -> `table.freeze`
* `"tab.getmeta"` -> `getmetatable`
* `"tab.setmeta"` -> `setmetatable`
---
* `"dbg.info"` -> `debug.info`
* `"dbg.trace"` -> `debug.traceback`
---
*/
pub fn create() -> LuaResult<&'static Lua> {
let lua = Lua::new().into_static();
let globals = &lua.globals();
let debug: LuaTable = globals.raw_get("debug")?;
let table: LuaTable = globals.raw_get("table")?;
let string: LuaTable = globals.raw_get("string")?;
let coroutine: LuaTable = globals.get("coroutine")?;
// Create a _G table that is separate from our built-in globals
let global_table = lua.create_table()?;
globals.set("_G", global_table)?;
// Store original lua global functions in the registry so we can use
// them later without passing them around and dealing with lifetimes
lua.set_named_registry_value("print", globals.get::<_, LuaFunction>("print")?)?;
lua.set_named_registry_value("error", globals.get::<_, LuaFunction>("error")?)?;
lua.set_named_registry_value("type", globals.get::<_, LuaFunction>("type")?)?;
lua.set_named_registry_value("typeof", globals.get::<_, LuaFunction>("typeof")?)?;
lua.set_named_registry_value("xpcall", globals.get::<_, LuaFunction>("xpcall")?)?;
lua.set_named_registry_value("pcall", globals.get::<_, LuaFunction>("pcall")?)?;
lua.set_named_registry_value("tostring", globals.get::<_, LuaFunction>("tostring")?)?;
lua.set_named_registry_value("tonumber", globals.get::<_, LuaFunction>("tonumber")?)?;
lua.set_named_registry_value("co.status", coroutine.get::<_, LuaFunction>("status")?)?;
lua.set_named_registry_value("co.yield", coroutine.get::<_, LuaFunction>("yield")?)?;
lua.set_named_registry_value("co.close", coroutine.get::<_, LuaFunction>("close")?)?;
lua.set_named_registry_value("dbg.info", debug.get::<_, LuaFunction>("info")?)?;
lua.set_named_registry_value("tab.pack", table.get::<_, LuaFunction>("pack")?)?;
lua.set_named_registry_value("tab.unpack", table.get::<_, LuaFunction>("unpack")?)?;
lua.set_named_registry_value("tab.freeze", table.get::<_, LuaFunction>("freeze")?)?;
lua.set_named_registry_value(
"tab.getmeta",
globals.get::<_, LuaFunction>("getmetatable")?,
)?;
lua.set_named_registry_value(
"tab.setmeta",
globals.get::<_, LuaFunction>("setmetatable")?,
)?;
// Create a trace function that can be called to obtain a full stack trace from
// lua, this is not possible to do from rust when using our manual scheduler
let dbg_trace_env = lua.create_table_with_capacity(0, 1)?;
dbg_trace_env.set("info", debug.get::<_, LuaFunction>("info")?)?;
dbg_trace_env.set("push", table.get::<_, LuaFunction>("insert")?)?;
dbg_trace_env.set("concat", table.get::<_, LuaFunction>("concat")?)?;
dbg_trace_env.set("format", string.get::<_, LuaFunction>("format")?)?;
let dbg_trace_fn = lua
.load(TRACE_IMPL_LUA)
.set_name("=dbg.trace")
.set_environment(dbg_trace_env)
.into_function()?;
lua.set_named_registry_value("dbg.trace", dbg_trace_fn)?;
// Modify the _VERSION global to also contain the current version of Lune
let luau_version_full = globals
.get::<_, LuaString>("_VERSION")
.expect("Missing _VERSION global");
let luau_version = luau_version_full
.to_str()?
.strip_prefix("Luau 0.")
.expect("_VERSION global is formatted incorrectly")
.trim();
if luau_version.is_empty() {
panic!("_VERSION global is missing version number")
}
globals.set(
"_VERSION",
lua.create_string(&format!(
"Lune {lune}+{luau}",
lune = env!("CARGO_PKG_VERSION"),
luau = luau_version,
))?,
)?;
// All done
Ok(lua)
}

View file

@ -1,155 +0,0 @@
use std::collections::VecDeque;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use mlua::prelude::*;
use tokio::fs;
use super::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, _options: 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 dirs.iter_mut() {
*dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf()
}
for (_, file) in files.iter_mut() {
*file = file.strip_prefix(&normalized_root).unwrap().to_path_buf()
}
// 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 {
fs::remove_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,153 +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::*;
#[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<f64>,
pub(crate) modified_at: Option<f64>,
pub(crate) accessed_at: Option<f64>,
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, 5)?;
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,
// FUTURE: Turn these into DateTime structs instead when that's implemented
created_at: system_time_to_timestamp(value.created()),
modified_at: system_time_to_timestamp(value.modified()),
accessed_at: system_time_to_timestamp(value.accessed()),
permissions: Some(FsPermissions::from(value.permissions())),
}
}
}
fn system_time_to_timestamp(res: IoResult<SystemTime>) -> Option<f64> {
match res {
Ok(t) => match t.duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => Some(d.as_secs_f64()),
Err(_) => None,
},
Err(_) => None,
}
}

View file

@ -1,7 +0,0 @@
mod copy;
mod metadata;
mod options;
pub use copy::copy;
pub use metadata::FsMetadata;
pub use options::FsWriteOptions;

View file

@ -1,31 +0,0 @@
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub struct FsWriteOptions {
pub(crate) overwrite: bool,
}
impl<'lua> FromLua<'lua> for FsWriteOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
Ok(match value {
LuaValue::Nil => Self { overwrite: false },
LuaValue::Boolean(b) => Self { overwrite: b },
LuaValue::Table(t) => {
let overwrite: Option<bool> = t.get("overwrite")?;
Self {
overwrite: overwrite.unwrap_or(false),
}
}
_ => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "FsWriteOptions",
message: Some(format!(
"Invalid write options - expected boolean or table, got {}",
value.type_name()
)),
})
}
})
}
}

View file

@ -1,12 +0,0 @@
mod create;
pub mod async_ext;
pub mod fs;
pub mod net;
pub mod process;
pub mod serde;
pub mod stdio;
pub mod table;
pub mod task;
pub use create::create as create_lune_lua;

View file

@ -1,49 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
use hyper::{header::HeaderName, http::HeaderValue, HeaderMap};
use reqwest::{IntoUrl, Method, RequestBuilder};
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()).map_err(LuaError::external)?;
let hval = HeaderValue::from_bytes(val.as_ref()).map_err(LuaError::external)?;
map.insert(hkey, hval);
}
self.builder = self.builder.default_headers(map);
Ok(self)
}
pub fn build(self) -> LuaResult<NetClient> {
let client = self.builder.build().map_err(LuaError::external)?;
Ok(NetClient(client))
}
}
#[derive(Debug, Clone)]
pub struct NetClient(reqwest::Client);
impl NetClient {
pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
self.0.request(method, url)
}
}
impl LuaUserData for NetClient {}

View file

@ -1,204 +0,0 @@
use std::collections::HashMap;
use mlua::prelude::*;
use reqwest::Method;
// 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> {
// Nil means default options, table means custom options
if let LuaValue::Nil = value {
return Ok(Self::default());
} else if let LuaValue::Table(tab) = value {
// Extract flags
let decompress = match tab.raw_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(),
)),
}?;
return Ok(Self { decompress });
}
// 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<'a> {
pub url: String,
pub method: Method,
pub query: HashMap<LuaString<'a>, LuaString<'a>>,
pub headers: HashMap<LuaString<'a>, LuaString<'a>>,
pub body: Option<Vec<u8>>,
pub options: RequestConfigOptions,
}
impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
fn from_lua(value: LuaValue<'lua>, lua: &'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 {
return Ok(Self {
url: s.to_string_lossy().to_string(),
method: Method::GET,
query: HashMap::new(),
headers: HashMap::new(),
body: None,
options: Default::default(),
});
}
// If we got a table we are able to configure the entire request
if let LuaValue::Table(tab) = value {
// Extract url
let url = match tab.raw_get::<_, LuaString>("url") {
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
Err(_) => Err(LuaError::RuntimeError(
"Missing 'url' in request config".to_string(),
)),
}?;
// Extract method
let method = match tab.raw_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.raw_get::<_, LuaTable>("query") {
Ok(config_headers) => {
let mut lua_headers = HashMap::new();
for pair in config_headers.pairs::<LuaString, LuaString>() {
let (key, value) = pair?.to_owned();
lua_headers.insert(key, value);
}
lua_headers
}
Err(_) => HashMap::new(),
};
// Extract headers
let headers = match tab.raw_get::<_, LuaTable>("headers") {
Ok(config_headers) => {
let mut lua_headers = HashMap::new();
for pair in config_headers.pairs::<LuaString, LuaString>() {
let (key, value) = pair?.to_owned();
lua_headers.insert(key, value);
}
lua_headers
}
Err(_) => HashMap::new(),
};
// Extract body
let body = match tab.raw_get::<_, LuaString>("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.raw_get::<_, LuaValue>("options") {
Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?,
Err(_) => RequestConfigOptions::default(),
};
// All good, validated and we got what we need
return Ok(Self {
url,
method,
query,
headers,
body,
options,
});
};
// 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
pub struct ServeConfig<'a> {
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> {
let message = match &value {
LuaValue::Function(f) => {
return Ok(ServeConfig {
handle_request: f.clone(),
handle_web_socket: None,
})
}
LuaValue::Table(t) => {
let handle_request: Option<LuaFunction> = t.raw_get("handleRequest")?;
let handle_web_socket: Option<LuaFunction> = t.raw_get("handleWebSocket")?;
if handle_request.is_some() || handle_web_socket.is_some() {
return Ok(ServeConfig {
handle_request: handle_request.unwrap_or_else(|| {
let chunk = r#"
return {
status = 426,
body = "Upgrade Required",
headers = {
Upgrade = "websocket",
},
}
"#;
lua.load(chunk)
.into_function()
.expect("Failed to create default http responder function")
}),
handle_web_socket,
});
} else {
Some("Missing handleRequest and / or handleWebSocket".to_string())
}
}
_ => None,
};
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig",
message,
})
}
}

View file

@ -1,11 +0,0 @@
mod client;
mod config;
mod response;
mod server;
mod websocket;
pub use client::{NetClient, NetClientBuilder};
pub use config::{RequestConfig, ServeConfig};
pub use response::{NetServeResponse, NetServeResponseKind};
pub use server::{NetLocalExec, NetService};
pub use websocket::NetWebSocket;

View file

@ -1,106 +0,0 @@
use std::collections::HashMap;
use hyper::{Body, Response};
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub enum NetServeResponseKind {
PlainText,
Table,
}
#[derive(Debug, Clone)]
pub struct NetServeResponse {
kind: NetServeResponseKind,
status: u16,
headers: HashMap<String, Vec<u8>>,
body: Option<Vec<u8>>,
}
impl NetServeResponse {
pub fn into_response(self) -> LuaResult<Response<Body>> {
Ok(match self.kind {
NetServeResponseKind::PlainText => Response::builder()
.status(200)
.header("Content-Type", "text/plain")
.body(Body::from(self.body.unwrap()))
.map_err(LuaError::external)?,
NetServeResponseKind::Table => {
let mut response = Response::builder();
for (key, value) in self.headers {
response = response.header(&key, value);
}
response
.status(self.status)
.body(Body::from(self.body.unwrap_or_default()))
.map_err(LuaError::external)?
}
})
}
}
impl<'lua> FromLua<'lua> for NetServeResponse {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
// Plain strings from the handler are plaintext responses
LuaValue::String(s) => Ok(Self {
kind: NetServeResponseKind::PlainText,
status: 200,
headers: HashMap::new(),
body: Some(s.as_bytes().to_vec()),
}),
// Tables are more detailed responses with potential status, headers, body
LuaValue::Table(t) => {
let status: Option<u16> = t.get("status")?;
let headers: Option<LuaTable> = t.get("headers")?;
let body: Option<LuaString> = t.get("body")?;
let mut headers_map = HashMap::new();
if let Some(headers) = headers {
for pair in headers.pairs::<String, LuaString>() {
let (h, v) = pair?;
headers_map.insert(h, v.as_bytes().to_vec());
}
}
let body_bytes = body.map(|s| s.as_bytes().to_vec());
Ok(Self {
kind: NetServeResponseKind::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,
}),
}
}
}
impl<'lua> IntoLua<'lua> for NetServeResponse {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
if self.headers.len() > i32::MAX as usize {
return Err(LuaError::ToLuaConversionError {
from: "NetServeResponse",
to: "table",
message: Some("Too many header values".to_string()),
});
}
let body = self.body.map(|b| lua.create_string(b)).transpose()?;
let headers = lua.create_table_with_capacity(0, self.headers.len())?;
for (key, value) in self.headers {
headers.set(key, lua.create_string(&value)?)?;
}
let table = lua.create_table_with_capacity(0, 3)?;
table.set("status", self.status)?;
table.set("headers", headers)?;
table.set("body", body)?;
table.set_readonly(true);
Ok(LuaValue::Table(table))
}
}

View file

@ -1,178 +0,0 @@
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use mlua::prelude::*;
use hyper::{body::to_bytes, server::conn::AddrStream, service::Service};
use hyper::{Body, Request, Response};
use hyper_tungstenite::{is_upgrade_request as is_ws_upgrade_request, upgrade as ws_upgrade};
use tokio::task;
use crate::lune::{
lua::table::TableBuilder,
lua::task::{TaskScheduler, TaskSchedulerAsyncExt, TaskSchedulerScheduleExt},
};
use super::{NetServeResponse, NetWebSocket};
// Hyper service implementation for net, lots of boilerplate here
// but make_svc and make_svc_function do not work for what we need
pub struct NetServiceInner(
&'static Lua,
Arc<LuaRegistryKey>,
Arc<Option<LuaRegistryKey>>,
);
impl Service<Request<Body>> for NetServiceInner {
type Response = Response<Body>;
type Error = LuaError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, mut req: Request<Body>) -> Self::Future {
let lua = self.0;
if self.2.is_some() && is_ws_upgrade_request(&req) {
// Websocket upgrade request + websocket handler exists,
// we should now upgrade this connection to a websocket
// and then call our handler with a new socket object
let kopt = self.2.clone();
let key = kopt.as_ref().as_ref().unwrap();
let handler: LuaFunction = lua.registry_value(key).expect("Missing websocket handler");
let (response, ws) = ws_upgrade(&mut req, None).expect("Failed to upgrade websocket");
// This should be spawned as a registered task, otherwise
// the scheduler may exit early and cancel this even though what
// we want here is a long-running task that keeps the program alive
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler");
let task = sched.register_background_task();
task::spawn_local(async move {
// Create our new full websocket object, then
// schedule our handler to get called asap
let ws = ws.await.map_err(LuaError::external)?;
let sock = NetWebSocket::new(ws).into_lua_table(lua)?;
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler");
let result = sched.schedule_blocking(
lua.create_thread(handler)?,
LuaMultiValue::from_vec(vec![LuaValue::Table(sock)]),
);
task.unregister(Ok(()));
result
});
Box::pin(async move { Ok(response) })
} else {
// Got a normal http request or no websocket handler
// exists, just call the http request handler
let key = self.1.clone();
let (parts, body) = req.into_parts();
Box::pin(async move {
// Convert request body into bytes, extract handler
let bytes = to_bytes(body).await.map_err(LuaError::external)?;
let handler: LuaFunction = lua.registry_value(&key)?;
// Create a readonly table for the request query params
let query_params = TableBuilder::new(lua)?
.with_values(
parts
.uri
.query()
.unwrap_or_default()
.split('&')
.filter_map(|q| q.split_once('='))
.collect(),
)?
.build_readonly()?;
// Do the same for headers
let header_map = TableBuilder::new(lua)?
.with_values(
parts
.headers
.iter()
.map(|(name, value)| {
(name.to_string(), value.to_str().unwrap().to_string())
})
.collect(),
)?
.build_readonly()?;
// Create a readonly table with request info to pass to the handler
let request = TableBuilder::new(lua)?
.with_value("path", parts.uri.path())?
.with_value("query", query_params)?
.with_value("method", parts.method.as_str())?
.with_value("headers", header_map)?
.with_value("body", lua.create_string(&bytes)?)?
.build_readonly()?;
let response: LuaResult<NetServeResponse> = handler.call(request);
// Send below errors to task scheduler so that they can emit properly
let lua_error = match response {
Ok(r) => match r.into_response() {
Ok(res) => return Ok(res),
Err(err) => err,
},
Err(err) => err,
};
lua.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler")
.forward_lua_error(lua_error);
Ok(Response::builder()
.status(500)
.body(Body::from("Internal Server Error"))
.unwrap())
})
}
}
}
pub struct NetService(
&'static Lua,
Arc<LuaRegistryKey>,
Arc<Option<LuaRegistryKey>>,
);
impl NetService {
pub fn new(
lua: &'static Lua,
callback_http: LuaRegistryKey,
callback_websocket: Option<LuaRegistryKey>,
) -> Self {
Self(lua, Arc::new(callback_http), Arc::new(callback_websocket))
}
}
impl Service<&AddrStream> for NetService {
type Response = NetServiceInner;
type Error = hyper::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _: &AddrStream) -> Self::Future {
let lua = self.0;
let key1 = self.1.clone();
let key2 = self.2.clone();
Box::pin(async move { Ok(NetServiceInner(lua, key1, key2)) })
}
}
#[derive(Clone, Copy, Debug)]
pub struct NetLocalExec;
impl<F> hyper::rt::Executor<F> for NetLocalExec
where
F: std::future::Future + 'static, // not requiring `Send`
{
fn execute(&self, fut: F) {
task::spawn_local(fut);
}
}

View file

@ -1,229 +0,0 @@
use std::{cell::Cell, sync::Arc};
use hyper::upgrade::Upgraded;
use mlua::prelude::*;
use futures_util::{
stream::{SplitSink, SplitStream},
SinkExt, StreamExt,
};
use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
sync::Mutex as AsyncMutex,
};
use hyper_tungstenite::{
tungstenite::{
protocol::{frame::coding::CloseCode as WsCloseCode, CloseFrame as WsCloseFrame},
Message as WsMessage,
},
WebSocketStream,
};
use tokio_tungstenite::MaybeTlsStream;
use crate::lune::lua::table::TableBuilder;
const WEB_SOCKET_IMPL_LUA: &str = r#"
return freeze(setmetatable({
close = function(...)
return close(websocket, ...)
end,
send = function(...)
return send(websocket, ...)
end,
next = function(...)
return next(websocket, ...)
end,
}, {
__index = function(self, key)
if key == "closeCode" then
return close_code(websocket)
end
end,
}))
"#;
#[derive(Debug)]
pub struct NetWebSocket<T> {
close_code: Arc<Cell<Option<u16>>>,
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: Arc::clone(&self.close_code),
read_stream: Arc::clone(&self.read_stream),
write_stream: Arc::clone(&self.write_stream),
}
}
}
impl<T> NetWebSocket<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
pub fn new(value: WebSocketStream<T>) -> Self {
let (write, read) = value.split();
Self {
close_code: Arc::new(Cell::new(None)),
read_stream: Arc::new(AsyncMutex::new(read)),
write_stream: Arc::new(AsyncMutex::new(write)),
}
}
fn into_lua_table_with_env<'lua>(
lua: &'lua Lua,
env: LuaTable<'lua>,
) -> LuaResult<LuaTable<'lua>> {
lua.load(WEB_SOCKET_IMPL_LUA)
.set_name("websocket")
.set_environment(env)
.eval()
}
}
type NetWebSocketStreamClient = MaybeTlsStream<TcpStream>;
impl NetWebSocket<NetWebSocketStreamClient> {
pub fn into_lua_table(self, lua: &'static Lua) -> LuaResult<LuaTable> {
let socket_env = TableBuilder::new(lua)?
.with_value("websocket", self)?
.with_function("close_code", close_code::<NetWebSocketStreamClient>)?
.with_async_function("close", close::<NetWebSocketStreamClient>)?
.with_async_function("send", send::<NetWebSocketStreamClient>)?
.with_async_function("next", next::<NetWebSocketStreamClient>)?
.with_value(
"setmetatable",
lua.named_registry_value::<LuaFunction>("tab.setmeta")?,
)?
.with_value(
"freeze",
lua.named_registry_value::<LuaFunction>("tab.freeze")?,
)?
.build_readonly()?;
Self::into_lua_table_with_env(lua, socket_env)
}
}
type NetWebSocketStreamServer = Upgraded;
impl NetWebSocket<NetWebSocketStreamServer> {
pub fn into_lua_table(self, lua: &'static Lua) -> LuaResult<LuaTable> {
let socket_env = TableBuilder::new(lua)?
.with_value("websocket", self)?
.with_function("close_code", close_code::<NetWebSocketStreamServer>)?
.with_async_function("close", close::<NetWebSocketStreamServer>)?
.with_async_function("send", send::<NetWebSocketStreamServer>)?
.with_async_function("next", next::<NetWebSocketStreamServer>)?
.with_value(
"setmetatable",
lua.named_registry_value::<LuaFunction>("tab.setmeta")?,
)?
.with_value(
"freeze",
lua.named_registry_value::<LuaFunction>("tab.freeze")?,
)?
.build_readonly()?;
Self::into_lua_table_with_env(lua, socket_env)
}
}
impl<T> LuaUserData for NetWebSocket<T> {}
fn close_code<'lua, T>(
_lua: &'lua Lua,
socket: LuaUserDataRef<'lua, NetWebSocket<T>>,
) -> LuaResult<LuaValue<'lua>>
where
T: AsyncRead + AsyncWrite + Unpin,
{
Ok(match socket.close_code.get() {
Some(code) => LuaValue::Number(code as f64),
None => LuaValue::Nil,
})
}
async fn close<'lua, T>(
_lua: &'lua Lua,
(socket, code): (LuaUserDataRef<'lua, NetWebSocket<T>>, Option<u16>),
) -> LuaResult<()>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let mut ws = socket.write_stream.lock().await;
ws.send(WsMessage::Close(Some(WsCloseFrame {
code: match code {
Some(code) if (1000..=4999).contains(&code) => WsCloseCode::from(code),
Some(code) => {
return Err(LuaError::RuntimeError(format!(
"Close code must be between 1000 and 4999, got {code}"
)))
}
None => WsCloseCode::Normal,
},
reason: "".into(),
})))
.await
.map_err(LuaError::external)?;
let res = ws.close();
res.await.map_err(LuaError::external)
}
async fn send<'lua, T>(
_lua: &'lua Lua,
(socket, string, as_binary): (
LuaUserDataRef<'lua, NetWebSocket<T>>,
LuaString<'lua>,
Option<bool>,
),
) -> LuaResult<()>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let msg = if matches!(as_binary, Some(true)) {
WsMessage::Binary(string.as_bytes().to_vec())
} else {
let s = string.to_str().map_err(LuaError::external)?;
WsMessage::Text(s.to_string())
};
let mut ws = socket.write_stream.lock().await;
ws.send(msg).await.map_err(LuaError::external)
}
async fn next<'lua, T>(
lua: &'lua Lua,
socket: LuaUserDataRef<'lua, NetWebSocket<T>>,
) -> LuaResult<LuaValue<'lua>>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let mut ws = socket.read_stream.lock().await;
let item = ws.next().await.transpose().map_err(LuaError::external);
let msg = match item {
Ok(Some(WsMessage::Close(msg))) => {
if let Some(msg) = &msg {
socket.close_code.replace(Some(msg.code.into()));
}
Ok(Some(WsMessage::Close(msg)))
}
val => val,
}?;
while let Some(msg) = &msg {
let msg_string_opt = match msg {
WsMessage::Binary(bin) => Some(lua.create_string(bin)?),
WsMessage::Text(txt) => Some(lua.create_string(txt)?),
// Stop waiting for next message if we get a close message
WsMessage::Close(_) => return Ok(LuaValue::Nil),
// Ignore ping/pong/frame messages, they are handled by tungstenite
_ => None,
};
if let Some(msg_string) = msg_string_opt {
return Ok(LuaValue::String(msg_string));
}
}
Ok(LuaValue::Nil)
}

View file

@ -1,51 +0,0 @@
use std::process::ExitStatus;
use mlua::prelude::*;
use tokio::{io, process::Child, task};
mod tee_writer;
use tee_writer::AsyncTeeWriter;
pub async fn pipe_and_inherit_child_process_stdio(
mut child: Child,
) -> LuaResult<(ExitStatus, Vec<u8>, Vec<u8>)> {
let mut child_stdout = child.stdout.take().unwrap();
let mut child_stderr = child.stderr.take().unwrap();
/*
NOTE: We do not need to register these
independent tasks spawning in the scheduler
This function is only used by `process.spawn` which in
turn registers a task with the scheduler that awaits this
*/
let stdout_thread = task::spawn(async move {
let mut stdout = io::stdout();
let mut tee = AsyncTeeWriter::new(&mut stdout);
io::copy(&mut child_stdout, &mut tee)
.await
.map_err(LuaError::external)?;
Ok::<_, LuaError>(tee.into_vec())
});
let stderr_thread = task::spawn(async move {
let mut stderr = io::stderr();
let mut tee = AsyncTeeWriter::new(&mut stderr);
io::copy(&mut child_stderr, &mut tee)
.await
.map_err(LuaError::external)?;
Ok::<_, LuaError>(tee.into_vec())
});
let status = child.wait().await.expect("Child process failed to start");
let stdout_buffer = stdout_thread.await.expect("Tee writer for stdout errored");
let stderr_buffer = stderr_thread.await.expect("Tee writer for stderr errored");
Ok::<_, LuaError>((status, stdout_buffer?, stderr_buffer?))
}

View file

@ -1,64 +0,0 @@
use std::{
io::Write,
pin::Pin,
task::{Context, Poll},
};
use pin_project::pin_project;
use tokio::io::{self, AsyncWrite};
#[pin_project]
pub struct AsyncTeeWriter<'a, W>
where
W: AsyncWrite + Unpin,
{
#[pin]
writer: &'a mut W,
buffer: Vec<u8>,
}
impl<'a, W> AsyncTeeWriter<'a, W>
where
W: AsyncWrite + Unpin,
{
pub fn new(writer: &'a mut W) -> Self {
Self {
writer,
buffer: Vec::new(),
}
}
pub fn into_vec(self) -> Vec<u8> {
self.buffer
}
}
impl<'a, W> AsyncWrite for AsyncTeeWriter<'a, W>
where
W: AsyncWrite + Unpin,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let mut this = self.project();
match this.writer.as_mut().poll_write(cx, buf) {
Poll::Ready(res) => {
this.buffer
.write_all(buf)
.expect("Failed to write to internal tee buffer");
Poll::Ready(res)
}
Poll::Pending => Poll::Pending,
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.project().writer.as_mut().poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.project().writer.as_mut().poll_shutdown(cx)
}
}

View file

@ -1,162 +0,0 @@
use lz4_flex::{compress_prepend_size, decompress_size_prepended};
use mlua::prelude::*;
use tokio::{
io::{copy, BufReader},
task,
};
use async_compression::{
tokio::bufread::{
BrotliDecoder, BrotliEncoder, GzipDecoder, GzipEncoder, ZlibDecoder, ZlibEncoder,
},
Level::Best as CompressionQuality,
};
#[derive(Debug, Clone, Copy)]
pub enum CompressDecompressFormat {
Brotli,
GZip,
LZ4,
ZLib,
}
#[allow(dead_code)]
impl CompressDecompressFormat {
pub fn detect_from_bytes(bytes: impl AsRef<[u8]>) -> Option<Self> {
match bytes.as_ref() {
// https://github.com/PSeitz/lz4_flex/blob/main/src/frame/header.rs#L28
b if b.len() >= 4
&& matches!(
u32::from_le_bytes(b[0..4].try_into().unwrap()),
0x184D2204 | 0x184C2102
) =>
{
Some(Self::LZ4)
}
// https://github.com/dropbox/rust-brotli/blob/master/src/enc/brotli_bit_stream.rs#L2805
b if b.len() >= 4
&& matches!(
b[0..3],
[0xE1, 0x97, 0x81] | [0xE1, 0x97, 0x82] | [0xE1, 0x97, 0x80]
) =>
{
Some(Self::Brotli)
}
// https://github.com/rust-lang/flate2-rs/blob/main/src/gz/mod.rs#L135
b if b.len() >= 3 && matches!(b[0..3], [0x1F, 0x8B, 0x08]) => Some(Self::GZip),
// https://stackoverflow.com/a/43170354
b if b.len() >= 2
&& matches!(
b[0..2],
[0x78, 0x01] | [0x78, 0x5E] | [0x78, 0x9C] | [0x78, 0xDA]
) =>
{
Some(Self::ZLib)
}
_ => None,
}
}
pub fn detect_from_header_str(header: impl AsRef<str>) -> Option<Self> {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives
match header.as_ref().to_ascii_lowercase().trim() {
"br" | "brotli" => Some(Self::Brotli),
"deflate" => Some(Self::ZLib),
"gz" | "gzip" => Some(Self::GZip),
_ => None,
}
}
}
impl<'lua> FromLua<'lua> for CompressDecompressFormat {
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() {
"brotli" => Ok(Self::Brotli),
"gzip" => Ok(Self::GZip),
"lz4" => Ok(Self::LZ4),
"zlib" => Ok(Self::ZLib),
kind => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "CompressDecompressFormat",
message: Some(format!(
"Invalid format '{kind}', valid formats are: brotli, gzip, lz4, zlib"
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "CompressDecompressFormat",
message: None,
})
}
}
}
pub async fn compress<'lua>(
format: CompressDecompressFormat,
source: impl AsRef<[u8]>,
) -> LuaResult<Vec<u8>> {
if let CompressDecompressFormat::LZ4 = format {
let source = source.as_ref().to_vec();
return task::spawn_blocking(move || compress_prepend_size(&source))
.await
.map_err(LuaError::external);
}
let mut bytes = Vec::new();
let reader = BufReader::new(source.as_ref());
match format {
CompressDecompressFormat::Brotli => {
let mut encoder = BrotliEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::GZip => {
let mut encoder = GzipEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::ZLib => {
let mut encoder = ZlibEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::LZ4 => unreachable!(),
}
Ok(bytes)
}
pub async fn decompress<'lua>(
format: CompressDecompressFormat,
source: impl AsRef<[u8]>,
) -> LuaResult<Vec<u8>> {
if let CompressDecompressFormat::LZ4 = format {
let source = source.as_ref().to_vec();
return task::spawn_blocking(move || decompress_size_prepended(&source))
.await
.map_err(LuaError::external)?
.map_err(LuaError::external);
}
let mut bytes = Vec::new();
let reader = BufReader::new(source.as_ref());
match format {
CompressDecompressFormat::Brotli => {
let mut decoder = BrotliDecoder::new(reader);
copy(&mut decoder, &mut bytes).await?;
}
CompressDecompressFormat::GZip => {
let mut decoder = GzipDecoder::new(reader);
copy(&mut decoder, &mut bytes).await?;
}
CompressDecompressFormat::ZLib => {
let mut decoder = ZlibDecoder::new(reader);
copy(&mut decoder, &mut bytes).await?;
}
CompressDecompressFormat::LZ4 => unreachable!(),
}
Ok(bytes)
}

View file

@ -1,121 +0,0 @@
use mlua::prelude::*;
use serde_json::Value as JsonValue;
use serde_yaml::Value as YamlValue;
use toml::Value as TomlValue;
#[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,
})
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct EncodeDecodeConfig {
pub format: EncodeDecodeFormat,
pub pretty: bool,
}
impl EncodeDecodeConfig {
pub fn serialize_to_string<'lua>(
self,
lua: &'lua Lua,
value: LuaValue<'lua>,
) -> LuaResult<LuaString<'lua>> {
let bytes = match self.format {
EncodeDecodeFormat::Json => {
if self.pretty {
serde_json::to_vec_pretty(&value).map_err(LuaError::external)?
} else {
serde_json::to_vec(&value).map_err(LuaError::external)?
}
}
EncodeDecodeFormat::Yaml => {
let mut writer = Vec::with_capacity(128);
serde_yaml::to_writer(&mut writer, &value).map_err(LuaError::external)?;
writer
}
EncodeDecodeFormat::Toml => {
let s = if self.pretty {
toml::to_string_pretty(&value).map_err(LuaError::external)?
} else {
toml::to_string(&value).map_err(LuaError::external)?
};
s.as_bytes().to_vec()
}
};
lua.create_string(bytes)
}
pub fn deserialize_from_string<'lua>(
self,
lua: &'lua Lua,
string: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
let bytes = string.as_bytes();
match self.format {
EncodeDecodeFormat::Json => {
let value: JsonValue = serde_json::from_slice(bytes).map_err(LuaError::external)?;
lua.to_value(&value)
}
EncodeDecodeFormat::Yaml => {
let value: YamlValue = serde_yaml::from_slice(bytes).map_err(LuaError::external)?;
lua.to_value(&value)
}
EncodeDecodeFormat::Toml => {
if let Ok(s) = string.to_str() {
let value: TomlValue = toml::from_str(s).map_err(LuaError::external)?;
lua.to_value(&value)
} else {
Err(LuaError::RuntimeError(
"TOML must be valid utf-8".to_string(),
))
}
}
}
}
}
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,
}
}
}

View file

@ -1,5 +0,0 @@
mod compress_decompress;
mod encode_decode;
pub use compress_decompress::{compress, decompress, CompressDecompressFormat};
pub use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat};

View file

@ -1,473 +0,0 @@
use std::fmt::Write;
use console::{colors_enabled, set_colors_enabled, style, Style};
use mlua::prelude::*;
use once_cell::sync::Lazy;
use crate::lune::lua::task::TaskReference;
const MAX_FORMAT_DEPTH: usize = 4;
const INDENT: &str = " ";
pub const STYLE_RESET_STR: &str = "\x1b[0m";
// Colors
pub static COLOR_BLACK: Lazy<Style> = Lazy::new(|| Style::new().black());
pub static COLOR_RED: Lazy<Style> = Lazy::new(|| Style::new().red());
pub static COLOR_GREEN: Lazy<Style> = Lazy::new(|| Style::new().green());
pub static COLOR_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow());
pub static COLOR_BLUE: Lazy<Style> = Lazy::new(|| Style::new().blue());
pub static COLOR_PURPLE: Lazy<Style> = Lazy::new(|| Style::new().magenta());
pub static COLOR_CYAN: Lazy<Style> = Lazy::new(|| Style::new().cyan());
pub static COLOR_WHITE: Lazy<Style> = Lazy::new(|| Style::new().white());
// Styles
pub static STYLE_BOLD: Lazy<Style> = Lazy::new(|| Style::new().bold());
pub static STYLE_DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
fn can_be_plain_lua_table_key(s: &LuaString) -> bool {
let str = s.to_string_lossy().to_string();
let first_char = str.chars().next().unwrap();
if first_char.is_alphabetic() {
str.chars().all(|c| c == '_' || c.is_alphanumeric())
} else {
false
}
}
pub fn format_label<S: AsRef<str>>(s: S) -> String {
format!(
"{}{}{} ",
style("[").dim(),
match s.as_ref().to_ascii_lowercase().as_str() {
"info" => style("INFO").blue(),
"warn" => style("WARN").yellow(),
"error" => style("ERROR").red(),
_ => style(""),
},
style("]").dim()
)
}
pub fn format_style(style: Option<&'static Style>) -> String {
if cfg!(test) {
"".to_string()
} else if let Some(style) = style {
// HACK: We have no direct way of referencing the ansi color code
// of the style that console::Style provides, and we also know for
// sure that styles always include the reset sequence at the end,
// unless we are in a CI environment on non-interactive terminal
style
.apply_to("")
.to_string()
.trim_end_matches(STYLE_RESET_STR)
.to_string()
} else {
STYLE_RESET_STR.to_string()
}
}
pub fn style_from_color_str<S: AsRef<str>>(s: S) -> LuaResult<Option<&'static Style>> {
Ok(match s.as_ref() {
"reset" => None,
"black" => Some(&COLOR_BLACK),
"red" => Some(&COLOR_RED),
"green" => Some(&COLOR_GREEN),
"yellow" => Some(&COLOR_YELLOW),
"blue" => Some(&COLOR_BLUE),
"purple" => Some(&COLOR_PURPLE),
"cyan" => Some(&COLOR_CYAN),
"white" => Some(&COLOR_WHITE),
_ => {
return Err(LuaError::RuntimeError(format!(
"The color '{}' is not a valid color name",
s.as_ref()
)));
}
})
}
pub fn style_from_style_str<S: AsRef<str>>(s: S) -> LuaResult<Option<&'static Style>> {
Ok(match s.as_ref() {
"reset" => None,
"bold" => Some(&STYLE_BOLD),
"dim" => Some(&STYLE_DIM),
_ => {
return Err(LuaError::RuntimeError(format!(
"The style '{}' is not a valid style name",
s.as_ref()
)));
}
})
}
pub fn pretty_format_value(
buffer: &mut String,
value: &LuaValue,
depth: usize,
) -> std::fmt::Result {
// TODO: Handle tables with cyclic references
match &value {
LuaValue::Nil => write!(buffer, "nil")?,
LuaValue::Boolean(true) => write!(buffer, "{}", COLOR_YELLOW.apply_to("true"))?,
LuaValue::Boolean(false) => write!(buffer, "{}", COLOR_YELLOW.apply_to("false"))?,
LuaValue::Number(n) => write!(buffer, "{}", COLOR_CYAN.apply_to(format!("{n}")))?,
LuaValue::Integer(i) => write!(buffer, "{}", COLOR_CYAN.apply_to(format!("{i}")))?,
LuaValue::String(s) => write!(
buffer,
"\"{}\"",
COLOR_GREEN.apply_to(
s.to_string_lossy()
.replace('"', r#"\""#)
.replace('\r', r#"\r"#)
.replace('\n', r#"\n"#)
)
)?,
LuaValue::Table(ref tab) => {
if depth >= MAX_FORMAT_DEPTH {
write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?;
} else if let Some(s) = call_table_tostring_metamethod(tab) {
write!(buffer, "{s}")?;
} else {
let mut is_empty = false;
let depth_indent = INDENT.repeat(depth);
write!(buffer, "{}", STYLE_DIM.apply_to("{"))?;
for pair in tab.clone().pairs::<LuaValue, LuaValue>() {
let (key, value) = pair.unwrap();
match &key {
LuaValue::String(s) if can_be_plain_lua_table_key(s) => write!(
buffer,
"\n{}{}{} {} ",
depth_indent,
INDENT,
s.to_string_lossy(),
STYLE_DIM.apply_to("=")
)?,
_ => {
write!(buffer, "\n{depth_indent}{INDENT}[")?;
pretty_format_value(buffer, &key, depth)?;
write!(buffer, "] {} ", STYLE_DIM.apply_to("="))?;
}
}
pretty_format_value(buffer, &value, depth + 1)?;
write!(buffer, "{}", STYLE_DIM.apply_to(","))?;
is_empty = false;
}
if is_empty {
write!(buffer, "{}", STYLE_DIM.apply_to(" }"))?;
} else {
write!(buffer, "\n{depth_indent}{}", STYLE_DIM.apply_to("}"))?;
}
}
}
LuaValue::Vector(v) => write!(
buffer,
"{}",
COLOR_PURPLE.apply_to(format!(
"<vector({x}, {y}, {z})>",
x = v.x(),
y = v.y(),
z = v.z()
))
)?,
LuaValue::Thread(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<thread>"))?,
LuaValue::Function(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<function>"))?,
LuaValue::UserData(u) => {
if u.is::<TaskReference>() {
// Task references must be transparent
// to lua and pretend to be normal lua
// threads for compatibility purposes
write!(buffer, "{}", COLOR_PURPLE.apply_to("<thread>"))?
} else if let Some(s) = call_userdata_tostring_metamethod(u) {
write!(buffer, "{s}")?
} else {
write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?
}
}
LuaValue::LightUserData(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?,
LuaValue::Error(e) => write!(buffer, "{}", pretty_format_luau_error(e, false),)?,
}
Ok(())
}
pub fn pretty_format_multi_value(multi: &LuaMultiValue) -> LuaResult<String> {
let mut buffer = String::new();
let mut counter = 0;
for value in multi {
counter += 1;
if let LuaValue::String(s) = value {
write!(buffer, "{}", s.to_string_lossy()).map_err(LuaError::external)?;
} else {
pretty_format_value(&mut buffer, value, 0).map_err(LuaError::external)?;
}
if counter < multi.len() {
write!(&mut buffer, " ").map_err(LuaError::external)?;
}
}
Ok(buffer)
}
pub fn pretty_format_luau_error(e: &LuaError, colorized: bool) -> String {
let previous_colors_enabled = if !colorized {
set_colors_enabled(false);
Some(colors_enabled())
} else {
None
};
let stack_begin = format!("[{}]", COLOR_BLUE.apply_to("Stack Begin"));
let stack_end = format!("[{}]", COLOR_BLUE.apply_to("Stack End"));
let err_string = match e {
LuaError::RuntimeError(e) => {
// Remove unnecessary prefix
let mut err_string = e.to_string();
if let Some(no_prefix) = err_string.strip_prefix("runtime error: ") {
err_string = no_prefix.to_string();
}
// Add "Stack Begin" instead of default stack traceback string
let mut err_lines = err_string
.lines()
.map(|s| s.to_string())
.collect::<Vec<String>>();
let mut found_stack_begin = false;
for (index, line) in err_lines.clone().iter().enumerate().rev() {
if *line == "stack traceback:" {
err_lines[index] = stack_begin.clone();
found_stack_begin = true;
break;
}
}
// Add "Stack End" to the very end of the stack trace for symmetry
if found_stack_begin {
err_lines.push(stack_end.clone());
}
err_lines.join("\n")
}
LuaError::CallbackError { traceback, cause } => {
// Find the best traceback (most lines) and the root error message
// The traceback may also start with "override traceback:" which
// means it was passed from somewhere that wants a custom trace,
// so we should then respect that and get the best override instead
let mut full_trace = traceback.to_string();
let mut root_cause = cause.as_ref();
let mut trace_override = false;
while let LuaError::CallbackError { cause, traceback } = root_cause {
let is_override = traceback.starts_with("override traceback:");
if is_override {
if !trace_override || traceback.lines().count() > full_trace.len() {
full_trace = traceback
.trim_start_matches("override traceback:")
.to_string();
trace_override = true;
}
} else if !trace_override {
full_trace = format!("{traceback}\n{full_trace}");
}
root_cause = cause;
}
// If we got a runtime error with an embedded traceback, we should
// use that instead since it generally contains more information
if matches!(root_cause, LuaError::RuntimeError(e) if e.contains("stack traceback:")) {
pretty_format_luau_error(root_cause, colorized)
} else {
// Otherwise we format whatever root error we got using
// the same error formatting as for above runtime errors
format!(
"{}\n{}\n{}\n{}",
pretty_format_luau_error(root_cause, colorized),
stack_begin,
full_trace.trim_start_matches("stack traceback:\n"),
stack_end
)
}
}
LuaError::BadArgument { pos, cause, .. } => match cause.as_ref() {
// TODO: Add more detail to this error message
LuaError::FromLuaConversionError { from, to, .. } => {
format!("Argument #{pos} must be of type '{to}', got '{from}'")
}
c => format!(
"Bad argument #{pos}\n{}",
pretty_format_luau_error(c, colorized)
),
},
e => format!("{e}"),
};
// Re-enable colors if they were previously enabled
if let Some(true) = previous_colors_enabled {
set_colors_enabled(true)
}
// Remove the script path from the error message
// itself, it can be found in the stack trace
let mut err_lines = err_string.lines().collect::<Vec<_>>();
if let Some(first_line) = err_lines.first() {
if first_line.starts_with("[string \"") {
if let Some(closing_bracket) = first_line.find("]:") {
let after_closing_bracket = &first_line[closing_bracket + 2..first_line.len()];
if let Some(last_colon) = after_closing_bracket.find(": ") {
err_lines[0] = &after_closing_bracket
[last_colon + 2..first_line.len() - closing_bracket - 2];
} else {
err_lines[0] = after_closing_bracket
}
}
}
}
// Find where the stack trace stars and ends
let stack_begin_idx =
err_lines.iter().enumerate().find_map(
|(i, line)| {
if *line == stack_begin {
Some(i)
} else {
None
}
},
);
let stack_end_idx =
err_lines.iter().enumerate().find_map(
|(i, line)| {
if *line == stack_end {
Some(i)
} else {
None
}
},
);
// If we have a stack trace, we should transform the formatting from the
// default mlua formatting into something more friendly, similar to Roblox
if let (Some(idx_start), Some(idx_end)) = (stack_begin_idx, stack_end_idx) {
let stack_lines = err_lines
.iter()
.enumerate()
// Filter out stack lines
.filter_map(|(idx, line)| {
if idx > idx_start && idx < idx_end {
Some(*line)
} else {
None
}
})
// Transform from mlua format into friendly format, while also
// ensuring that leading whitespace / indentation is consistent
.map(transform_stack_line)
.collect::<Vec<_>>();
fix_error_nitpicks(format!(
"{}\n{}\n{}\n{}",
err_lines
.iter()
.take(idx_start)
.copied()
.collect::<Vec<_>>()
.join("\n"),
stack_begin,
stack_lines.join("\n"),
stack_end,
))
} else {
fix_error_nitpicks(err_string)
}
}
fn transform_stack_line(line: &str) -> String {
match (line.find('['), line.find(']')) {
(Some(idx_start), Some(idx_end)) => {
let name = line[idx_start..idx_end + 1]
.trim_start_matches('[')
.trim_start_matches("string ")
.trim_start_matches('"')
.trim_end_matches(']')
.trim_end_matches('"');
let after_name = &line[idx_end + 1..];
let line_num = match after_name.find(':') {
Some(lineno_start) => match after_name[lineno_start + 1..].find(':') {
Some(lineno_end) => &after_name[lineno_start + 1..lineno_end + 1],
None => match after_name.contains("in function") || after_name.contains("in ?")
{
false => &after_name[lineno_start + 1..],
true => "",
},
},
None => "",
};
let func_name = match after_name.find("in function ") {
Some(func_start) => after_name[func_start + 12..]
.trim()
.trim_end_matches('\'')
.trim_start_matches('\'')
.trim_start_matches("_G."),
None => "",
};
let mut result = String::new();
write!(
result,
" Script '{}'",
match name {
"C" => "[C]",
name => name,
},
)
.unwrap();
if !line_num.is_empty() {
write!(result, ", Line {line_num}").unwrap();
}
if !func_name.is_empty() {
write!(result, " - function {func_name}").unwrap();
}
result
}
(_, _) => line.to_string(),
}
}
fn fix_error_nitpicks(full_message: String) -> String {
full_message
// Hacky fix for our custom require appearing as a normal script
// TODO: It's probably better to pull in the regex crate here ..
.replace("'require', Line 5", "'[C]' - function require")
.replace("'require', Line 7", "'[C]' - function require")
.replace("'require', Line 8", "'[C]' - function require")
// Same thing here for our async script
.replace("'async', Line 2", "'[C]'")
.replace("'async', Line 3", "'[C]'")
// Fix error calls in custom script chunks coming through
.replace(
"'[C]' - function error\n Script '[C]' - function require",
"'[C]' - function require",
)
// Fix strange double require
.replace(
"'[C]' - function require - function require",
"'[C]' - function require",
)
// Fix strange double C
.replace("'[C]'\n Script '[C]'", "'[C]'")
}
fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
let f = match tab.get_metatable() {
None => None,
Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
}
fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
let f = match tab.get_metatable() {
Err(_) => None,
Ok(meta) => match meta.get::<LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
}

View file

@ -1,2 +0,0 @@
pub mod formatting;
pub mod prompt;

View file

@ -1,192 +0,0 @@
use std::fmt;
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub enum PromptKind {
Text,
Confirm,
Select,
MultiSelect,
}
impl PromptKind {
fn get_all() -> Vec<Self> {
vec![Self::Text, Self::Confirm, Self::Select, Self::MultiSelect]
}
}
impl Default for PromptKind {
fn default() -> Self {
Self::Text
}
}
impl fmt::Display for PromptKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Text => "Text",
Self::Confirm => "Confirm",
Self::Select => "Select",
Self::MultiSelect => "MultiSelect",
}
)
}
}
impl<'lua> FromLua<'lua> for PromptKind {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::Nil = value {
Ok(Self::default())
} else if let LuaValue::String(s) = value {
let s = s.to_str()?;
/*
If the user only typed the prompt kind slightly wrong, meaning
it has some kind of space in it, a weird character, or an uppercase
character, we should try to be permissive as possible and still work
Not everyone is using an IDE with proper Luau type definitions
installed, and Luau is still a permissive scripting language
even though it has a strict (but optional) type system
*/
let s = s
.chars()
.filter_map(|c| {
if c.is_ascii_alphabetic() {
Some(c.to_ascii_lowercase())
} else {
None
}
})
.collect::<String>();
// If the prompt kind is still invalid we will
// show the user a descriptive error message
match s.as_ref() {
"text" => Ok(Self::Text),
"confirm" => Ok(Self::Confirm),
"select" => Ok(Self::Select),
"multiselect" => Ok(Self::MultiSelect),
s => Err(LuaError::FromLuaConversionError {
from: "string",
to: "PromptKind",
message: Some(format!(
"Invalid prompt kind '{s}', valid kinds are:\n{}",
PromptKind::get_all()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptKind",
message: None,
})
}
}
}
pub struct PromptOptions {
pub kind: PromptKind,
pub text: Option<String>,
pub default_string: Option<String>,
pub default_bool: Option<bool>,
pub options: Option<Vec<String>>,
}
impl<'lua> FromLuaMulti<'lua> for PromptOptions {
fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
// Argument #1 - prompt kind (optional)
let kind = values
.pop_front()
.map(|value| PromptKind::from_lua(value, lua))
.transpose()?
.unwrap_or_default();
// Argument #2 - prompt text (optional)
let text = values
.pop_front()
.map(|text| String::from_lua(text, lua))
.transpose()?;
// Argument #3 - default value / options,
// this is different per each prompt kind
let (default_bool, default_string, options) = match values.pop_front() {
None => (None, None, None),
Some(options) => match options {
LuaValue::Nil => (None, None, None),
LuaValue::Boolean(b) => (Some(b), None, None),
LuaValue::String(s) => (
None,
Some(String::from_lua(LuaValue::String(s), lua)?),
None,
),
LuaValue::Table(t) => (
None,
None,
Some(Vec::<String>::from_lua(LuaValue::Table(t), lua)?),
),
value => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "PromptOptions",
message: Some("Argument #3 must be a boolean, table, or nil".to_string()),
})
}
},
};
/*
Make sure we got the required values for the specific prompt kind:
- "Confirm" requires a message to be present so the user knows what they are confirming
- "Select" and "MultiSelect" both require a table of options to choose from
*/
if matches!(kind, PromptKind::Confirm) && text.is_none() {
return Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptOptions",
message: Some("Argument #2 missing or nil".to_string()),
});
}
if matches!(kind, PromptKind::Select | PromptKind::MultiSelect) && options.is_none() {
return Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptOptions",
message: Some("Argument #3 missing or nil".to_string()),
});
}
// All good, return the prompt options
Ok(Self {
kind,
text,
default_bool,
default_string,
options,
})
}
}
#[derive(Debug, Clone)]
pub enum PromptResult {
String(String),
Boolean(bool),
Index(usize),
Indices(Vec<usize>),
None,
}
impl<'lua> IntoLua<'lua> for PromptResult {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
Ok(match self {
Self::String(s) => LuaValue::String(lua.create_string(&s)?),
Self::Boolean(b) => LuaValue::Boolean(b),
Self::Index(i) => LuaValue::Number(i as f64),
Self::Indices(v) => v.into_lua(lua)?,
Self::None => LuaValue::Nil,
})
}
}

View file

@ -1,93 +0,0 @@
use std::future::Future;
use mlua::prelude::*;
use crate::lune::lua::async_ext::LuaAsyncExt;
pub struct TableBuilder {
lua: &'static Lua,
tab: LuaTable<'static>,
}
#[allow(dead_code)]
impl TableBuilder {
pub fn new(lua: &'static Lua) -> LuaResult<Self> {
let tab = lua.create_table()?;
Ok(Self { lua, tab })
}
pub fn with_value<K, V>(self, key: K, value: V) -> LuaResult<Self>
where
K: IntoLua<'static>,
V: IntoLua<'static>,
{
self.tab.raw_set(key, value)?;
Ok(self)
}
pub fn with_values<K, V>(self, values: Vec<(K, V)>) -> LuaResult<Self>
where
K: IntoLua<'static>,
V: IntoLua<'static>,
{
for (key, value) in values {
self.tab.raw_set(key, value)?;
}
Ok(self)
}
pub fn with_sequential_value<V>(self, value: V) -> LuaResult<Self>
where
V: IntoLua<'static>,
{
self.tab.raw_push(value)?;
Ok(self)
}
pub fn with_sequential_values<V>(self, values: Vec<V>) -> LuaResult<Self>
where
V: IntoLua<'static>,
{
for value in values {
self.tab.raw_push(value)?;
}
Ok(self)
}
pub fn with_metatable(self, table: LuaTable) -> LuaResult<Self> {
self.tab.set_metatable(Some(table));
Ok(self)
}
pub fn with_function<K, A, R, F>(self, key: K, func: F) -> LuaResult<Self>
where
K: IntoLua<'static>,
A: FromLuaMulti<'static>,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'static Lua, A) -> LuaResult<R>,
{
let f = self.lua.create_function(func)?;
self.with_value(key, LuaValue::Function(f))
}
pub fn with_async_function<K, A, R, F, FR>(self, key: K, func: F) -> LuaResult<Self>
where
K: IntoLua<'static>,
A: FromLuaMulti<'static>,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'static Lua, A) -> FR,
FR: 'static + Future<Output = LuaResult<R>>,
{
let f = self.lua.create_async_function(func)?;
self.with_value(key, LuaValue::Function(f))
}
pub fn build_readonly(self) -> LuaResult<LuaTable<'static>> {
self.tab.set_readonly(true);
Ok(self.tab)
}
pub fn build(self) -> LuaResult<LuaTable<'static>> {
Ok(self.tab)
}
}

View file

@ -1,3 +0,0 @@
mod builder;
pub use builder::TableBuilder;

View file

@ -1,135 +0,0 @@
use std::time::Duration;
use async_trait::async_trait;
use futures_util::Future;
use mlua::prelude::*;
use tokio::time::{sleep, Instant};
use crate::lune::lua::task::TaskKind;
use super::super::{
scheduler::TaskReference, scheduler::TaskScheduler, scheduler_handle::TaskSchedulerAsyncHandle,
scheduler_message::TaskSchedulerMessage,
};
/*
Trait definition - same as the implementation, ignore this
We use traits here to prevent misuse of certain scheduler
APIs, making importing of them as intentional as possible
*/
#[async_trait(?Send)]
pub trait TaskSchedulerAsyncExt<'fut> {
fn register_background_task(&self) -> TaskSchedulerAsyncHandle;
fn schedule_async<'sched, R, F, FR>(
&'sched self,
thread: LuaThread<'_>,
func: F,
) -> LuaResult<TaskReference>
where
'sched: 'fut,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'static Lua) -> FR,
FR: 'static + Future<Output = LuaResult<R>>;
fn schedule_wait(
&'fut self,
reference: LuaThread<'_>,
duration: Option<f64>,
) -> LuaResult<TaskReference>;
}
/*
Trait implementation
*/
#[async_trait(?Send)]
impl<'fut> TaskSchedulerAsyncExt<'fut> for TaskScheduler<'fut> {
/**
Registers a new background task with the task scheduler.
The returned [`TaskSchedulerAsyncHandle`] must have its
[`TaskSchedulerAsyncHandle::unregister`] method called
upon completion of the background task to prevent
the task scheduler from running indefinitely.
*/
fn register_background_task(&self) -> TaskSchedulerAsyncHandle {
let sender = self.futures_tx.clone();
sender
.send(TaskSchedulerMessage::Spawned)
.unwrap_or_else(|e| {
panic!(
"\
\nFailed to unregister background task - this is an internal error! \
\nPlease report it at {} \
\nDetails: {e} \
",
env!("CARGO_PKG_REPOSITORY")
)
});
TaskSchedulerAsyncHandle::new(sender)
}
/**
Schedules a lua thread or function
to be resumed after running a future.
The given lua thread or function will be resumed
using the optional arguments returned by the future.
*/
fn schedule_async<'sched, R, F, FR>(
&'sched self,
thread: LuaThread<'_>,
func: F,
) -> LuaResult<TaskReference>
where
'sched: 'fut, // Scheduler must live at least as long as the future
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'static Lua) -> FR,
FR: 'static + Future<Output = LuaResult<R>>,
{
self.queue_async_task(thread, None, async move {
match func(self.lua).await {
Ok(res) => match res.into_lua_multi(self.lua) {
Ok(multi) => Ok(Some(multi)),
Err(e) => Err(e),
},
Err(e) => Err(e),
}
})
}
/**
Schedules a task reference to be resumed after a certain amount of time.
The given task will be resumed with the elapsed time as its one and only argument.
*/
fn schedule_wait(
&'fut self,
thread: LuaThread<'_>,
duration: Option<f64>,
) -> LuaResult<TaskReference> {
let reference = self.create_task(TaskKind::Future, thread, None, true)?;
// Insert the future
let futs = self
.futures
.try_lock()
.expect("Tried to add future to queue during futures resumption");
futs.push(Box::pin(async move {
let before = Instant::now();
sleep(Duration::from_secs_f64(
duration.unwrap_or_default().max(0.0),
))
.await;
let elapsed_secs = before.elapsed().as_secs_f64();
let args = elapsed_secs.into_lua_multi(self.lua).unwrap();
(Some(reference), Ok(Some(args)))
}));
Ok(reference)
}
}

View file

@ -1,7 +0,0 @@
mod async_ext;
mod resume_ext;
mod schedule_ext;
pub use async_ext::TaskSchedulerAsyncExt;
pub use resume_ext::TaskSchedulerResumeExt;
pub use schedule_ext::TaskSchedulerScheduleExt;

View file

@ -1,180 +0,0 @@
use std::time::Duration;
use async_trait::async_trait;
use mlua::prelude::*;
use futures_util::StreamExt;
use tokio::time::sleep;
use super::super::{
scheduler_message::TaskSchedulerMessage, scheduler_state::TaskSchedulerState, TaskScheduler,
};
/*
Trait definition - same as the implementation, ignore this
We use traits here to prevent misuse of certain scheduler
APIs, making importing of them as intentional as possible
*/
#[async_trait(?Send)]
pub trait TaskSchedulerResumeExt {
async fn resume_queue(&self) -> TaskSchedulerState;
}
/*
Trait implementation
*/
#[async_trait(?Send)]
impl TaskSchedulerResumeExt for TaskScheduler<'_> {
/**
Resumes the task scheduler queue.
This will run any spawned or deferred Lua tasks in a blocking manner.
Once all spawned and / or deferred Lua tasks have finished running,
this will process delayed tasks, waiting tasks, and native Rust
futures concurrently, awaiting the first one to be ready for resumption.
*/
async fn resume_queue(&self) -> TaskSchedulerState {
let current = TaskSchedulerState::new(self);
if current.num_blocking > 0 {
// 1. Blocking tasks
resume_next_blocking_task(self, None)
} else if current.num_futures > 0 || current.num_background > 0 {
// 2. Async and/or background tasks
tokio::select! {
result = resume_next_async_task(self) => result,
result = receive_next_message(self) => result,
}
} else {
// 3. No tasks left, here we sleep one millisecond in case
// the caller of resume_queue accidentally calls this in
// a busy loop to prevent cpu usage from going to 100%
sleep(Duration::from_millis(1)).await;
TaskSchedulerState::new(self)
}
}
}
/*
Private functions for the trait that operate on the task scheduler
These could be implemented as normal methods but if we put them in the
trait they become public, and putting them in the task scheduler's
own implementation block will clutter that up unnecessarily
*/
/**
Resumes the next queued Lua task, if one exists, blocking
the current thread until it either yields or finishes.
*/
fn resume_next_blocking_task<'sched, 'args>(
scheduler: &TaskScheduler<'sched>,
override_args: Option<LuaResult<LuaMultiValue<'args>>>,
) -> TaskSchedulerState
where
'args: 'sched,
{
match {
let mut queue_guard = scheduler.tasks_queue_blocking.borrow_mut();
let task = queue_guard.pop_front();
drop(queue_guard);
task
} {
None => TaskSchedulerState::new(scheduler),
Some(task) => match scheduler.resume_task(task, override_args) {
Err(task_err) => {
scheduler.wake_completed_task(task, Err(task_err.clone()));
TaskSchedulerState::err(scheduler, task_err)
}
Ok(rets) if rets.0 == LuaThreadStatus::Unresumable => {
scheduler.wake_completed_task(task, Ok(rets.1));
TaskSchedulerState::new(scheduler)
}
Ok(_) => TaskSchedulerState::new(scheduler),
},
}
}
/**
Awaits the first available queued future, and resumes its associated
Lua task which will be ready for resumption when that future wakes.
Panics if there are no futures currently queued.
Use [`TaskScheduler::next_queue_future_exists`]
to check if there are any queued futures.
*/
async fn resume_next_async_task(scheduler: &TaskScheduler<'_>) -> TaskSchedulerState {
let (task, result) = {
let mut futs = scheduler
.futures
.try_lock()
.expect("Tried to resume next queued future while already resuming or modifying");
futs.next()
.await
.expect("Tried to resume next queued future but none are queued")
};
// The future might not return a reference that it wants to resume
if let Some(task) = task {
// Promote this future task to a blocking task and resume it
// right away, also taking care to not borrow mutably twice
// by dropping this guard before trying to resume it
let mut queue_guard = scheduler.tasks_queue_blocking.borrow_mut();
queue_guard.push_front(task);
drop(queue_guard);
}
resume_next_blocking_task(scheduler, result.transpose())
}
/**
Awaits the next background task registration
message, if any messages exist in the queue.
This is a no-op if there are no background tasks left running
and / or the background task messages channel was closed.
*/
async fn receive_next_message(scheduler: &TaskScheduler<'_>) -> TaskSchedulerState {
let message_opt = {
let mut rx = scheduler.futures_rx.lock().await;
rx.recv().await
};
if let Some(message) = message_opt {
match message {
TaskSchedulerMessage::NewBlockingTaskReady => TaskSchedulerState::new(scheduler),
TaskSchedulerMessage::NewLuaErrorReady(err) => TaskSchedulerState::err(scheduler, err),
TaskSchedulerMessage::Spawned => {
let prev = scheduler.futures_background_count.get();
scheduler.futures_background_count.set(prev + 1);
TaskSchedulerState::new(scheduler)
}
TaskSchedulerMessage::Terminated(result) => {
let prev = scheduler.futures_background_count.get();
scheduler.futures_background_count.set(prev - 1);
if prev == 0 {
panic!(
r#"
Terminated a background task without it running - this is an internal error!
Please report it at {}
"#,
env!("CARGO_PKG_REPOSITORY")
)
}
if let Err(e) = result {
TaskSchedulerState::err(scheduler, e)
} else {
TaskSchedulerState::new(scheduler)
}
}
}
} else {
TaskSchedulerState::new(scheduler)
}
}

View file

@ -1,91 +0,0 @@
use std::time::Duration;
use mlua::prelude::*;
use tokio::time::sleep;
use super::super::{scheduler::TaskKind, scheduler::TaskReference, scheduler::TaskScheduler};
/*
Trait definition - same as the implementation, ignore this
We use traits here to prevent misuse of certain scheduler
APIs, making importing of them as intentional as possible
*/
pub trait TaskSchedulerScheduleExt {
fn schedule_blocking(
&self,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference>;
fn schedule_blocking_deferred(
&self,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference>;
fn schedule_blocking_after_seconds(
&self,
after_secs: f64,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference>;
}
/*
Trait implementation
*/
impl TaskSchedulerScheduleExt for TaskScheduler<'_> {
/**
Schedules a lua thread or function to resume ***first*** during this
resumption point, ***skipping ahead*** of any other currently queued tasks.
The given lua thread or function will be resumed
using the given `thread_args` as its argument(s).
*/
fn schedule_blocking(
&self,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference> {
self.queue_blocking_task(TaskKind::Instant, thread, Some(thread_args))
}
/**
Schedules a lua thread or function to resume ***after all***
currently resuming tasks, during this resumption point.
The given lua thread or function will be resumed
using the given `thread_args` as its argument(s).
*/
fn schedule_blocking_deferred(
&self,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference> {
self.queue_blocking_task(TaskKind::Deferred, thread, Some(thread_args))
}
/**
Schedules a lua thread or function to
be resumed after waiting asynchronously.
The given lua thread or function will be resumed
using the given `thread_args` as its argument(s).
*/
fn schedule_blocking_after_seconds(
&self,
after_secs: f64,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference> {
self.queue_async_task(thread, Some(thread_args), async move {
sleep(Duration::from_secs_f64(after_secs)).await;
Ok(None)
})
}
}

View file

@ -1,15 +0,0 @@
mod ext;
mod proxy;
mod scheduler;
mod scheduler_handle;
mod scheduler_message;
mod scheduler_state;
mod task_kind;
mod task_reference;
mod task_waiter;
pub use ext::*;
pub use proxy::*;
pub use scheduler::*;
pub use scheduler_handle::*;
pub use scheduler_state::*;

View file

@ -1,118 +0,0 @@
use mlua::prelude::*;
use super::TaskReference;
/*
Proxy enum to deal with both threads & functions
*/
#[derive(Debug, Clone)]
pub enum LuaThreadOrFunction<'lua> {
Thread(LuaThread<'lua>),
Function(LuaFunction<'lua>),
}
impl<'lua> LuaThreadOrFunction<'lua> {
pub fn into_thread(self, lua: &'lua Lua) -> LuaResult<LuaThread<'lua>> {
match self {
Self::Thread(t) => Ok(t),
Self::Function(f) => lua.create_thread(f),
}
}
}
impl<'lua> From<LuaThread<'lua>> for LuaThreadOrFunction<'lua> {
fn from(value: LuaThread<'lua>) -> Self {
Self::Thread(value)
}
}
impl<'lua> From<LuaFunction<'lua>> for LuaThreadOrFunction<'lua> {
fn from(value: LuaFunction<'lua>) -> Self {
Self::Function(value)
}
}
impl<'lua> FromLua<'lua> for LuaThreadOrFunction<'lua> {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
LuaValue::Thread(t) => Ok(Self::Thread(t)),
LuaValue::Function(f) => Ok(Self::Function(f)),
value => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "LuaThreadOrFunction",
message: Some(format!(
"Expected thread or function, got '{}'",
value.type_name()
)),
}),
}
}
}
impl<'lua> IntoLua<'lua> for LuaThreadOrFunction<'lua> {
fn into_lua(self, _: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
match self {
Self::Thread(t) => Ok(LuaValue::Thread(t)),
Self::Function(f) => Ok(LuaValue::Function(f)),
}
}
}
/*
Proxy enum to deal with both threads & task scheduler task references
*/
#[derive(Debug, Clone)]
pub enum LuaThreadOrTaskReference<'lua> {
Thread(LuaThread<'lua>),
TaskReference(TaskReference),
}
impl<'lua> From<LuaThread<'lua>> for LuaThreadOrTaskReference<'lua> {
fn from(value: LuaThread<'lua>) -> Self {
Self::Thread(value)
}
}
impl<'lua> From<TaskReference> for LuaThreadOrTaskReference<'lua> {
fn from(value: TaskReference) -> Self {
Self::TaskReference(value)
}
}
impl<'lua> FromLua<'lua> for LuaThreadOrTaskReference<'lua> {
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let tname = value.type_name();
match value {
LuaValue::Thread(t) => Ok(Self::Thread(t)),
LuaValue::UserData(u) => {
if let Ok(task) =
LuaUserDataRef::<TaskReference>::from_lua(LuaValue::UserData(u), lua)
{
Ok(Self::TaskReference(*task))
} else {
Err(LuaError::FromLuaConversionError {
from: tname,
to: "thread",
message: Some(format!("Expected thread, got '{tname}'")),
})
}
}
_ => Err(LuaError::FromLuaConversionError {
from: tname,
to: "thread",
message: Some(format!("Expected thread, got '{tname}'")),
}),
}
}
}
impl<'lua> IntoLua<'lua> for LuaThreadOrTaskReference<'lua> {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
match self {
Self::TaskReference(t) => t.into_lua(lua),
Self::Thread(t) => Ok(LuaValue::Thread(t)),
}
}
}

View file

@ -1,480 +0,0 @@
use core::panic;
use std::{
cell::{Cell, RefCell},
collections::{HashMap, VecDeque},
process::ExitCode,
sync::Arc,
};
use futures_util::{future::LocalBoxFuture, stream::FuturesUnordered, Future};
use mlua::prelude::*;
use tokio::sync::{mpsc, Mutex as AsyncMutex};
use super::{
scheduler_message::TaskSchedulerMessage,
task_waiter::{TaskWaiterFuture, TaskWaiterState},
};
pub use super::{task_kind::TaskKind, task_reference::TaskReference};
type TaskFutureRets<'fut> = LuaResult<Option<LuaMultiValue<'fut>>>;
type TaskFuture<'fut> = LocalBoxFuture<'fut, (Option<TaskReference>, TaskFutureRets<'fut>)>;
/// A struct representing a task contained in the task scheduler
#[derive(Debug)]
pub struct Task {
kind: TaskKind,
thread: LuaRegistryKey,
args: LuaRegistryKey,
}
/// A task scheduler that implements task queues
/// with instant, deferred, and delayed tasks
#[derive(Debug)]
pub struct TaskScheduler<'fut> {
/*
Lots of cell and refcell here, however we need full interior mutability and never outer
since the scheduler struct may be accessed from lua more than once at the same time.
An example of this is the implementation of coroutine.resume, which instantly resumes the given
task, where the task getting resumed may also create new scheduler tasks during its resumption.
The same goes for values used during resumption of futures (`futures` and `futures_rx`)
which must use async-aware mutexes to be cancellation safe across await points.
*/
// Internal state & flags
pub(super) lua: &'static Lua,
pub(super) guid: Cell<usize>,
pub(super) exit_code: Cell<Option<ExitCode>>,
// Blocking tasks
pub(super) tasks: RefCell<HashMap<TaskReference, Task>>,
pub(super) tasks_count: Cell<usize>,
pub(super) tasks_current: Cell<Option<TaskReference>>,
pub(super) tasks_queue_blocking: RefCell<VecDeque<TaskReference>>,
pub(super) tasks_waiter_states:
RefCell<HashMap<TaskReference, Vec<Arc<AsyncMutex<TaskWaiterState<'fut>>>>>>,
pub(super) tasks_current_lua_error: Arc<AsyncMutex<Option<LuaError>>>,
// Future tasks & objects for waking
pub(super) futures: AsyncMutex<FuturesUnordered<TaskFuture<'fut>>>,
pub(super) futures_count: Cell<usize>,
pub(super) futures_background_count: Cell<usize>,
pub(super) futures_tx: mpsc::UnboundedSender<TaskSchedulerMessage>,
pub(super) futures_rx: AsyncMutex<mpsc::UnboundedReceiver<TaskSchedulerMessage>>,
}
impl<'fut> TaskScheduler<'fut> {
/**
Creates a new task scheduler.
*/
pub fn new(lua: &'static Lua) -> LuaResult<Self> {
let (tx, rx) = mpsc::unbounded_channel();
let tasks_current_lua_error = Arc::new(AsyncMutex::new(None));
let tasks_current_lua_error_inner = tasks_current_lua_error.clone();
lua.set_interrupt(move |_| {
match tasks_current_lua_error_inner.try_lock().unwrap().take() {
Some(err) => Err(err),
None => Ok(LuaVmState::Continue),
}
});
Ok(Self {
lua,
guid: Cell::new(0),
exit_code: Cell::new(None),
tasks: RefCell::new(HashMap::new()),
tasks_count: Cell::new(0),
tasks_current: Cell::new(None),
tasks_queue_blocking: RefCell::new(VecDeque::new()),
tasks_waiter_states: RefCell::new(HashMap::new()),
tasks_current_lua_error,
futures: AsyncMutex::new(FuturesUnordered::new()),
futures_tx: tx,
futures_rx: AsyncMutex::new(rx),
futures_count: Cell::new(0),
futures_background_count: Cell::new(0),
})
}
/**
Consumes and leaks the task scheduler,
returning a static reference `&'static TaskScheduler`.
This function is useful when the task scheduler object is
supposed to live for the remainder of the program's life.
Note that dropping the returned reference will cause a memory leak.
*/
pub fn into_static(self) -> &'static Self {
Box::leak(Box::new(self))
}
/**
Stores the exit code for the task scheduler.
This will be passed back to the Rust thread that is running the task scheduler,
in the [`TaskSchedulerState`] returned on resumption of the task scheduler queue.
Setting this exit code will signal to that thread that it
should stop resuming tasks, and gracefully terminate the program.
*/
pub fn set_exit_code(&self, code: ExitCode) {
self.exit_code.set(Some(code));
}
/**
Forwards a lua error to be emitted as soon as possible,
after any current blocking / queued tasks have been resumed.
Useful when an async function may call into Lua and get a
result back, without erroring out of the entire async block.
*/
pub fn forward_lua_error(&self, err: LuaError) {
let sender = self.futures_tx.clone();
sender
.send(TaskSchedulerMessage::NewLuaErrorReady(err))
.unwrap_or_else(|e| {
panic!(
"\
\nFailed to forward lua error - this is an internal error! \
\nPlease report it at {} \
\nDetails: {e} \
",
env!("CARGO_PKG_REPOSITORY")
)
});
}
/**
Forces the current task to be set to the given reference.
Useful if a task is to be resumed externally but full
compatibility with the task scheduler is still necessary.
*/
pub(crate) fn force_set_current_task(&self, reference: Option<TaskReference>) {
self.tasks_current.set(reference);
}
/**
Checks if a task still exists in the scheduler.
A task may no longer exist in the scheduler if it has been manually
cancelled and removed by calling [`TaskScheduler::cancel_task()`].
*/
#[allow(dead_code)]
pub fn contains_task(&self, reference: TaskReference) -> bool {
self.tasks.borrow().contains_key(&reference)
}
/**
Returns the currently running task, if any.
*/
pub fn current_task(&self) -> Option<TaskReference> {
self.tasks_current.get()
}
/**
Returns the status of a specific task, if it exists in the scheduler.
*/
pub fn get_task_status(&self, reference: TaskReference) -> Option<LuaString> {
self.tasks.borrow().get(&reference).map(|task| {
let status: LuaFunction = self
.lua
.named_registry_value("co.status")
.expect("Missing coroutine status function in registry");
let thread: LuaThread = self
.lua
.registry_value(&task.thread)
.expect("Task thread missing from registry");
status
.call(thread)
.expect("Task thread failed to call status")
})
}
/**
Creates a new task, storing a new Lua thread
for it, as well as the arguments to give the
thread on resumption, in the Lua registry.
Note that this task will ***not*** resume on its
own, it needs to be used together with either the
scheduling functions or [`TaskScheduler::resume_task`].
*/
pub fn create_task(
&self,
kind: TaskKind,
thread: LuaThread<'_>,
thread_args: Option<LuaMultiValue<'_>>,
inherit_current_guid: bool,
) -> LuaResult<TaskReference> {
// Store the thread and its arguments in the registry
// NOTE: We must convert to a vec since multis
// can't be stored in the registry directly
let task_args_vec: Option<Vec<LuaValue>> = thread_args.map(|opt| opt.into_vec());
let task_args_key: LuaRegistryKey = self.lua.create_registry_value(task_args_vec)?;
let task_thread_key: LuaRegistryKey = self.lua.create_registry_value(thread)?;
// Create the full task struct
let task = Task {
kind,
thread: task_thread_key,
args: task_args_key,
};
// Create the task ref to use
let guid = if inherit_current_guid {
self.current_task()
.ok_or_else(|| LuaError::RuntimeError("No current guid to inherit".to_string()))?
.id()
} else {
let guid = self.guid.get();
self.guid.set(guid + 1);
guid
};
let reference = TaskReference::new(kind, guid);
// Increment the corresponding task counter
match kind {
TaskKind::Future => self.futures_count.set(self.futures_count.get() + 1),
_ => self.tasks_count.set(self.tasks_count.get() + 1),
}
// Add the task to the scheduler
{
let mut tasks = self.tasks.borrow_mut();
tasks.insert(reference, task);
}
Ok(reference)
}
/**
Cancels a task, if the task still exists in the scheduler.
It is possible to hold one or more task references that point
to a task that no longer exists in the scheduler, and calling
this method with one of those references will return `false`.
*/
pub fn remove_task(&self, reference: TaskReference) -> LuaResult<bool> {
/*
Remove the task from the task list and the Lua registry
This is all we need to do since resume_task will always
ignore resumption of any task that no longer exists there
This does lead to having some amount of "junk" futures that will
build up in the queue but these will get cleaned up and not block
the program from exiting since the scheduler only runs until there
are no tasks left in the task list, the futures do not matter there
*/
let mut found = false;
let mut tasks = self.tasks.borrow_mut();
// Unfortunately we have to loop through to find which task
// references to remove instead of removing directly since
// tasks can switch kinds between instant, deferred, future
let tasks_to_remove: Vec<_> = tasks
.keys()
.filter(|task_ref| task_ref.id() == reference.id())
.copied()
.collect();
for task_ref in &tasks_to_remove {
if let Some(task) = tasks.remove(task_ref) {
// Decrement the corresponding task counter
match task.kind {
TaskKind::Future => self.futures_count.set(self.futures_count.get() - 1),
_ => self.tasks_count.set(self.tasks_count.get() - 1),
}
// NOTE: We need to close the thread here to
// make 100% sure that nothing can resume it
let close: LuaFunction = self.lua.named_registry_value("co.close")?;
let thread: LuaThread = self.lua.registry_value(&task.thread)?;
close.call(thread)?;
self.lua.remove_registry_value(task.thread)?;
self.lua.remove_registry_value(task.args)?;
found = true;
}
}
Ok(found)
}
/**
Resumes a task, if the task still exists in the scheduler.
A task may no longer exist in the scheduler if it has been manually
cancelled and removed by calling [`TaskScheduler::cancel_task()`].
This will be a no-op if the task no longer exists.
*/
pub fn resume_task<'a, 'r>(
&self,
reference: TaskReference,
override_args: Option<LuaResult<LuaMultiValue<'a>>>,
) -> LuaResult<(LuaThreadStatus, LuaMultiValue<'r>)>
where
'a: 'r,
{
// Fetch and check if the task was removed, if it got
// removed it means it was intentionally cancelled
let task = {
let mut tasks = self.tasks.borrow_mut();
match tasks.remove(&reference) {
Some(task) => task,
None => return Ok((LuaThreadStatus::Unresumable, LuaMultiValue::new())),
}
};
// Decrement the corresponding task counter
match task.kind {
TaskKind::Future => self.futures_count.set(self.futures_count.get() - 1),
_ => self.tasks_count.set(self.tasks_count.get() - 1),
}
// Fetch and remove the thread to resume + its arguments
let thread: LuaThread = self.lua.registry_value(&task.thread)?;
let thread_args: Option<LuaMultiValue> = {
self.lua
.registry_value::<Option<Vec<LuaValue>>>(&task.args)
.expect("Failed to get stored args for task")
.map(LuaMultiValue::from_vec)
};
self.lua.remove_registry_value(task.thread)?;
self.lua.remove_registry_value(task.args)?;
// We got everything we need and our references
// were cleaned up properly, resume the thread
self.tasks_current.set(Some(reference));
let rets = match override_args {
Some(override_res) => match override_res {
Ok(args) => thread.resume(args),
Err(e) => {
// NOTE: Setting this error here means that when the thread
// is resumed it will error instantly, so we don't need
// to call it with proper args, empty args is fine
self.tasks_current_lua_error.try_lock().unwrap().replace(e);
thread.resume(())
}
},
None => match thread_args {
Some(args) => thread.resume(args),
None => thread.resume(()),
},
};
self.tasks_current.set(None);
match rets {
Ok(rets) => Ok((thread.status(), rets)),
Err(e) => Err(e),
}
}
/**
Queues a new blocking task to run on the task scheduler.
*/
pub(crate) fn queue_blocking_task(
&self,
kind: TaskKind,
thread: LuaThread<'_>,
thread_args: Option<LuaMultiValue<'_>>,
) -> LuaResult<TaskReference> {
if kind == TaskKind::Future {
panic!("Tried to schedule future using normal task schedule method")
}
let task_ref = self.create_task(kind, thread, thread_args, false)?;
// Add the task to the front of the queue, unless it
// should be deferred, in that case add it to the back
let mut queue = self.tasks_queue_blocking.borrow_mut();
let num_prev_blocking_tasks = queue.len();
if kind == TaskKind::Deferred {
queue.push_back(task_ref);
} else {
queue.push_front(task_ref);
}
/*
If we had any previous task and are currently async
waiting on tasks, we should send a signal to wake up
and run the new blocking task that was just queued
This can happen in cases such as an async http
server waking up from a connection and then wanting to
run a lua callback in response, to create the.. response
*/
if num_prev_blocking_tasks == 0 {
self.futures_tx
.send(TaskSchedulerMessage::NewBlockingTaskReady)
.expect("Futures waker channel was closed")
}
Ok(task_ref)
}
/**
Queues a new future to run on the task scheduler.
*/
pub(crate) fn queue_async_task(
&self,
thread: LuaThread<'_>,
thread_args: Option<LuaMultiValue<'_>>,
fut: impl Future<Output = TaskFutureRets<'fut>> + 'fut,
) -> LuaResult<TaskReference> {
let task_ref = self.create_task(TaskKind::Future, thread, thread_args, false)?;
let futs = self
.futures
.try_lock()
.expect("Tried to add future to queue during futures resumption");
futs.push(Box::pin(async move {
let result = fut.await;
(Some(task_ref), result)
}));
Ok(task_ref)
}
/**
Queues a new future to run on the task scheduler,
inheriting the task id of the currently running task.
*/
pub(crate) fn queue_async_task_inherited(
&self,
thread: LuaThread<'_>,
thread_args: Option<LuaMultiValue<'_>>,
fut: impl Future<Output = TaskFutureRets<'fut>> + 'fut,
) -> LuaResult<TaskReference> {
let task_ref = self.create_task(TaskKind::Future, thread, thread_args, true)?;
let futs = self
.futures
.try_lock()
.expect("Tried to add future to queue during futures resumption");
futs.push(Box::pin(async move {
let result = fut.await;
(Some(task_ref), result)
}));
Ok(task_ref)
}
/**
Waits for a task to complete.
Panics if the task is not currently in the scheduler.
*/
pub(crate) async fn wait_for_task_completion(
&self,
reference: TaskReference,
) -> LuaResult<LuaMultiValue> {
if !self.tasks.borrow().contains_key(&reference) {
panic!("Task does not exist in scheduler")
}
let state = TaskWaiterState::new();
{
let mut all_states = self.tasks_waiter_states.borrow_mut();
all_states
.entry(reference)
.or_insert_with(Vec::new)
.push(Arc::clone(&state));
}
TaskWaiterFuture::new(&state).await
}
/**
Wakes a task that has been completed and may have external code
waiting on it using [`TaskScheduler::wait_for_task_completion`].
*/
pub(super) fn wake_completed_task(
&self,
reference: TaskReference,
result: LuaResult<LuaMultiValue<'fut>>,
) {
if let Some(waiter_states) = self.tasks_waiter_states.borrow_mut().remove(&reference) {
for waiter_state in waiter_states {
waiter_state.try_lock().unwrap().finalize(result.clone());
}
}
}
}

View file

@ -1,46 +0,0 @@
use core::panic;
use mlua::prelude::*;
use tokio::sync::mpsc;
use super::scheduler_message::TaskSchedulerMessage;
/**
A handle to a registered asynchronous background task.
[`TaskSchedulerAsyncHandle::unregister`] must be
called upon completion of the background task to
prevent the task scheduler from running indefinitely.
*/
#[must_use = "Background tasks must be unregistered"]
#[derive(Debug)]
pub struct TaskSchedulerAsyncHandle {
unregistered: bool,
sender: mpsc::UnboundedSender<TaskSchedulerMessage>,
}
impl TaskSchedulerAsyncHandle {
pub fn new(sender: mpsc::UnboundedSender<TaskSchedulerMessage>) -> Self {
Self {
unregistered: false,
sender,
}
}
pub fn unregister(mut self, result: LuaResult<()>) {
self.unregistered = true;
self.sender
.send(TaskSchedulerMessage::Terminated(result))
.unwrap_or_else(|_| {
panic!(
"\
\nFailed to unregister background task - this is an internal error! \
\nPlease report it at {} \
\nDetails: Manual \
",
env!("CARGO_PKG_REPOSITORY")
)
});
}
}

View file

@ -1,11 +0,0 @@
use mlua::prelude::*;
/// Internal message enum for the task scheduler, used to notify
/// futures to wake up and schedule their respective blocking tasks
#[derive(Debug, Clone)]
pub enum TaskSchedulerMessage {
NewBlockingTaskReady,
NewLuaErrorReady(LuaError),
Spawned,
Terminated(LuaResult<()>),
}

View file

@ -1,172 +0,0 @@
use std::{fmt, process::ExitCode};
use mlua::prelude::*;
use super::scheduler::TaskScheduler;
/// Struct representing the current state of the task scheduler
#[derive(Debug, Clone)]
#[must_use = "Scheduler state must be checked after every resumption"]
pub struct TaskSchedulerState {
pub(super) lua_error: Option<LuaError>,
pub(super) exit_code: Option<ExitCode>,
pub(super) num_blocking: usize,
pub(super) num_futures: usize,
pub(super) num_background: usize,
}
impl TaskSchedulerState {
pub(super) fn new(sched: &TaskScheduler) -> Self {
Self {
lua_error: None,
exit_code: sched.exit_code.get(),
num_blocking: sched.tasks_count.get(),
num_futures: sched.futures_count.get(),
num_background: sched.futures_background_count.get(),
}
}
pub(super) fn err(sched: &TaskScheduler, err: LuaError) -> Self {
let mut this = Self::new(sched);
this.lua_error = Some(err);
this
}
/**
Returns a clone of the error from
this task scheduler result, if any.
*/
pub fn get_lua_error(&self) -> Option<LuaError> {
self.lua_error.clone()
}
/**
Returns a clone of the exit code from
this task scheduler result, if any.
*/
pub fn get_exit_code(&self) -> Option<ExitCode> {
self.exit_code
}
/**
Returns `true` if the task scheduler still
has blocking lua threads left to run.
*/
pub fn is_blocking(&self) -> bool {
self.num_blocking > 0
}
/**
Returns `true` if the task scheduler has finished all
blocking lua tasks, but still has yielding tasks running.
*/
pub fn is_yielding(&self) -> bool {
self.num_blocking == 0 && self.num_futures > 0
}
/**
Returns `true` if the task scheduler has finished all
lua threads, but still has background tasks running.
*/
pub fn is_background(&self) -> bool {
self.num_blocking == 0 && self.num_futures == 0 && self.num_background > 0
}
/**
Returns `true` if the task scheduler is done,
meaning it has no lua threads left to run, and
no spawned tasks are running in the background.
Also returns `true` if a task has requested to exit the process.
*/
pub fn is_done(&self) -> bool {
self.exit_code.is_some()
|| (self.num_blocking == 0 && self.num_futures == 0 && self.num_background == 0)
}
}
impl fmt::Display for TaskSchedulerState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.is_blocking() {
"Busy"
} else if self.is_yielding() {
"Yielding"
} else if self.is_background() {
"Background"
} else {
"Done"
};
let code = match self.get_exit_code() {
Some(code) => format!("{code:?}"),
None => "-".to_string(),
};
let err = match self.get_lua_error() {
Some(e) => format!("{e:?}")
.as_bytes()
.chunks(42) // Kinda arbitrary but should fit in most terminals
.enumerate()
.map(|(idx, buf)| {
format!(
"{}{}{}{}{}",
if idx == 0 { "" } else { "\n" },
if idx == 0 {
"".to_string()
} else {
" ".repeat(16)
},
if idx == 0 { "" } else { "" },
String::from_utf8_lossy(buf),
if buf.len() == 42 { "" } else { "" },
)
})
.collect::<String>(),
None => "-".to_string(),
};
let parts = vec![
format!("Status │ {status}"),
format!("Tasks active │ {}", self.num_blocking),
format!("Tasks background │ {}", self.num_background),
format!("Status code │ {code}"),
format!("Lua error │ {err}"),
];
let lengths = parts
.iter()
.map(|part| {
part.lines()
.next()
.unwrap()
.trim_end_matches("")
.chars()
.count()
})
.collect::<Vec<_>>();
let longest = &parts
.iter()
.enumerate()
.fold(0, |acc, (index, _)| acc.max(lengths[index]));
let sep = "".repeat(longest + 2);
writeln!(f, "┌{}┐", &sep)?;
for (index, part) in parts.iter().enumerate() {
writeln!(
f,
"│ {}{} │",
part.trim_end_matches(""),
" ".repeat(
longest
- part
.lines()
.last()
.unwrap()
.trim_end_matches("")
.chars()
.count()
)
)?;
if index < parts.len() - 1 {
writeln!(f, "┝{}┥", &sep)?;
}
}
write!(f, "└{}┘", &sep)?;
Ok(())
}
}

View file

@ -1,39 +0,0 @@
use std::fmt;
/// Enum representing different kinds of tasks
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TaskKind {
Instant,
Deferred,
Future,
}
#[allow(dead_code)]
impl TaskKind {
pub fn is_instant(&self) -> bool {
*self == Self::Instant
}
pub fn is_deferred(&self) -> bool {
*self == Self::Deferred
}
pub fn is_blocking(&self) -> bool {
*self != Self::Future
}
pub fn is_future(&self) -> bool {
*self == Self::Future
}
}
impl fmt::Display for TaskKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name: &'static str = match self {
TaskKind::Instant => "Instant",
TaskKind::Deferred => "Deferred",
TaskKind::Future => "Future",
};
write!(f, "{name}")
}
}

View file

@ -1,51 +0,0 @@
use std::{
fmt,
hash::{Hash, Hasher},
};
use mlua::prelude::*;
use super::task_kind::TaskKind;
/// A lightweight, copyable struct that represents a
/// task in the scheduler and is accessible from Lua
#[derive(Debug, Clone, Copy)]
pub struct TaskReference {
kind: TaskKind,
guid: usize,
}
impl TaskReference {
pub const fn new(kind: TaskKind, guid: usize) -> Self {
Self { kind, guid }
}
pub const fn id(&self) -> usize {
self.guid
}
}
impl fmt::Display for TaskReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.guid == 0 {
write!(f, "TaskReference(MAIN)")
} else {
write!(f, "TaskReference({} - {})", self.kind, self.guid)
}
}
}
impl Eq for TaskReference {}
impl PartialEq for TaskReference {
fn eq(&self, other: &Self) -> bool {
self.guid == other.guid
}
}
impl Hash for TaskReference {
fn hash<H: Hasher>(&self, state: &mut H) {
self.guid.hash(state);
}
}
impl LuaUserData for TaskReference {}

View file

@ -1,66 +0,0 @@
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll, Waker},
};
use tokio::sync::Mutex as AsyncMutex;
use mlua::prelude::*;
#[derive(Debug, Clone)]
pub(super) struct TaskWaiterState<'fut> {
rets: Option<LuaResult<LuaMultiValue<'fut>>>,
waker: Option<Waker>,
}
impl<'fut> TaskWaiterState<'fut> {
pub fn new() -> Arc<AsyncMutex<Self>> {
Arc::new(AsyncMutex::new(TaskWaiterState {
rets: None,
waker: None,
}))
}
pub fn finalize(&mut self, rets: LuaResult<LuaMultiValue<'fut>>) {
self.rets = Some(rets);
if let Some(waker) = self.waker.take() {
waker.wake();
}
}
}
#[derive(Debug)]
pub(super) struct TaskWaiterFuture<'fut> {
state: Arc<AsyncMutex<TaskWaiterState<'fut>>>,
}
impl<'fut> TaskWaiterFuture<'fut> {
pub fn new(state: &Arc<AsyncMutex<TaskWaiterState<'fut>>>) -> Self {
Self {
state: Arc::clone(state),
}
}
}
impl<'fut> Clone for TaskWaiterFuture<'fut> {
fn clone(&self) -> Self {
Self {
state: Arc::clone(&self.state),
}
}
}
impl<'fut> Future for TaskWaiterFuture<'fut> {
type Output = LuaResult<LuaMultiValue<'fut>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut shared_state = self.state.try_lock().unwrap();
if let Some(rets) = shared_state.rets.clone() {
Poll::Ready(rets)
} else {
shared_state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}

View file

@ -1,109 +0,0 @@
use std::process::ExitCode;
use lua::task::{TaskScheduler, TaskSchedulerResumeExt, TaskSchedulerScheduleExt};
use mlua::prelude::*;
use mlua::Compiler as LuaCompiler;
use tokio::task::LocalSet;
pub mod builtins;
pub mod importer;
pub mod lua;
mod error;
pub use error::LuneError;
#[derive(Clone, Debug, Default)]
pub struct Lune {
args: Vec<String>,
}
impl Lune {
/**
Creates a new Lune script runner.
*/
pub fn new() -> Self {
Self::default()
}
/**
Arguments to give in `process.args` for a Lune script.
*/
pub fn with_args<V>(mut self, args: V) -> Self
where
V: Into<Vec<String>>,
{
self.args = args.into();
self
}
/**
Runs a Lune script.
This will create a new sandboxed Luau environment with the configured
globals and arguments, running inside of a [`tokio::task::LocalSet`].
Some Lune globals may spawn separate tokio tasks on other threads, but the Luau
environment itself is guaranteed to run on a single thread in the local set.
Note that this will create a static Lua instance and task scheduler that will
both live for the remainer of the program, and that this leaks memory using
[`Box::leak`] that will then get deallocated when the program exits.
*/
pub async fn run(
&self,
script_name: impl AsRef<str>,
script_contents: impl AsRef<[u8]>,
) -> Result<ExitCode, LuneError> {
self.run_inner(script_name, script_contents)
.await
.map_err(LuneError::from_lua_error)
}
async fn run_inner(
&self,
script_name: impl AsRef<str>,
script_contents: impl AsRef<[u8]>,
) -> Result<ExitCode, LuaError> {
// Create our special lune-flavored Lua object with extra registry values
let lua = lua::create_lune_lua()?;
let script = LuaCompiler::default().compile(script_contents);
// Create our task scheduler and all globals
// NOTE: Some globals require the task scheduler to exist on startup
let sched = TaskScheduler::new(lua)?.into_static();
lua.set_app_data(sched);
importer::create(lua, self.args.clone())?;
// Create the main thread and schedule it
let main_chunk = lua
.load(script)
.set_name(script_name.as_ref())
.into_function()?;
let main_thread = lua.create_thread(main_chunk)?;
let main_thread_args = LuaValue::Nil.into_lua_multi(lua)?;
sched.schedule_blocking(main_thread, main_thread_args)?;
// Keep running the scheduler until there are either no tasks
// left to run, or until a task requests to exit the process
let exit_code = LocalSet::new()
.run_until(async move {
let mut got_error = false;
loop {
let result = sched.resume_queue().await;
if let Some(err) = result.get_lua_error() {
eprintln!("{}", LuneError::from_lua_error(err));
got_error = true;
}
if result.is_done() {
if let Some(exit_code) = result.get_exit_code() {
break exit_code;
} else if got_error {
break ExitCode::FAILURE;
} else {
break ExitCode::SUCCESS;
}
}
}
})
.await;
Ok(exit_code)
}
}

View file

@ -1,38 +0,0 @@
#![deny(clippy::all)]
#![warn(clippy::cargo, clippy::pedantic)]
#![allow(
clippy::cargo_common_metadata,
clippy::match_bool,
clippy::module_name_repetitions,
clippy::multiple_crate_versions,
clippy::needless_pass_by_value
)]
use std::process::ExitCode;
use clap::Parser;
pub(crate) mod cli;
use cli::Cli;
use console::style;
#[tokio::main(flavor = "multi_thread")]
async fn main() -> ExitCode {
let logger_env = env_logger::Env::default().default_filter_or("error");
env_logger::Builder::from_env(logger_env)
.format_timestamp(None)
.init();
match Cli::parse().run().await {
Ok(code) => code,
Err(err) => {
eprintln!(
"{}{}{}\n{err:?}",
style("[").dim(),
style("ERROR").red(),
style("]").dim(),
);
ExitCode::FAILURE
}
}
}

View file

@ -1,56 +0,0 @@
use mlua::prelude::*;
use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
use super::extension::DomValueExt;
pub fn ensure_valid_attribute_name(name: impl AsRef<str>) -> LuaResult<()> {
let name = name.as_ref();
if name.to_ascii_uppercase().starts_with("RBX") {
Err(LuaError::RuntimeError(
"Attribute names must not start with the prefix \"RBX\"".to_string(),
))
} else if !name.chars().all(|c| c == '_' || c.is_alphanumeric()) {
Err(LuaError::RuntimeError(
"Attribute names must only use alphanumeric characters and underscore".to_string(),
))
} else if name.len() > 100 {
Err(LuaError::RuntimeError(
"Attribute names must be 100 characters or less in length".to_string(),
))
} else {
Ok(())
}
}
pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
let is_valid = matches!(
value.ty(),
DomType::Bool
| DomType::BrickColor
| DomType::CFrame
| DomType::Color3
| DomType::ColorSequence
| DomType::Float32
| DomType::Float64
| DomType::Font
| DomType::Int32
| DomType::Int64
| DomType::NumberRange
| DomType::NumberSequence
| DomType::Rect
| DomType::String
| DomType::UDim
| DomType::UDim2
| DomType::Vector2
| DomType::Vector3
);
if is_valid {
Ok(())
} else {
Err(LuaError::RuntimeError(format!(
"'{}' is not a valid attribute type",
value.ty().variant_name().unwrap_or("???")
)))
}
}

View file

@ -1,340 +0,0 @@
use mlua::prelude::*;
use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
use crate::roblox::{datatypes::extension::DomValueExt, instance::Instance};
use super::*;
pub(crate) trait LuaToDomValue<'lua> {
/**
Converts a lua value into a weak dom value.
If a `variant_type` is given the conversion will be more strict
and also more accurate, it should be given whenever possible.
*/
fn lua_to_dom_value(
&self,
lua: &'lua Lua,
variant_type: Option<DomType>,
) -> DomConversionResult<DomValue>;
}
pub(crate) trait DomValueToLua<'lua>: Sized {
/**
Converts a weak dom value into a lua value.
*/
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self>;
}
/*
Blanket trait implementations for converting between LuaValue and rbx_dom Variant values
These should be considered stable and done, already containing all of the known primitives
See bottom of module for implementations between our custom datatypes and lua userdata
*/
impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self> {
use rbx_dom_weak::types as dom;
match LuaAnyUserData::dom_value_to_lua(lua, variant) {
Ok(value) => Ok(LuaValue::UserData(value)),
Err(e) => match variant {
DomValue::Bool(b) => Ok(LuaValue::Boolean(*b)),
DomValue::Int64(i) => Ok(LuaValue::Number(*i as f64)),
DomValue::Int32(i) => Ok(LuaValue::Number(*i as f64)),
DomValue::Float64(n) => Ok(LuaValue::Number(*n)),
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::Content(s) => Ok(LuaValue::String(
lua.create_string(AsRef::<str>::as_ref(s))?,
)),
// NOTE: Dom references may point to instances that
// no longer exist, so we handle that here instead of
// in the userdata conversion to be able to return nils
DomValue::Ref(value) => match Instance::new_opt(*value) {
Some(inst) => Ok(inst.into_lua(lua)?),
None => Ok(LuaValue::Nil),
},
// NOTE: Some values are either optional or default and we should handle
// that properly here since the userdata conversion above will always fail
DomValue::OptionalCFrame(None) => Ok(LuaValue::Nil),
DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => Ok(LuaValue::Nil),
_ => Err(e),
},
}
}
}
impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
fn lua_to_dom_value(
&self,
lua: &'lua Lua,
variant_type: Option<DomType>,
) -> DomConversionResult<DomValue> {
use rbx_dom_weak::types as dom;
if let Some(variant_type) = variant_type {
match (self, variant_type) {
(LuaValue::Boolean(b), DomType::Bool) => Ok(DomValue::Bool(*b)),
(LuaValue::Integer(i), DomType::Int64) => Ok(DomValue::Int64(*i as i64)),
(LuaValue::Integer(i), DomType::Int32) => Ok(DomValue::Int32(*i)),
(LuaValue::Integer(i), DomType::Float64) => Ok(DomValue::Float64(*i as f64)),
(LuaValue::Integer(i), DomType::Float32) => Ok(DomValue::Float32(*i as f32)),
(LuaValue::Number(n), DomType::Int64) => Ok(DomValue::Int64(*n as i64)),
(LuaValue::Number(n), DomType::Int32) => Ok(DomValue::Int32(*n as i32)),
(LuaValue::Number(n), DomType::Float64) => Ok(DomValue::Float64(*n)),
(LuaValue::Number(n), DomType::Float32) => Ok(DomValue::Float32(*n as f32)),
(LuaValue::String(s), DomType::String) => {
Ok(DomValue::String(s.to_str()?.to_string()))
}
(LuaValue::String(s), DomType::BinaryString) => {
Ok(DomValue::BinaryString(s.as_ref().into()))
}
(LuaValue::String(s), DomType::Content) => {
Ok(DomValue::Content(s.to_str()?.to_string().into()))
}
// NOTE: Some values are either optional or default and we
// should handle that here before trying to convert as userdata
(LuaValue::Nil, DomType::OptionalCFrame) => Ok(DomValue::OptionalCFrame(None)),
(LuaValue::Nil, DomType::PhysicalProperties) => Ok(DomValue::PhysicalProperties(
dom::PhysicalProperties::Default,
)),
(LuaValue::UserData(u), d) => u.lua_to_dom_value(lua, Some(d)),
(v, d) => Err(DomConversionError::ToDomValue {
to: d.variant_name().unwrap_or("???"),
from: v.type_name(),
detail: None,
}),
}
} else {
match self {
LuaValue::Boolean(b) => Ok(DomValue::Bool(*b)),
LuaValue::Integer(i) => Ok(DomValue::Int32(*i)),
LuaValue::Number(n) => Ok(DomValue::Float64(*n)),
LuaValue::String(s) => Ok(DomValue::String(s.to_str()?.to_string())),
LuaValue::UserData(u) => u.lua_to_dom_value(lua, None),
v => Err(DomConversionError::ToDomValue {
to: "unknown",
from: v.type_name(),
detail: None,
}),
}
}
}
}
/*
Trait implementations for converting between all of
our custom datatypes and generic Lua userdata values
NOTE: When adding a new datatype, make sure to add it below to _both_
of the traits and not just one to allow for bidirectional conversion
*/
macro_rules! dom_to_userdata {
($lua:expr, $value:ident => $to_type:ty) => {
Ok($lua.create_userdata(Into::<$to_type>::into($value.clone()))?)
};
}
/**
Converts a generic lua userdata to an rbx-dom type.
Since the type of the userdata needs to be specified
in an explicit manner, this macro syntax was chosen:
```rs
userdata_to_dom!(value_identifier as UserdataType => DomType)
```
*/
macro_rules! userdata_to_dom {
($userdata:ident as $from_type:ty => $to_type:ty) => {
match $userdata.borrow::<$from_type>() {
Ok(value) => Ok(From::<$to_type>::from(value.clone().into())),
Err(error) => match error {
LuaError::UserDataTypeMismatch => Err(DomConversionError::ToDomValue {
to: stringify!($to_type),
from: "userdata",
detail: Some("Type mismatch".to_string()),
}),
e => Err(DomConversionError::ToDomValue {
to: stringify!($to_type),
from: "userdata",
detail: Some(format!("Internal error: {e}")),
}),
},
}
};
}
impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
#[rustfmt::skip]
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self> {
use super::types::*;
use rbx_dom_weak::types as dom;
match variant {
DomValue::Axes(value) => dom_to_userdata!(lua, value => Axes),
DomValue::BrickColor(value) => dom_to_userdata!(lua, value => BrickColor),
DomValue::CFrame(value) => dom_to_userdata!(lua, value => CFrame),
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3),
DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
DomValue::NumberSequence(value) => dom_to_userdata!(lua, value => NumberSequence),
DomValue::Ray(value) => dom_to_userdata!(lua, value => Ray),
DomValue::Rect(value) => dom_to_userdata!(lua, value => Rect),
DomValue::Region3(value) => dom_to_userdata!(lua, value => Region3),
DomValue::Region3int16(value) => dom_to_userdata!(lua, value => Region3int16),
DomValue::UDim(value) => dom_to_userdata!(lua, value => UDim),
DomValue::UDim2(value) => dom_to_userdata!(lua, value => UDim2),
DomValue::Vector2(value) => dom_to_userdata!(lua, value => Vector2),
DomValue::Vector2int16(value) => dom_to_userdata!(lua, value => Vector2int16),
DomValue::Vector3(value) => dom_to_userdata!(lua, value => Vector3),
DomValue::Vector3int16(value) => dom_to_userdata!(lua, value => Vector3int16),
// NOTE: The none and default variants of these types are handled in
// DomValueToLua for the LuaValue type instead, allowing for nil/default
DomValue::OptionalCFrame(Some(value)) => dom_to_userdata!(lua, value => CFrame),
DomValue::PhysicalProperties(dom::PhysicalProperties::Custom(value)) => {
dom_to_userdata!(lua, value => PhysicalProperties)
},
v => {
Err(DomConversionError::FromDomValue {
from: v.variant_name().unwrap_or("???"),
to: "userdata",
detail: Some("Type not supported".to_string()),
})
}
}
}
}
impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
#[rustfmt::skip]
fn lua_to_dom_value(
&self,
_: &'lua Lua,
variant_type: Option<DomType>,
) -> DomConversionResult<DomValue> {
use super::types::*;
use rbx_dom_weak::types as dom;
if let Some(variant_type) = variant_type {
/*
Strict target type, use it to skip checking the actual
type of the userdata and try to just do a pure conversion
*/
match variant_type {
DomType::Axes => userdata_to_dom!(self as Axes => dom::Axes),
DomType::BrickColor => userdata_to_dom!(self as BrickColor => dom::BrickColor),
DomType::CFrame => userdata_to_dom!(self as CFrame => dom::CFrame),
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum),
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
DomType::Font => userdata_to_dom!(self as Font => dom::Font),
DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange),
DomType::NumberSequence => userdata_to_dom!(self as NumberSequence => dom::NumberSequence),
DomType::Ray => userdata_to_dom!(self as Ray => dom::Ray),
DomType::Rect => userdata_to_dom!(self as Rect => dom::Rect),
DomType::Ref => userdata_to_dom!(self as Instance => dom::Ref),
DomType::Region3 => userdata_to_dom!(self as Region3 => dom::Region3),
DomType::Region3int16 => userdata_to_dom!(self as Region3int16 => dom::Region3int16),
DomType::UDim => userdata_to_dom!(self as UDim => dom::UDim),
DomType::UDim2 => userdata_to_dom!(self as UDim2 => dom::UDim2),
DomType::Vector2 => userdata_to_dom!(self as Vector2 => dom::Vector2),
DomType::Vector2int16 => userdata_to_dom!(self as Vector2int16 => dom::Vector2int16),
DomType::Vector3 => userdata_to_dom!(self as Vector3 => dom::Vector3),
DomType::Vector3int16 => userdata_to_dom!(self as Vector3int16 => dom::Vector3int16),
// NOTE: The none and default variants of these types are handled in
// LuaToDomValue for the LuaValue type instead, allowing for nil/default
DomType::OptionalCFrame => {
return match self.borrow::<CFrame>() {
Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"),
Ok(value) => Ok(DomValue::OptionalCFrame(Some(dom::CFrame::from(*value)))),
}
}
DomType::PhysicalProperties => {
return match self.borrow::<PhysicalProperties>() {
Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"),
Ok(value) => {
let props = dom::CustomPhysicalProperties::from(*value);
let custom = dom::PhysicalProperties::Custom(props);
Ok(DomValue::PhysicalProperties(custom))
}
}
}
ty => {
return Err(DomConversionError::ToDomValue {
to: ty.variant_name().unwrap_or("???"),
from: "userdata",
detail: Some("Type not supported".to_string()),
})
}
}
} else {
/*
Non-strict target type, here we need to do manual typechecks
on the userdata to see what we should be converting it into
This is used for example for attributes, where the wanted
type is not known by the dom and instead determined by the user
*/
match self {
value if value.is::<Axes>() => userdata_to_dom!(value as Axes => dom::Axes),
value if value.is::<BrickColor>() => userdata_to_dom!(value as BrickColor => dom::BrickColor),
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame),
value if value.is::<Color3>() => userdata_to_dom!(value as Color3 => dom::Color3),
value if value.is::<ColorSequence>() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence),
value if value.is::<Enum>() => userdata_to_dom!(value as EnumItem => dom::Enum),
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces),
value if value.is::<Font>() => userdata_to_dom!(value as Font => dom::Font),
value if value.is::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),
value if value.is::<NumberRange>() => userdata_to_dom!(value as NumberRange => dom::NumberRange),
value if value.is::<NumberSequence>() => userdata_to_dom!(value as NumberSequence => dom::NumberSequence),
value if value.is::<Ray>() => userdata_to_dom!(value as Ray => dom::Ray),
value if value.is::<Rect>() => userdata_to_dom!(value as Rect => dom::Rect),
value if value.is::<Region3>() => userdata_to_dom!(value as Region3 => dom::Region3),
value if value.is::<Region3int16>() => userdata_to_dom!(value as Region3int16 => dom::Region3int16),
value if value.is::<UDim>() => userdata_to_dom!(value as UDim => dom::UDim),
value if value.is::<UDim2>() => userdata_to_dom!(value as UDim2 => dom::UDim2),
value if value.is::<Vector2>() => userdata_to_dom!(value as Vector2 => dom::Vector2),
value if value.is::<Vector2int16>() => userdata_to_dom!(value as Vector2int16 => dom::Vector2int16),
value if value.is::<Vector3>() => userdata_to_dom!(value as Vector3 => dom::Vector3),
value if value.is::<Vector3int16>() => userdata_to_dom!(value as Vector3int16 => dom::Vector3int16),
_ => Err(DomConversionError::ToDomValue {
to: "unknown",
from: "userdata",
detail: Some("Type not supported".to_string()),
})
}
}
}
}

View file

@ -1,101 +0,0 @@
use mlua::prelude::*;
use crate::roblox::instance::Instance;
use super::*;
pub(crate) trait DomValueExt {
fn variant_name(&self) -> Option<&'static str>;
}
impl DomValueExt for DomType {
fn variant_name(&self) -> Option<&'static str> {
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",
Enum => "Enum",
Faces => "Faces",
Float32 => "Float32",
Float64 => "Float64",
Font => "Font",
Int32 => "Int32",
Int64 => "Int64",
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",
_ => return None,
})
}
}
impl DomValueExt for DomValue {
fn variant_name(&self) -> Option<&'static str> {
self.ty().variant_name()
}
}
pub trait RobloxUserdataTypenameExt {
fn roblox_type_name(&self) -> Option<&'static str>;
}
impl<'lua> RobloxUserdataTypenameExt for LuaAnyUserData<'lua> {
#[rustfmt::skip]
fn roblox_type_name(&self) -> Option<&'static str> {
use super::types::*;
Some(match self {
value if value.is::<Axes>() => "Axes",
value if value.is::<BrickColor>() => "BrickColor",
value if value.is::<CFrame>() => "CFrame",
value if value.is::<Color3>() => "Color3",
value if value.is::<ColorSequence>() => "ColorSequence",
value if value.is::<ColorSequenceKeypoint>() => "ColorSequenceKeypoint",
value if value.is::<Enums>() => "Enums",
value if value.is::<Enum>() => "Enum",
value if value.is::<EnumItem>() => "EnumItem",
value if value.is::<Faces>() => "Faces",
value if value.is::<Font>() => "Font",
value if value.is::<Instance>() => "Instance",
value if value.is::<NumberRange>() => "NumberRange",
value if value.is::<NumberSequence>() => "NumberSequence",
value if value.is::<NumberSequenceKeypoint>() => "NumberSequenceKeypoint",
value if value.is::<PhysicalProperties>() => "PhysicalProperties",
value if value.is::<Ray>() => "Ray",
value if value.is::<Rect>() => "Rect",
value if value.is::<Region3>() => "Region3",
value if value.is::<Region3int16>() => "Region3int16",
value if value.is::<UDim>() => "UDim",
value if value.is::<UDim2>() => "UDim2",
value if value.is::<Vector2>() => "Vector2",
value if value.is::<Vector2int16>() => "Vector2int16",
value if value.is::<Vector3>() => "Vector3",
value if value.is::<Vector3int16>() => "Vector3int16",
_ => return None,
})
}
}

View file

@ -1,11 +0,0 @@
pub(crate) use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
pub mod attributes;
pub mod conversion;
pub mod extension;
pub mod result;
pub mod types;
use result::*;
pub use crate::roblox::shared::userdata::*;

View file

@ -1,75 +0,0 @@
use core::fmt;
use std::error::Error;
use std::io::Error as IoError;
use mlua::Error as LuaError;
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub(crate) enum DomConversionError {
LuaError(LuaError),
External {
message: String,
},
FromDomValue {
from: &'static str,
to: &'static str,
detail: Option<String>,
},
ToDomValue {
to: &'static str,
from: &'static str,
detail: Option<String>,
},
}
impl fmt::Display for DomConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::LuaError(error) => error.to_string(),
Self::External { message } => message.to_string(),
Self::FromDomValue { from, to, detail } | Self::ToDomValue { from, to, detail } => {
match detail {
Some(d) => format!("Failed to convert from '{from}' into '{to}' - {d}"),
None => format!("Failed to convert from '{from}' into '{to}'",),
}
}
}
)
}
}
impl Error for DomConversionError {}
impl From<DomConversionError> for LuaError {
fn from(value: DomConversionError) -> Self {
use DomConversionError as E;
match value {
E::LuaError(e) => e,
E::External { message } => LuaError::external(message),
E::FromDomValue { .. } | E::ToDomValue { .. } => {
LuaError::RuntimeError(value.to_string())
}
}
}
}
impl From<LuaError> for DomConversionError {
fn from(value: LuaError) -> Self {
Self::LuaError(value)
}
}
impl From<IoError> for DomConversionError {
fn from(value: IoError) -> Self {
DomConversionError::External {
message: value.to_string(),
}
}
}
pub(crate) type DomConversionResult<T> = Result<T, DomConversionError>;

View file

@ -1,118 +0,0 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::Axes as DomAxes;
use super::{super::*, EnumItem};
/**
An implementation of the [Axes](https://create.roblox.com/docs/reference/engine/datatypes/Axes) Roblox datatype.
This implements all documented properties, methods & constructors of the Axes class as of March 2023.
*/
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Axes {
pub(crate) x: bool,
pub(crate) y: bool,
pub(crate) z: bool,
}
impl Axes {
pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> {
datatype_table.set(
"new",
lua.create_function(|_, args: LuaMultiValue| {
let mut x = false;
let mut y = false;
let mut z = false;
let mut check = |e: &EnumItem| {
if e.parent.desc.name == "Axis" {
match &e.name {
name if name == "X" => x = true,
name if name == "Y" => y = true,
name if name == "Z" => z = true,
_ => {}
}
} else if e.parent.desc.name == "NormalId" {
match &e.name {
name if name == "Left" || name == "Right" => x = true,
name if name == "Top" || name == "Bottom" => y = true,
name if name == "Front" || name == "Back" => z = true,
_ => {}
}
}
};
for (index, arg) in args.into_iter().enumerate() {
if let LuaValue::UserData(u) = arg {
if let Ok(e) = u.borrow::<EnumItem>() {
check(&e);
} else {
return Err(LuaError::RuntimeError(format!(
"Expected argument #{} to be an EnumItem, got userdata",
index
)));
}
} else {
return Err(LuaError::RuntimeError(format!(
"Expected argument #{} to be an EnumItem, got {}",
index,
arg.type_name()
)));
}
}
Ok(Axes { x, y, z })
})?,
)?;
Ok(())
}
}
impl LuaUserData for Axes {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("X", |_, this| Ok(this.x));
fields.add_field_method_get("Y", |_, this| Ok(this.y));
fields.add_field_method_get("Z", |_, this| Ok(this.z));
fields.add_field_method_get("Left", |_, this| Ok(this.x));
fields.add_field_method_get("Right", |_, this| Ok(this.x));
fields.add_field_method_get("Top", |_, this| Ok(this.y));
fields.add_field_method_get("Bottom", |_, this| Ok(this.y));
fields.add_field_method_get("Front", |_, this| Ok(this.z));
fields.add_field_method_get("Back", |_, this| Ok(this.z));
}
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 Axes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let write = make_list_writer();
write(f, self.x, "X")?;
write(f, self.y, "Y")?;
write(f, self.z, "Z")?;
Ok(())
}
}
impl From<DomAxes> for Axes {
fn from(v: DomAxes) -> Self {
let bits = v.bits();
Self {
x: (bits & 1) == 1,
y: ((bits >> 1) & 1) == 1,
z: ((bits >> 2) & 1) == 1,
}
}
}
impl From<Axes> for DomAxes {
fn from(v: Axes) -> Self {
let mut bits = 0;
bits += v.x as u8;
bits += (v.y as u8) << 1;
bits += (v.z as u8) << 2;
DomAxes::from_bits(bits).expect("Invalid bits")
}
}

View file

@ -1,436 +0,0 @@
use core::fmt;
use mlua::prelude::*;
use rand::seq::SliceRandom;
use rbx_dom_weak::types::BrickColor as DomBrickColor;
use super::{super::*, Color3};
/**
An implementation of the [BrickColor](https://create.roblox.com/docs/reference/engine/datatypes/BrickColor) Roblox datatype.
This implements all documented properties, methods & constructors of the BrickColor class as of March 2023.
*/
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BrickColor {
// Unfortunately we can't use DomBrickColor as the backing type here
// because it does not expose any way of getting the actual rgb colors :-(
pub(crate) number: u16,
pub(crate) name: &'static str,
pub(crate) rgb: (u8, u8, u8),
}
impl BrickColor {
pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> {
type ArgsNumber = u16;
type ArgsName = String;
type ArgsRgb = (u8, u8, u8);
type ArgsColor3<'lua> = LuaUserDataRef<'lua, Color3>;
datatype_table.set(
"new",
lua.create_function(|lua, args: LuaMultiValue| {
if let Ok(number) = ArgsNumber::from_lua_multi(args.clone(), lua) {
Ok(color_from_number(number))
} else if let Ok(name) = ArgsName::from_lua_multi(args.clone(), lua) {
Ok(color_from_name(name))
} else if let Ok((r, g, b)) = ArgsRgb::from_lua_multi(args.clone(), lua) {
Ok(color_from_rgb(r, g, b))
} else if let Ok(color) = ArgsColor3::from_lua_multi(args.clone(), lua) {
Ok(Self::from(*color))
} else {
// FUTURE: Better error message here using given arg types
Err(LuaError::RuntimeError(
"Invalid arguments to constructor".to_string(),
))
}
})?,
)?;
datatype_table.set(
"palette",
lua.create_function(|_, index: u16| {
if index == 0 {
Err(LuaError::RuntimeError("Invalid index".to_string()))
} else if let Some(number) = BRICK_COLOR_PALETTE.get((index - 1) as usize) {
Ok(color_from_number(*number))
} else {
Err(LuaError::RuntimeError("Invalid index".to_string()))
}
})?,
)?;
datatype_table.set(
"random",
lua.create_function(|_, ()| {
let number = BRICK_COLOR_PALETTE.choose(&mut rand::thread_rng());
Ok(color_from_number(*number.unwrap()))
})?,
)?;
for (name, number) in BRICK_COLOR_CONSTRUCTORS {
datatype_table.set(
*name,
lua.create_function(|_, ()| Ok(color_from_number(*number)))?,
)?;
}
Ok(())
}
}
impl LuaUserData for BrickColor {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("Number", |_, this| Ok(this.number));
fields.add_field_method_get("Name", |_, this| Ok(this.name));
fields.add_field_method_get("R", |_, this| Ok(this.rgb.0 as f32 / 255f32));
fields.add_field_method_get("G", |_, this| Ok(this.rgb.1 as f32 / 255f32));
fields.add_field_method_get("B", |_, this| Ok(this.rgb.2 as f32 / 255f32));
fields.add_field_method_get("r", |_, this| Ok(this.rgb.0 as f32 / 255f32));
fields.add_field_method_get("g", |_, this| Ok(this.rgb.1 as f32 / 255f32));
fields.add_field_method_get("b", |_, this| Ok(this.rgb.2 as f32 / 255f32));
fields.add_field_method_get("Color", |_, this| Ok(Color3::from(*this)));
}
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 Default for BrickColor {
fn default() -> Self {
color_from_number(BRICK_COLOR_DEFAULT)
}
}
impl fmt::Display for BrickColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl From<Color3> for BrickColor {
fn from(value: Color3) -> Self {
let r = value.r.clamp(u8::MIN as f32, u8::MAX as f32) as u8;
let g = value.g.clamp(u8::MIN as f32, u8::MAX as f32) as u8;
let b = value.b.clamp(u8::MIN as f32, u8::MAX as f32) as u8;
color_from_rgb(r, g, b)
}
}
impl From<BrickColor> for Color3 {
fn from(value: BrickColor) -> Self {
Self {
r: (value.rgb.0 as f32) / 255.0,
g: (value.rgb.1 as f32) / 255.0,
b: (value.rgb.2 as f32) / 255.0,
}
}
}
impl From<DomBrickColor> for BrickColor {
fn from(v: DomBrickColor) -> Self {
color_from_name(v.to_string())
}
}
impl From<BrickColor> for DomBrickColor {
fn from(v: BrickColor) -> Self {
DomBrickColor::from_number(v.number).unwrap_or(DomBrickColor::MediumStoneGrey)
}
}
/*
NOTE: The brick color definitions below are generated using
the brick_color script in the scripts dir next to src, which can
be ran using `cargo run packages/lib-roblox/scripts/brick_color`
*/
type BrickColorDef = &'static (u16, &'static str, (u8, u8, u8));
impl From<BrickColorDef> for BrickColor {
fn from(value: BrickColorDef) -> Self {
Self {
number: value.0,
name: value.1,
rgb: value.2,
}
}
}
const BRICK_COLOR_DEFAULT_VALUE: BrickColorDef =
&BRICK_COLOR_VALUES[(BRICK_COLOR_DEFAULT - 1) as usize];
fn color_from_number(index: u16) -> BrickColor {
BRICK_COLOR_VALUES
.iter()
.find(|color| color.0 == index)
.unwrap_or(BRICK_COLOR_DEFAULT_VALUE)
.into()
}
fn color_from_name(name: impl AsRef<str>) -> BrickColor {
let name = name.as_ref();
BRICK_COLOR_VALUES
.iter()
.find(|color| color.1 == name)
.unwrap_or(BRICK_COLOR_DEFAULT_VALUE)
.into()
}
fn color_from_rgb(r: u8, g: u8, b: u8) -> BrickColor {
let r = r as i16;
let g = g as i16;
let b = b as i16;
BRICK_COLOR_VALUES
.iter()
.fold(
(None, u16::MAX),
|(closest_color, closest_distance), color| {
let cr = color.2 .0 as i16;
let cg = color.2 .1 as i16;
let cb = color.2 .2 as i16;
let distance = ((r - cr) + (g - cg) + (b - cb)).unsigned_abs();
if distance < closest_distance {
(Some(color), distance)
} else {
(closest_color, closest_distance)
}
},
)
.0
.unwrap_or(BRICK_COLOR_DEFAULT_VALUE)
.into()
}
const BRICK_COLOR_DEFAULT: u16 = 194;
const BRICK_COLOR_VALUES: &[(u16, &str, (u8, u8, u8))] = &[
(1, "White", (242, 243, 243)),
(2, "Grey", (161, 165, 162)),
(3, "Light yellow", (249, 233, 153)),
(5, "Brick yellow", (215, 197, 154)),
(6, "Light green (Mint)", (194, 218, 184)),
(9, "Light reddish violet", (232, 186, 200)),
(11, "Pastel Blue", (128, 187, 219)),
(12, "Light orange brown", (203, 132, 66)),
(18, "Nougat", (204, 142, 105)),
(21, "Bright red", (196, 40, 28)),
(22, "Med. reddish violet", (196, 112, 160)),
(23, "Bright blue", (13, 105, 172)),
(24, "Bright yellow", (245, 205, 48)),
(25, "Earth orange", (98, 71, 50)),
(26, "Black", (27, 42, 53)),
(27, "Dark grey", (109, 110, 108)),
(28, "Dark green", (40, 127, 71)),
(29, "Medium green", (161, 196, 140)),
(36, "Lig. Yellowich orange", (243, 207, 155)),
(37, "Bright green", (75, 151, 75)),
(38, "Dark orange", (160, 95, 53)),
(39, "Light bluish violet", (193, 202, 222)),
(40, "Transparent", (236, 236, 236)),
(41, "Tr. Red", (205, 84, 75)),
(42, "Tr. Lg blue", (193, 223, 240)),
(43, "Tr. Blue", (123, 182, 232)),
(44, "Tr. Yellow", (247, 241, 141)),
(45, "Light blue", (180, 210, 228)),
(47, "Tr. Flu. Reddish orange", (217, 133, 108)),
(48, "Tr. Green", (132, 182, 141)),
(49, "Tr. Flu. Green", (248, 241, 132)),
(50, "Phosph. White", (236, 232, 222)),
(100, "Light red", (238, 196, 182)),
(101, "Medium red", (218, 134, 122)),
(102, "Medium blue", (110, 153, 202)),
(103, "Light grey", (199, 193, 183)),
(104, "Bright violet", (107, 50, 124)),
(105, "Br. yellowish orange", (226, 155, 64)),
(106, "Bright orange", (218, 133, 65)),
(107, "Bright bluish green", (0, 143, 156)),
(108, "Earth yellow", (104, 92, 67)),
(110, "Bright bluish violet", (67, 84, 147)),
(111, "Tr. Brown", (191, 183, 177)),
(112, "Medium bluish violet", (104, 116, 172)),
(113, "Tr. Medi. reddish violet", (229, 173, 200)),
(115, "Med. yellowish green", (199, 210, 60)),
(116, "Med. bluish green", (85, 165, 175)),
(118, "Light bluish green", (183, 215, 213)),
(119, "Br. yellowish green", (164, 189, 71)),
(120, "Lig. yellowish green", (217, 228, 167)),
(121, "Med. yellowish orange", (231, 172, 88)),
(123, "Br. reddish orange", (211, 111, 76)),
(124, "Bright reddish violet", (146, 57, 120)),
(125, "Light orange", (234, 184, 146)),
(126, "Tr. Bright bluish violet", (165, 165, 203)),
(127, "Gold", (220, 188, 129)),
(128, "Dark nougat", (174, 122, 89)),
(131, "Silver", (156, 163, 168)),
(133, "Neon orange", (213, 115, 61)),
(134, "Neon green", (216, 221, 86)),
(135, "Sand blue", (116, 134, 157)),
(136, "Sand violet", (135, 124, 144)),
(137, "Medium orange", (224, 152, 100)),
(138, "Sand yellow", (149, 138, 115)),
(140, "Earth blue", (32, 58, 86)),
(141, "Earth green", (39, 70, 45)),
(143, "Tr. Flu. Blue", (207, 226, 247)),
(145, "Sand blue metallic", (121, 136, 161)),
(146, "Sand violet metallic", (149, 142, 163)),
(147, "Sand yellow metallic", (147, 135, 103)),
(148, "Dark grey metallic", (87, 88, 87)),
(149, "Black metallic", (22, 29, 50)),
(150, "Light grey metallic", (171, 173, 172)),
(151, "Sand green", (120, 144, 130)),
(153, "Sand red", (149, 121, 119)),
(154, "Dark red", (123, 46, 47)),
(157, "Tr. Flu. Yellow", (255, 246, 123)),
(158, "Tr. Flu. Red", (225, 164, 194)),
(168, "Gun metallic", (117, 108, 98)),
(176, "Red flip/flop", (151, 105, 91)),
(178, "Yellow flip/flop", (180, 132, 85)),
(179, "Silver flip/flop", (137, 135, 136)),
(180, "Curry", (215, 169, 75)),
(190, "Fire Yellow", (249, 214, 46)),
(191, "Flame yellowish orange", (232, 171, 45)),
(192, "Reddish brown", (105, 64, 40)),
(193, "Flame reddish orange", (207, 96, 36)),
(194, "Medium stone grey", (163, 162, 165)),
(195, "Royal blue", (70, 103, 164)),
(196, "Dark Royal blue", (35, 71, 139)),
(198, "Bright reddish lilac", (142, 66, 133)),
(199, "Dark stone grey", (99, 95, 98)),
(200, "Lemon metalic", (130, 138, 93)),
(208, "Light stone grey", (229, 228, 223)),
(209, "Dark Curry", (176, 142, 68)),
(210, "Faded green", (112, 149, 120)),
(211, "Turquoise", (121, 181, 181)),
(212, "Light Royal blue", (159, 195, 233)),
(213, "Medium Royal blue", (108, 129, 183)),
(216, "Rust", (144, 76, 42)),
(217, "Brown", (124, 92, 70)),
(218, "Reddish lilac", (150, 112, 159)),
(219, "Lilac", (107, 98, 155)),
(220, "Light lilac", (167, 169, 206)),
(221, "Bright purple", (205, 98, 152)),
(222, "Light purple", (228, 173, 200)),
(223, "Light pink", (220, 144, 149)),
(224, "Light brick yellow", (240, 213, 160)),
(225, "Warm yellowish orange", (235, 184, 127)),
(226, "Cool yellow", (253, 234, 141)),
(232, "Dove blue", (125, 187, 221)),
(268, "Medium lilac", (52, 43, 117)),
(301, "Slime green", (80, 109, 84)),
(302, "Smoky grey", (91, 93, 105)),
(303, "Dark blue", (0, 16, 176)),
(304, "Parsley green", (44, 101, 29)),
(305, "Steel blue", (82, 124, 174)),
(306, "Storm blue", (51, 88, 130)),
(307, "Lapis", (16, 42, 220)),
(308, "Dark indigo", (61, 21, 133)),
(309, "Sea green", (52, 142, 64)),
(310, "Shamrock", (91, 154, 76)),
(311, "Fossil", (159, 161, 172)),
(312, "Mulberry", (89, 34, 89)),
(313, "Forest green", (31, 128, 29)),
(314, "Cadet blue", (159, 173, 192)),
(315, "Electric blue", (9, 137, 207)),
(316, "Eggplant", (123, 0, 123)),
(317, "Moss", (124, 156, 107)),
(318, "Artichoke", (138, 171, 133)),
(319, "Sage green", (185, 196, 177)),
(320, "Ghost grey", (202, 203, 209)),
(321, "Lilac", (167, 94, 155)),
(322, "Plum", (123, 47, 123)),
(323, "Olivine", (148, 190, 129)),
(324, "Laurel green", (168, 189, 153)),
(325, "Quill grey", (223, 223, 222)),
(327, "Crimson", (151, 0, 0)),
(328, "Mint", (177, 229, 166)),
(329, "Baby blue", (152, 194, 219)),
(330, "Carnation pink", (255, 152, 220)),
(331, "Persimmon", (255, 89, 89)),
(332, "Maroon", (117, 0, 0)),
(333, "Gold", (239, 184, 56)),
(334, "Daisy orange", (248, 217, 109)),
(335, "Pearl", (231, 231, 236)),
(336, "Fog", (199, 212, 228)),
(337, "Salmon", (255, 148, 148)),
(338, "Terra Cotta", (190, 104, 98)),
(339, "Cocoa", (86, 36, 36)),
(340, "Wheat", (241, 231, 199)),
(341, "Buttermilk", (254, 243, 187)),
(342, "Mauve", (224, 178, 208)),
(343, "Sunrise", (212, 144, 189)),
(344, "Tawny", (150, 85, 85)),
(345, "Rust", (143, 76, 42)),
(346, "Cashmere", (211, 190, 150)),
(347, "Khaki", (226, 220, 188)),
(348, "Lily white", (237, 234, 234)),
(349, "Seashell", (233, 218, 218)),
(350, "Burgundy", (136, 62, 62)),
(351, "Cork", (188, 155, 93)),
(352, "Burlap", (199, 172, 120)),
(353, "Beige", (202, 191, 163)),
(354, "Oyster", (187, 179, 178)),
(355, "Pine Cone", (108, 88, 75)),
(356, "Fawn brown", (160, 132, 79)),
(357, "Hurricane grey", (149, 137, 136)),
(358, "Cloudy grey", (171, 168, 158)),
(359, "Linen", (175, 148, 131)),
(360, "Copper", (150, 103, 102)),
(361, "Dirt brown", (86, 66, 54)),
(362, "Bronze", (126, 104, 63)),
(363, "Flint", (105, 102, 92)),
(364, "Dark taupe", (90, 76, 66)),
(365, "Burnt Sienna", (106, 57, 9)),
(1001, "Institutional white", (248, 248, 248)),
(1002, "Mid gray", (205, 205, 205)),
(1003, "Really black", (17, 17, 17)),
(1004, "Really red", (255, 0, 0)),
(1005, "Deep orange", (255, 176, 0)),
(1006, "Alder", (180, 128, 255)),
(1007, "Dusty Rose", (163, 75, 75)),
(1008, "Olive", (193, 190, 66)),
(1009, "New Yeller", (255, 255, 0)),
(1010, "Really blue", (0, 0, 255)),
(1011, "Navy blue", (0, 32, 96)),
(1012, "Deep blue", (33, 84, 185)),
(1013, "Cyan", (4, 175, 236)),
(1014, "CGA brown", (170, 85, 0)),
(1015, "Magenta", (170, 0, 170)),
(1016, "Pink", (255, 102, 204)),
(1017, "Deep orange", (255, 175, 0)),
(1018, "Teal", (18, 238, 212)),
(1019, "Toothpaste", (0, 255, 255)),
(1020, "Lime green", (0, 255, 0)),
(1021, "Camo", (58, 125, 21)),
(1022, "Grime", (127, 142, 100)),
(1023, "Lavender", (140, 91, 159)),
(1024, "Pastel light blue", (175, 221, 255)),
(1025, "Pastel orange", (255, 201, 201)),
(1026, "Pastel violet", (177, 167, 255)),
(1027, "Pastel blue-green", (159, 243, 233)),
(1028, "Pastel green", (204, 255, 204)),
(1029, "Pastel yellow", (255, 255, 204)),
(1030, "Pastel brown", (255, 204, 153)),
(1031, "Royal purple", (98, 37, 209)),
(1032, "Hot pink", (255, 0, 191)),
];
const BRICK_COLOR_PALETTE: &[u16] = &[
141, 301, 107, 26, 1012, 303, 1011, 304, 28, 1018, 302, 305, 306, 307, 308, 1021, 309, 310,
1019, 135, 102, 23, 1010, 312, 313, 37, 1022, 1020, 1027, 311, 315, 1023, 1031, 316, 151, 317,
318, 319, 1024, 314, 1013, 1006, 321, 322, 104, 1008, 119, 323, 324, 325, 320, 11, 1026, 1016,
1032, 1015, 327, 1005, 1009, 29, 328, 1028, 208, 45, 329, 330, 331, 1004, 21, 332, 333, 24,
334, 226, 1029, 335, 336, 342, 343, 338, 1007, 339, 133, 106, 340, 341, 1001, 1, 9, 1025, 337,
344, 345, 1014, 105, 346, 347, 348, 349, 1030, 125, 101, 350, 192, 351, 352, 353, 354, 1002, 5,
18, 217, 355, 356, 153, 357, 358, 359, 360, 38, 361, 362, 199, 194, 363, 364, 365, 1003,
];
const BRICK_COLOR_CONSTRUCTORS: &[(&str, u16)] = &[
("Yellow", 24),
("White", 1),
("Black", 26),
("Green", 28),
("Red", 21),
("DarkGray", 199),
("Blue", 23),
("Gray", 194),
];

View file

@ -1,390 +0,0 @@
use core::fmt;
use std::ops;
use glam::{EulerRot, Mat4, Quat, Vec3};
use mlua::prelude::*;
use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3};
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) -> (Vec3, Vec3, Vec3) {
(
self.0.x_axis.truncate(),
self.0.y_axis.truncate(),
self.0.z_axis.truncate(),
)
}
fn inverse(&self) -> Self {
Self(self.0.inverse())
}
pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> {
// Constants
datatype_table.set("identity", CFrame(Mat4::IDENTITY))?;
// Strict args constructors
datatype_table.set(
"lookAt",
lua.create_function(
|_,
(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,
)))
},
)?,
)?;
datatype_table.set(
"fromEulerAnglesXYZ",
lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
})?,
)?;
datatype_table.set(
"fromEulerAnglesYXZ",
lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
})?,
)?;
datatype_table.set(
"Angles",
lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
})?,
)?;
datatype_table.set(
"fromOrientation",
lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
})?,
)?;
datatype_table.set(
"fromAxisAngle",
lua.create_function(|_, (v, r): (LuaUserDataRef<Vector3>, f32)| {
Ok(CFrame(Mat4::from_axis_angle(v.0, r)))
})?,
)?;
datatype_table.set(
"fromMatrix",
lua.create_function(
|_,
(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(|r| r.0)
.unwrap_or_else(|| rx.0.cross(ry.0).normalize())
.extend(0.0),
pos.0.extend(1.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);
datatype_table.set(
"new",
lua.create_function(|lua, args: LuaMultiValue| {
if args.clone().into_vec().is_empty() {
Ok(CFrame(Mat4::IDENTITY))
} else if let Ok(pos) = ArgsPos::from_lua_multi(args.clone(), lua) {
Ok(CFrame(Mat4::from_translation(pos.0)))
} else 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.clone(), lua) {
Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z))))
} else if let Ok((x, y, z, qx, qy, qz, qw)) =
ArgsPosXYZQuat::from_lua_multi(args.clone(), lua)
{
Ok(CFrame(Mat4::from_rotation_translation(
Quat::from_array([qx, qy, qz, qw]),
Vec3::new(x, y, z),
)))
} else if let Ok((x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)) =
ArgsMatrix::from_lua_multi(args, lua)
{
Ok(CFrame(Mat4::from_cols_array_2d(&[
[r00, r01, r02, 0.0],
[r10, r11, r12, 0.0],
[r20, r21, r22, 0.0],
[x, y, z, 1.0],
])))
} else {
// FUTURE: Better error message here using given arg types
Err(LuaError::RuntimeError(
"Invalid arguments to constructor".to_string(),
))
}
})?,
)
}
}
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().0)));
fields.add_field_method_get("YVector", |_, this| Ok(Vector3(this.orientation().1)));
fields.add_field_method_get("ZVector", |_, this| Ok(Vector3(this.orientation().2)));
fields.add_field_method_get("RightVector", |_, this| Ok(Vector3(this.orientation().0)));
fields.add_field_method_get("UpVector", |_, this| Ok(Vector3(this.orientation().1)));
fields.add_field_method_get("LookVector", |_, this| Ok(Vector3(-this.orientation().2)));
}
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: LuaUserDataRef<CFrame>| {
Ok(*this * *rhs)
});
methods.add_method("ToObjectSpace", |_, this, rhs: LuaUserDataRef<CFrame>| {
Ok(this.inverse() * *rhs)
});
methods.add_method(
"PointToWorldSpace",
|_, this, rhs: LuaUserDataRef<Vector3>| Ok(*this * *rhs),
);
methods.add_method(
"PointToObjectSpace",
|_, this, rhs: LuaUserDataRef<Vector3>| Ok(this.inverse() * *rhs),
);
methods.add_method(
"VectorToWorldSpace",
|_, this, rhs: LuaUserDataRef<Vector3>| Ok((*this - Vector3(this.position())) * *rhs),
);
methods.add_method(
"VectorToObjectSpace",
|_, this, rhs: LuaUserDataRef<Vector3>| {
let inv = this.inverse();
Ok((inv - Vector3(inv.position())) * *rhs)
},
);
#[rustfmt::skip]
methods.add_method("GetComponents", |_, this, ()| {
let pos = this.position();
let (rx, ry, rz) = this.orientation();
Ok((
pos.x, pos.y, -pos.z,
rx.x, rx.y, rx.z,
ry.x, ry.y, ry.z,
rz.x, rz.y, rz.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 (rx, ry, rz) = self.orientation();
write!(
f,
"{}, {}, {}, {}",
Vector3(pos),
Vector3(rx),
Vector3(ry),
Vector3(rz)
)
}
}
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 {
CFrame(Mat4::from_cols(
Vector3::from(v.orientation.x).0.extend(0.0),
Vector3::from(v.orientation.y).0.extend(0.0),
Vector3::from(v.orientation.z).0.extend(0.0),
Vector3::from(v.position).0.extend(1.0),
))
}
}
impl From<CFrame> for DomCFrame {
fn from(v: CFrame) -> Self {
let (rx, ry, rz) = v.orientation();
DomCFrame {
position: DomVector3::from(Vector3(v.position())),
orientation: DomMatrix3::new(
DomVector3::from(Vector3(rx)),
DomVector3::from(Vector3(ry)),
DomVector3::from(Vector3(rz)),
),
}
}
}
/**
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),
)
}

View file

@ -1,305 +0,0 @@
use core::fmt;
use std::ops;
use glam::Vec3;
use mlua::prelude::*;
use rbx_dom_weak::types::{Color3 as DomColor3, Color3uint8 as DomColor3uint8};
use super::super::*;
/**
An implementation of the [Color3](https://create.roblox.com/docs/reference/engine/datatypes/Color3) Roblox datatype.
This implements all documented properties, methods & constructors of the Color3 class as of March 2023.
It also implements math operations for addition, subtraction, multiplication, and division,
all of which are suspiciously missing from the Roblox implementation of the Color3 datatype.
*/
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color3 {
pub(crate) r: f32,
pub(crate) g: f32,
pub(crate) b: f32,
}
impl Color3 {
pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> {
datatype_table.set(
"new",
lua.create_function(|_, (r, g, b): (Option<f32>, Option<f32>, Option<f32>)| {
Ok(Color3 {
r: r.unwrap_or_default(),
g: g.unwrap_or_default(),
b: b.unwrap_or_default(),
})
})?,
)?;
datatype_table.set(
"fromRGB",
lua.create_function(|_, (r, g, b): (Option<u8>, Option<u8>, Option<u8>)| {
Ok(Color3 {
r: (r.unwrap_or_default() as f32) / 255f32,
g: (g.unwrap_or_default() as f32) / 255f32,
b: (b.unwrap_or_default() as f32) / 255f32,
})
})?,
)?;
datatype_table.set(
"fromHSV",
lua.create_function(|_, (h, s, v): (f32, f32, f32)| {
// https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
let i = (h * 6.0).floor();
let f = h * 6.0 - i;
let p = v * (1.0 - s);
let q = v * (1.0 - f * s);
let t = v * (1.0 - (1.0 - f) * s);
let (r, g, b) = match (i % 6.0) as u8 {
0 => (v, t, p),
1 => (q, v, p),
2 => (p, v, t),
3 => (p, q, v),
4 => (t, p, v),
5 => (v, p, q),
_ => unreachable!(),
};
Ok(Color3 { r, g, b })
})?,
)?;
datatype_table.set(
"fromHex",
lua.create_function(|_, hex: String| {
let trimmed = hex.trim_start_matches('#').to_ascii_uppercase();
let chars = if trimmed.len() == 3 {
(
u8::from_str_radix(&trimmed[..1].repeat(2), 16),
u8::from_str_radix(&trimmed[1..2].repeat(2), 16),
u8::from_str_radix(&trimmed[2..3].repeat(2), 16),
)
} else if trimmed.len() == 6 {
(
u8::from_str_radix(&trimmed[..2], 16),
u8::from_str_radix(&trimmed[2..4], 16),
u8::from_str_radix(&trimmed[4..6], 16),
)
} else {
return Err(LuaError::RuntimeError(format!(
"Hex color string must be 3 or 6 characters long, got {} character{}",
trimmed.len(),
if trimmed.len() == 1 { "" } else { "s" }
)));
};
match chars {
(Ok(r), Ok(g), Ok(b)) => Ok(Color3 {
r: (r as f32) / 255f32,
g: (g as f32) / 255f32,
b: (b as f32) / 255f32,
}),
_ => Err(LuaError::RuntimeError(format!(
"Hex color string '{}' contains invalid character",
trimmed
))),
}
})?,
)?;
Ok(())
}
}
impl LuaUserData for Color3 {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("R", |_, this| Ok(this.r));
fields.add_field_method_get("G", |_, this| Ok(this.g));
fields.add_field_method_get("B", |_, this| Ok(this.b));
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
// Methods
methods.add_method(
"Lerp",
|_, this, (rhs, alpha): (LuaUserDataRef<Color3>, f32)| {
let v3_this = Vec3::new(this.r, this.g, this.b);
let v3_rhs = Vec3::new(rhs.r, rhs.g, rhs.b);
let v3 = v3_this.lerp(v3_rhs, alpha);
Ok(Color3 {
r: v3.x,
g: v3.y,
b: v3.z,
})
},
);
methods.add_method("ToHSV", |_, this, ()| {
// https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
let (r, g, b) = (this.r, this.g, this.b);
let min = r.min(g).min(b);
let max = r.max(g).max(b);
let diff = max - min;
let hue = (match max {
max if max == min => 0.0,
max if max == r => (g - b) / diff + (if g < b { 6.0 } else { 0.0 }),
max if max == g => (b - r) / diff + 2.0,
max if max == b => (r - g) / diff + 4.0,
_ => unreachable!(),
}) / 6.0;
let sat = if max == 0.0 {
0.0
} else {
(diff / max).clamp(0.0, 1.0)
};
Ok((hue, sat, max))
});
methods.add_method("ToHex", |_, this, ()| {
Ok(format!(
"{:02X}{:02X}{:02X}",
(this.r * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8,
(this.g * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8,
(this.b * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8,
))
});
// 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::Unm, userdata_impl_unm);
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32);
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32);
}
}
impl Default for Color3 {
fn default() -> Self {
Self {
r: 0f32,
g: 0f32,
b: 0f32,
}
}
}
impl fmt::Display for Color3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}, {}, {}", self.r, self.g, self.b)
}
}
impl ops::Neg for Color3 {
type Output = Self;
fn neg(self) -> Self::Output {
Color3 {
r: -self.r,
g: -self.g,
b: -self.b,
}
}
}
impl ops::Add for Color3 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Color3 {
r: self.r + rhs.r,
g: self.g + rhs.g,
b: self.b + rhs.b,
}
}
}
impl ops::Sub for Color3 {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Color3 {
r: self.r - rhs.r,
g: self.g - rhs.g,
b: self.b - rhs.b,
}
}
}
impl ops::Mul for Color3 {
type Output = Color3;
fn mul(self, rhs: Self) -> Self::Output {
Color3 {
r: self.r * rhs.r,
g: self.g * rhs.g,
b: self.b * rhs.b,
}
}
}
impl ops::Mul<f32> for Color3 {
type Output = Color3;
fn mul(self, rhs: f32) -> Self::Output {
Color3 {
r: self.r * rhs,
g: self.g * rhs,
b: self.b * rhs,
}
}
}
impl ops::Div for Color3 {
type Output = Color3;
fn div(self, rhs: Self) -> Self::Output {
Color3 {
r: self.r / rhs.r,
g: self.g / rhs.g,
b: self.b / rhs.b,
}
}
}
impl ops::Div<f32> for Color3 {
type Output = Color3;
fn div(self, rhs: f32) -> Self::Output {
Color3 {
r: self.r / rhs,
g: self.g / rhs,
b: self.b / rhs,
}
}
}
impl From<DomColor3> for Color3 {
fn from(v: DomColor3) -> Self {
Self {
r: v.r,
g: v.g,
b: v.b,
}
}
}
impl From<Color3> for DomColor3 {
fn from(v: Color3) -> Self {
Self {
r: v.r,
g: v.g,
b: v.b,
}
}
}
impl From<DomColor3uint8> for Color3 {
fn from(v: DomColor3uint8) -> Self {
Self {
r: (v.r as f32) / 255f32,
g: (v.g as f32) / 255f32,
b: (v.b as f32) / 255f32,
}
}
}
impl From<Color3> for DomColor3uint8 {
fn from(v: Color3) -> Self {
Self {
r: v.r.clamp(u8::MIN as f32, u8::MAX as f32) as u8,
g: v.g.clamp(u8::MIN as f32, u8::MAX as f32) as u8,
b: v.b.clamp(u8::MIN as f32, u8::MAX as f32) as u8,
}
}
}

View file

@ -1,117 +0,0 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::{
ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint,
};
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 ColorSequence {
pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> {
type ArgsColor<'lua> = LuaUserDataRef<'lua, Color3>;
type ArgsColors<'lua> = (LuaUserDataRef<'lua, Color3>, LuaUserDataRef<'lua, Color3>);
type ArgsKeypoints<'lua> = Vec<LuaUserDataRef<'lua, ColorSequenceKeypoint>>;
datatype_table.set(
"new",
lua.create_function(|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(),
))
}
})?,
)
}
}
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()
.cloned()
.map(ColorSequenceKeypoint::from)
.collect(),
}
}
}
impl From<ColorSequence> for DomColorSequence {
fn from(v: ColorSequence) -> Self {
Self {
keypoints: v
.keypoints
.iter()
.cloned()
.map(DomColorSequenceKeypoint::from)
.collect(),
}
}
}

View file

@ -1,68 +0,0 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::ColorSequenceKeypoint as DomColorSequenceKeypoint;
use super::{super::*, Color3};
/**
An implementation of the [ColorSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequenceKeypoint) Roblox datatype.
This implements all documented properties, methods & constructors of the ColorSequenceKeypoint class as of March 2023.
*/
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ColorSequenceKeypoint {
pub(crate) time: f32,
pub(crate) color: Color3,
}
impl ColorSequenceKeypoint {
pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> {
datatype_table.set(
"new",
lua.create_function(|_, (time, color): (f32, LuaUserDataRef<Color3>)| {
Ok(ColorSequenceKeypoint {
time,
color: *color,
})
})?,
)?;
Ok(())
}
}
impl LuaUserData for ColorSequenceKeypoint {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("Time", |_, this| Ok(this.time));
fields.add_field_method_get("Value", |_, this| Ok(this.color));
}
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 ColorSequenceKeypoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} > {}", self.time, self.color)
}
}
impl From<DomColorSequenceKeypoint> for ColorSequenceKeypoint {
fn from(v: DomColorSequenceKeypoint) -> Self {
Self {
time: v.time,
color: v.color.into(),
}
}
}
impl From<ColorSequenceKeypoint> for DomColorSequenceKeypoint {
fn from(v: ColorSequenceKeypoint) -> Self {
Self {
time: v.time,
color: v.color.into(),
}
}
}

View file

@ -1,71 +0,0 @@
use core::fmt;
use mlua::prelude::*;
use rbx_reflection::EnumDescriptor;
use super::{super::*, EnumItem};
/**
An implementation of the [Enum](https://create.roblox.com/docs/reference/engine/datatypes/Enum) Roblox datatype.
This implements all documented properties, methods & constructors of the Enum class as of March 2023.
*/
#[derive(Debug, Clone)]
pub struct Enum {
pub(crate) desc: &'static EnumDescriptor<'static>,
}
impl Enum {
pub(crate) fn from_name(name: impl AsRef<str>) -> Option<Self> {
let db = rbx_reflection_database::get();
db.enums.get(name.as_ref()).map(Enum::from)
}
}
impl LuaUserData for Enum {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
// Methods
methods.add_method("GetEnumItems", |_, this, ()| {
Ok(this
.desc
.items
.iter()
.map(|(name, value)| EnumItem {
parent: this.clone(),
name: name.to_string(),
value: *value,
})
.collect::<Vec<_>>())
});
methods.add_meta_method(LuaMetaMethod::Index, |_, this, name: String| {
match EnumItem::from_enum_and_name(this, &name) {
Some(item) => Ok(item),
None => Err(LuaError::RuntimeError(format!(
"The enum item '{}' does not exist for enum '{}'",
name, this.desc.name
))),
}
});
// Metamethods
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl fmt::Display for Enum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Enum.{}", self.desc.name)
}
}
impl PartialEq for Enum {
fn eq(&self, other: &Self) -> bool {
self.desc.name == other.desc.name
}
}
impl From<&'static EnumDescriptor<'static>> for Enum {
fn from(value: &'static EnumDescriptor<'static>) -> Self {
Self { desc: value }
}
}

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