mirror of
https://github.com/CompeyDev/lune-packaging.git
synced 2025-01-08 11:49:10 +00:00
feat: revamped AUR PKGBUILDs
This commit is contained in:
parent
62ed729e15
commit
f9ee37c83a
276 changed files with 151 additions and 24707 deletions
|
@ -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"
|
|
@ -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
3
.gitmodules
vendored
|
@ -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
11
.luaurc
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"languageMode": "strict",
|
||||
"lint": {
|
||||
"*": true
|
||||
},
|
||||
"lintErrors": false,
|
||||
"typeErrors": true,
|
||||
"globals": [
|
||||
"warn"
|
||||
]
|
||||
}
|
|
@ -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))
|
|
@ -1,4 +0,0 @@
|
|||
Header1,Header2,Header3
|
||||
Hello,World,!
|
||||
1,2,3
|
||||
Foo,Bar,Baz
|
|
|
@ -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! 🌙")
|
|
@ -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)
|
|
@ -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! 🌙")
|
|
@ -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)
|
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"esbenp.prettier-vscode",
|
||||
"JohnnyMorganz.stylua",
|
||||
"DavidAnson.vscode-markdownlint"
|
||||
]
|
||||
}
|
31
.vscode/settings.json
vendored
31
.vscode/settings.json
vendored
|
@ -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"
|
||||
}
|
||||
}
|
840
CHANGELOG.md
840
CHANGELOG.md
|
@ -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
2717
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
130
Cargo.toml
130
Cargo.toml
|
@ -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" }
|
373
LICENSE.txt
373
LICENSE.txt
|
@ -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.
|
45
README.md
45
README.md
|
@ -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!
|
|
@ -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"
|
16
package/aur/lune-bin/.SRCINFO
Normal file
16
package/aur/lune-bin/.SRCINFO
Normal 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
|
20
package/aur/lune-bin/PKGBUILD
Normal file
20
package/aur/lune-bin/PKGBUILD
Normal 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
|
||||
}
|
17
package/aur/lune-git/.SRCINFO
Normal file
17
package/aur/lune-git/.SRCINFO
Normal 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
|
45
package/aur/lune-git/PKGBUILD
Normal file
45
package/aur/lune-git/PKGBUILD
Normal 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
16
package/aur/lune/.SRCINFO
Normal 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
37
package/aur/lune/PKGBUILD
Normal 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
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -1,6 +0,0 @@
|
|||
std = "luau+lune"
|
||||
|
||||
exclude = ["luneTypes.d.luau"]
|
||||
|
||||
[lints]
|
||||
high_cyclomatic_complexity = "warn"
|
|
@ -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())
|
||||
}
|
187
src/cli/mod.rs
187
src/cli/mod.rs
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
128
src/cli/setup.rs
128
src/cli/setup.rs
|
@ -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}}",
|
||||
);
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod files;
|
||||
pub mod listing;
|
|
@ -1,7 +0,0 @@
|
|||
mod lune;
|
||||
mod roblox;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use crate::lune::{Lune, LuneError};
|
|
@ -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
|
||||
}
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
|
@ -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 {}
|
|
@ -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(())
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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(¤t_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(¤t_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(())
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mod copy;
|
||||
mod metadata;
|
||||
mod options;
|
||||
|
||||
pub use copy::copy;
|
||||
pub use metadata::FsMetadata;
|
||||
pub use options::FsWriteOptions;
|
|
@ -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()
|
||||
)),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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 {}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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?))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mod compress_decompress;
|
||||
mod encode_decode;
|
||||
|
||||
pub use compress_decompress::{compress, decompress, CompressDecompressFormat};
|
||||
pub use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat};
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod formatting;
|
||||
pub mod prompt;
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
mod builder;
|
||||
|
||||
pub use builder::TableBuilder;
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<()>),
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
109
src/lune/mod.rs
109
src/lune/mod.rs
|
@ -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)
|
||||
}
|
||||
}
|
38
src/main.rs
38
src/main.rs
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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("???")
|
||||
)))
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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>;
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
];
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue