mirror of
https://github.com/lune-org/lune.git
synced 2025-05-04 10:43:57 +01:00
Remove old builtins from lune crate and use lune-std instead
This commit is contained in:
parent
2261733516
commit
c8dcea0a21
115 changed files with 77 additions and 14005 deletions
93
Cargo.lock
generated
93
Cargo.lock
generated
|
@ -205,17 +205,6 @@ version = "4.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
|
checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-trait"
|
|
||||||
version = "0.1.80"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.60",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
@ -759,12 +748,6 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -1367,15 +1350,6 @@ version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
@ -1484,56 +1458,27 @@ name = "lune"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
|
||||||
"async-trait",
|
|
||||||
"blocking",
|
|
||||||
"bstr",
|
|
||||||
"chrono",
|
|
||||||
"chrono_lc",
|
|
||||||
"clap",
|
"clap",
|
||||||
"console",
|
"console",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"directories",
|
"directories",
|
||||||
"dunce",
|
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"glam",
|
|
||||||
"http 1.1.0",
|
|
||||||
"http-body-util",
|
|
||||||
"hyper 1.3.1",
|
|
||||||
"hyper-tungstenite",
|
|
||||||
"hyper-util",
|
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"itertools",
|
"lune-roblox",
|
||||||
"lz4_flex",
|
"lune-std",
|
||||||
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
"mlua-luau-scheduler 0.0.2",
|
"mlua-luau-scheduler",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"os_str_bytes",
|
|
||||||
"path-clean",
|
|
||||||
"pathdiff",
|
|
||||||
"pin-project",
|
|
||||||
"rand",
|
|
||||||
"rbx_binary",
|
|
||||||
"rbx_cookie",
|
|
||||||
"rbx_dom_weak",
|
|
||||||
"rbx_reflection",
|
|
||||||
"rbx_reflection_database",
|
|
||||||
"rbx_xml",
|
|
||||||
"regex",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
"self_cell",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
|
||||||
"toml",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"urlencoding",
|
|
||||||
"zip_next",
|
"zip_next",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1570,7 +1515,7 @@ dependencies = [
|
||||||
"lune-std-task",
|
"lune-std-task",
|
||||||
"lune-utils",
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
"mlua-luau-scheduler 0.0.1",
|
"mlua-luau-scheduler",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -1620,7 +1565,7 @@ dependencies = [
|
||||||
"lune-std-serde",
|
"lune-std-serde",
|
||||||
"lune-utils",
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
"mlua-luau-scheduler 0.0.1",
|
"mlua-luau-scheduler",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
|
@ -1634,7 +1579,7 @@ dependencies = [
|
||||||
"directories",
|
"directories",
|
||||||
"lune-utils",
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
"mlua-luau-scheduler 0.0.1",
|
"mlua-luau-scheduler",
|
||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -1657,7 +1602,7 @@ dependencies = [
|
||||||
"lune-roblox",
|
"lune-roblox",
|
||||||
"lune-utils",
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
"mlua-luau-scheduler 0.0.1",
|
"mlua-luau-scheduler",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rbx_cookie",
|
"rbx_cookie",
|
||||||
]
|
]
|
||||||
|
@ -1685,7 +1630,7 @@ dependencies = [
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"lune-utils",
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
"mlua-luau-scheduler 0.0.1",
|
"mlua-luau-scheduler",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1695,7 +1640,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lune-utils",
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
"mlua-luau-scheduler 0.0.1",
|
"mlua-luau-scheduler",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1827,23 +1772,6 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mlua-luau-scheduler"
|
|
||||||
version = "0.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a13eabdbc57fa38cf0b604d98ce3431573c79a964aac56e09c16c240d36cb1bf"
|
|
||||||
dependencies = [
|
|
||||||
"async-executor",
|
|
||||||
"blocking",
|
|
||||||
"concurrent-queue",
|
|
||||||
"derive_more",
|
|
||||||
"event-listener 4.0.3",
|
|
||||||
"futures-lite",
|
|
||||||
"mlua",
|
|
||||||
"rustc-hash",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mlua-sys"
|
name = "mlua-sys"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -3074,7 +3002,6 @@ dependencies = [
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"tracing",
|
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,11 @@ reqwest = { version = "0.11", default-features = false, features = [
|
||||||
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
|
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
|
|
||||||
tokio = { version = "1", default-features = false, features = ["sync", "net"] }
|
tokio = { version = "1", default-features = false, features = [
|
||||||
|
"sync",
|
||||||
|
"net",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
|
|
||||||
lune-utils = { version = "0.1.0", path = "../lune-utils" }
|
lune-utils = { version = "0.1.0", path = "../lune-utils" }
|
||||||
lune-std-serde = { version = "0.1.0", path = "../lune-std-serde" }
|
lune-std-serde = { version = "0.1.0", path = "../lune-std-serde" }
|
||||||
|
|
|
@ -18,7 +18,31 @@ name = "lune"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cli", "roblox"]
|
default = [
|
||||||
|
"datetime",
|
||||||
|
"fs",
|
||||||
|
"luau",
|
||||||
|
"net",
|
||||||
|
"process",
|
||||||
|
"regex",
|
||||||
|
"roblox",
|
||||||
|
"serde",
|
||||||
|
"stdio",
|
||||||
|
"task",
|
||||||
|
"cli",
|
||||||
|
]
|
||||||
|
|
||||||
|
datetime = ["lune-std/datetime"]
|
||||||
|
fs = ["lune-std/fs"]
|
||||||
|
luau = ["lune-std/luau"]
|
||||||
|
net = ["lune-std/net"]
|
||||||
|
process = ["lune-std/process"]
|
||||||
|
regex = ["lune-std/regex"]
|
||||||
|
roblox = ["lune-std/roblox", "dep:lune-roblox"]
|
||||||
|
serde = ["lune-std/serde"]
|
||||||
|
stdio = ["lune-std/stdio"]
|
||||||
|
task = ["lune-std/task"]
|
||||||
|
|
||||||
cli = [
|
cli = [
|
||||||
"dep:anyhow",
|
"dep:anyhow",
|
||||||
"dep:env_logger",
|
"dep:env_logger",
|
||||||
|
@ -27,113 +51,39 @@ cli = [
|
||||||
"dep:rustyline",
|
"dep:rustyline",
|
||||||
"dep:zip_next",
|
"dep:zip_next",
|
||||||
]
|
]
|
||||||
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",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = 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]
|
[dependencies]
|
||||||
|
mlua = { version = "0.9.7", features = ["luau"] }
|
||||||
|
mlua-luau-scheduler = "0.0.1"
|
||||||
|
|
||||||
console = "0.15"
|
console = "0.15"
|
||||||
|
dialoguer = "0.11"
|
||||||
directories = "5.0"
|
directories = "5.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
once_cell = "1.17"
|
once_cell = "1.17"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
async-trait = "0.1"
|
|
||||||
dialoguer = "0.11"
|
|
||||||
dunce = "1.0"
|
|
||||||
lz4_flex = "0.11"
|
|
||||||
path-clean = "1.0"
|
|
||||||
pathdiff = "0.2"
|
|
||||||
pin-project = "1.0"
|
|
||||||
urlencoding = "2.1"
|
|
||||||
bstr = "1.9"
|
|
||||||
regex = "1.10"
|
|
||||||
self_cell = "1.0"
|
|
||||||
|
|
||||||
### RUNTIME
|
|
||||||
|
|
||||||
blocking = "1.5"
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tokio = { version = "1.24", features = ["full", "tracing"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
|
||||||
|
|
||||||
mlua-luau-scheduler = { version = "0.0.2" }
|
|
||||||
mlua = { version = "0.9.7", features = [
|
|
||||||
"luau",
|
|
||||||
"luau-jit",
|
|
||||||
"async",
|
|
||||||
"serialize",
|
|
||||||
] }
|
|
||||||
|
|
||||||
### SERDE
|
|
||||||
|
|
||||||
async-compression = { version = "0.4", features = [
|
|
||||||
"tokio",
|
|
||||||
"brotli",
|
|
||||||
"deflate",
|
|
||||||
"gzip",
|
|
||||||
"zlib",
|
|
||||||
] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
|
||||||
serde_yaml = "0.9"
|
|
||||||
toml = { version = "0.8", features = ["preserve_order"] }
|
|
||||||
|
|
||||||
### NET
|
|
||||||
|
|
||||||
hyper = { version = "1.1", features = ["full"] }
|
|
||||||
hyper-util = { version = "0.1", features = ["full"] }
|
|
||||||
http = "1.0"
|
|
||||||
http-body-util = { version = "0.1" }
|
|
||||||
hyper-tungstenite = { version = "0.13" }
|
|
||||||
|
|
||||||
reqwest = { version = "0.11", default-features = false, features = [
|
reqwest = { version = "0.11", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
|
lune-std = { version = "0.1.0", path = "../lune-std" }
|
||||||
|
lune-utils = { version = "0.1.0", path = "../lune-utils" }
|
||||||
### DATETIME
|
lune-roblox = { optional = true, version = "0.1.0", path = "../lune-roblox" }
|
||||||
chrono = "=0.4.34" # NOTE: 0.4.35 does not compile with chrono_lc
|
|
||||||
chrono_lc = "0.1"
|
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
anyhow = { optional = true, version = "1.0" }
|
anyhow = { optional = true, version = "1.0" }
|
||||||
env_logger = { optional = true, version = "0.11" }
|
env_logger = { optional = true, version = "0.11" }
|
||||||
itertools = "0.12"
|
|
||||||
clap = { optional = true, version = "4.1", features = ["derive"] }
|
clap = { optional = true, version = "4.1", features = ["derive"] }
|
||||||
include_dir = { optional = true, version = "0.7", features = ["glob"] }
|
include_dir = { optional = true, version = "0.7", features = ["glob"] }
|
||||||
rustyline = { optional = true, version = "14.0" }
|
rustyline = { optional = true, version = "14.0" }
|
||||||
zip_next = { optional = true, version = "1.1" }
|
zip_next = { optional = true, version = "1.1" }
|
||||||
|
|
||||||
### ROBLOX
|
|
||||||
|
|
||||||
glam = { optional = true, version = "0.27" }
|
|
||||||
rand = { optional = true, version = "0.8" }
|
|
||||||
|
|
||||||
rbx_cookie = { optional = true, version = "0.1.4", default-features = false }
|
|
||||||
|
|
||||||
rbx_binary = { optional = true, version = "0.7.3" }
|
|
||||||
rbx_dom_weak = { optional = true, version = "2.6.0" }
|
|
||||||
rbx_reflection = { optional = true, version = "4.4.0" }
|
|
||||||
rbx_reflection_database = { optional = true, version = "0.2.9" }
|
|
||||||
rbx_xml = { optional = true, version = "0.13.2" }
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ use std::{
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use console::style;
|
use console::style;
|
||||||
use directories::UserDirs;
|
use directories::UserDirs;
|
||||||
use itertools::Itertools;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
const LUNE_COMMENT_PREFIX: &str = "-->";
|
const LUNE_COMMENT_PREFIX: &str = "-->";
|
||||||
|
@ -180,10 +179,9 @@ pub fn parse_lune_description_from_file(contents: &str) -> Option<String> {
|
||||||
});
|
});
|
||||||
let unindented_lines = comment_lines
|
let unindented_lines = comment_lines
|
||||||
.iter()
|
.iter()
|
||||||
.map(|line| &line[shortest_indent..])
|
.map(|line| line[shortest_indent..].to_string())
|
||||||
// Replace newlines with a single space inbetween instead
|
.collect::<Vec<_>>()
|
||||||
.interleave(std::iter::repeat(" ").take(comment_lines.len() - 1))
|
.join(" ");
|
||||||
.collect();
|
|
||||||
Some(unindented_lines)
|
Some(unindented_lines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
mod lune;
|
#![allow(clippy::cargo_common_metadata)]
|
||||||
|
|
||||||
|
mod rt;
|
||||||
|
|
||||||
#[cfg(feature = "roblox")]
|
#[cfg(feature = "roblox")]
|
||||||
pub mod roblox;
|
pub use lune_roblox as roblox;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use crate::lune::{Runtime, RuntimeError};
|
pub use crate::rt::{Runtime, RuntimeError};
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub type DateTimeResult<T, E = DateTimeError> = Result<T, E>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)]
|
|
||||||
pub enum DateTimeError {
|
|
||||||
#[error("invalid date")]
|
|
||||||
InvalidDate,
|
|
||||||
#[error("invalid time")]
|
|
||||||
InvalidTime,
|
|
||||||
#[error("ambiguous date or time")]
|
|
||||||
Ambiguous,
|
|
||||||
#[error("date or time is outside allowed range")]
|
|
||||||
OutOfRangeUnspecified,
|
|
||||||
#[error("{name} must be within range {min} -> {max}, got {value}")]
|
|
||||||
OutOfRange {
|
|
||||||
name: &'static str,
|
|
||||||
value: String,
|
|
||||||
min: String,
|
|
||||||
max: String,
|
|
||||||
},
|
|
||||||
#[error(transparent)]
|
|
||||||
ParseError(#[from] chrono::ParseError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DateTimeError> for LuaError {
|
|
||||||
fn from(value: DateTimeError) -> Self {
|
|
||||||
LuaError::runtime(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,251 +0,0 @@
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use chrono::prelude::*;
|
|
||||||
use chrono::DateTime as ChronoDateTime;
|
|
||||||
use chrono_lc::LocaleDate;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
mod error;
|
|
||||||
mod values;
|
|
||||||
|
|
||||||
use error::*;
|
|
||||||
use values::*;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("fromIsoDate", |_, iso_date: String| {
|
|
||||||
Ok(DateTime::from_iso_date(iso_date)?)
|
|
||||||
})?
|
|
||||||
.with_function("fromLocalTime", |_, values| {
|
|
||||||
Ok(DateTime::from_local_time(&values)?)
|
|
||||||
})?
|
|
||||||
.with_function("fromUniversalTime", |_, values| {
|
|
||||||
Ok(DateTime::from_universal_time(&values)?)
|
|
||||||
})?
|
|
||||||
.with_function("fromUnixTimestamp", |_, timestamp| {
|
|
||||||
Ok(DateTime::from_unix_timestamp_float(timestamp)?)
|
|
||||||
})?
|
|
||||||
.with_function("now", |_, ()| Ok(DateTime::now()))?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
|
|
||||||
const DEFAULT_LOCALE: &str = "en";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct DateTime {
|
|
||||||
// NOTE: We store this as the UTC time zone since it is the most commonly
|
|
||||||
// used and getting the generics right for TimeZone is somewhat tricky,
|
|
||||||
// but none of the method implementations below should rely on this tz
|
|
||||||
inner: ChronoDateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DateTime {
|
|
||||||
/**
|
|
||||||
Creates a new `DateTime` struct representing the current moment in time.
|
|
||||||
|
|
||||||
See [`chrono::DateTime::now`] for additional details.
|
|
||||||
*/
|
|
||||||
pub fn now() -> Self {
|
|
||||||
Self { inner: Utc::now() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates a new `DateTime` struct from the given `unix_timestamp`,
|
|
||||||
which is a float of seconds passed since the UNIX epoch.
|
|
||||||
|
|
||||||
This is somewhat unconventional, but fits our Luau interface and dynamic types quite well.
|
|
||||||
To use this method the same way you would use a more traditional `from_unix_timestamp`
|
|
||||||
that takes a `u64` of seconds or similar type, casting the value is sufficient:
|
|
||||||
|
|
||||||
```rust ignore
|
|
||||||
DateTime::from_unix_timestamp_float(123456789u64 as f64)
|
|
||||||
```
|
|
||||||
|
|
||||||
See [`chrono::DateTime::from_timestamp`] for additional details.
|
|
||||||
*/
|
|
||||||
pub fn from_unix_timestamp_float(unix_timestamp: f64) -> DateTimeResult<Self> {
|
|
||||||
let whole = unix_timestamp.trunc() as i64;
|
|
||||||
let fract = unix_timestamp.fract();
|
|
||||||
let nanos = (fract * 1_000_000_000f64)
|
|
||||||
.round()
|
|
||||||
.clamp(u32::MIN as f64, u32::MAX as f64) as u32;
|
|
||||||
let inner = ChronoDateTime::<Utc>::from_timestamp(whole, nanos)
|
|
||||||
.ok_or(DateTimeError::OutOfRangeUnspecified)?;
|
|
||||||
Ok(Self { inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Transforms individual date & time values into a new
|
|
||||||
`DateTime` struct, using the universal (UTC) time zone.
|
|
||||||
|
|
||||||
See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
|
|
||||||
for additional details and cases where this constructor may return an error.
|
|
||||||
*/
|
|
||||||
pub fn from_universal_time(values: &DateTimeValues) -> DateTimeResult<Self> {
|
|
||||||
let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
|
|
||||||
.ok_or(DateTimeError::InvalidDate)?;
|
|
||||||
|
|
||||||
let time = NaiveTime::from_hms_milli_opt(
|
|
||||||
values.hour,
|
|
||||||
values.minute,
|
|
||||||
values.second,
|
|
||||||
values.millisecond,
|
|
||||||
)
|
|
||||||
.ok_or(DateTimeError::InvalidTime)?;
|
|
||||||
|
|
||||||
let inner = Utc.from_utc_datetime(&NaiveDateTime::new(date, time));
|
|
||||||
|
|
||||||
Ok(Self { inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Transforms individual date & time values into a new
|
|
||||||
`DateTime` struct, using the current local time zone.
|
|
||||||
|
|
||||||
See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
|
|
||||||
for additional details and cases where this constructor may return an error.
|
|
||||||
*/
|
|
||||||
pub fn from_local_time(values: &DateTimeValues) -> DateTimeResult<Self> {
|
|
||||||
let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
|
|
||||||
.ok_or(DateTimeError::InvalidDate)?;
|
|
||||||
|
|
||||||
let time = NaiveTime::from_hms_milli_opt(
|
|
||||||
values.hour,
|
|
||||||
values.minute,
|
|
||||||
values.second,
|
|
||||||
values.millisecond,
|
|
||||||
)
|
|
||||||
.ok_or(DateTimeError::InvalidTime)?;
|
|
||||||
|
|
||||||
let inner = Local
|
|
||||||
.from_local_datetime(&NaiveDateTime::new(date, time))
|
|
||||||
.single()
|
|
||||||
.ok_or(DateTimeError::Ambiguous)?
|
|
||||||
.with_timezone(&Utc);
|
|
||||||
|
|
||||||
Ok(Self { inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Formats the `DateTime` using the universal (UTC) time
|
|
||||||
zone, the given format string, and the given locale.
|
|
||||||
|
|
||||||
`format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
|
|
||||||
|
|
||||||
See [`chrono_lc::DateTime::formatl`] for additional details.
|
|
||||||
*/
|
|
||||||
pub fn format_string_local(&self, format: Option<&str>, locale: Option<&str>) -> String {
|
|
||||||
self.inner
|
|
||||||
.with_timezone(&Local)
|
|
||||||
.formatl(
|
|
||||||
format.unwrap_or(DEFAULT_FORMAT),
|
|
||||||
locale.unwrap_or(DEFAULT_LOCALE),
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Formats the `DateTime` using the universal (UTC) time
|
|
||||||
zone, the given format string, and the given locale.
|
|
||||||
|
|
||||||
`format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
|
|
||||||
|
|
||||||
See [`chrono_lc::DateTime::formatl`] for additional details.
|
|
||||||
*/
|
|
||||||
pub fn format_string_universal(&self, format: Option<&str>, locale: Option<&str>) -> String {
|
|
||||||
self.inner
|
|
||||||
.with_timezone(&Utc)
|
|
||||||
.formatl(
|
|
||||||
format.unwrap_or(DEFAULT_FORMAT),
|
|
||||||
locale.unwrap_or(DEFAULT_LOCALE),
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Parses a time string in the ISO 8601 format, such as
|
|
||||||
`1996-12-19T16:39:57-08:00`, into a new `DateTime` struct.
|
|
||||||
|
|
||||||
See [`chrono::DateTime::parse_from_rfc3339`] for additional details.
|
|
||||||
*/
|
|
||||||
pub fn from_iso_date(iso_date: impl AsRef<str>) -> DateTimeResult<Self> {
|
|
||||||
let inner = ChronoDateTime::parse_from_rfc3339(iso_date.as_ref())?.with_timezone(&Utc);
|
|
||||||
Ok(Self { inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Extracts individual date & time values from this
|
|
||||||
`DateTime`, using the current local time zone.
|
|
||||||
*/
|
|
||||||
pub fn to_local_time(self) -> DateTimeValues {
|
|
||||||
DateTimeValues::from(self.inner.with_timezone(&Local))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Extracts individual date & time values from this
|
|
||||||
`DateTime`, using the universal (UTC) time zone.
|
|
||||||
*/
|
|
||||||
pub fn to_universal_time(self) -> DateTimeValues {
|
|
||||||
DateTimeValues::from(self.inner.with_timezone(&Utc))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Formats a time string in the ISO 8601 format, such as `1996-12-19T16:39:57-08:00`.
|
|
||||||
|
|
||||||
See [`chrono::DateTime::to_rfc3339`] for additional details.
|
|
||||||
*/
|
|
||||||
pub fn to_iso_date(self) -> String {
|
|
||||||
self.inner.to_rfc3339()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for DateTime {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("unixTimestamp", |_, this| Ok(this.inner.timestamp()));
|
|
||||||
fields.add_field_method_get("unixTimestampMillis", |_, this| {
|
|
||||||
Ok(this.inner.timestamp_millis())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
// Metamethods to compare DateTime as instants in time
|
|
||||||
methods.add_meta_method(
|
|
||||||
LuaMetaMethod::Eq,
|
|
||||||
|_, this: &Self, other: LuaUserDataRef<Self>| Ok(this.eq(&other)),
|
|
||||||
);
|
|
||||||
methods.add_meta_method(
|
|
||||||
LuaMetaMethod::Lt,
|
|
||||||
|_, this: &Self, other: LuaUserDataRef<Self>| {
|
|
||||||
Ok(matches!(this.cmp(&other), Ordering::Less))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_meta_method(
|
|
||||||
LuaMetaMethod::Le,
|
|
||||||
|_, this: &Self, other: LuaUserDataRef<Self>| {
|
|
||||||
Ok(matches!(this.cmp(&other), Ordering::Less | Ordering::Equal))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// Normal methods
|
|
||||||
methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_iso_date()));
|
|
||||||
methods.add_method(
|
|
||||||
"formatUniversalTime",
|
|
||||||
|_, this, (format, locale): (Option<String>, Option<String>)| {
|
|
||||||
Ok(this.format_string_universal(format.as_deref(), locale.as_deref()))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method(
|
|
||||||
"formatLocalTime",
|
|
||||||
|_, this, (format, locale): (Option<String>, Option<String>)| {
|
|
||||||
Ok(this.format_string_local(format.as_deref(), locale.as_deref()))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method("toUniversalTime", |_, this: &Self, ()| {
|
|
||||||
Ok(this.to_universal_time())
|
|
||||||
});
|
|
||||||
methods.add_method("toLocalTime", |_, this: &Self, ()| Ok(this.to_local_time()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use chrono::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
use super::error::{DateTimeError, DateTimeResult};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct DateTimeValues {
|
|
||||||
pub year: i32,
|
|
||||||
pub month: u32,
|
|
||||||
pub day: u32,
|
|
||||||
pub hour: u32,
|
|
||||||
pub minute: u32,
|
|
||||||
pub second: u32,
|
|
||||||
pub millisecond: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DateTimeValues {
|
|
||||||
/**
|
|
||||||
Verifies that all of the date & time values are within allowed ranges:
|
|
||||||
|
|
||||||
| Name | Range |
|
|
||||||
|---------------|----------------|
|
|
||||||
| `year` | `1400 -> 9999` |
|
|
||||||
| `month` | `1 -> 12` |
|
|
||||||
| `day` | `1 -> 31` |
|
|
||||||
| `hour` | `0 -> 23` |
|
|
||||||
| `minute` | `0 -> 59` |
|
|
||||||
| `second` | `0 -> 60` |
|
|
||||||
| `millisecond` | `0 -> 999` |
|
|
||||||
*/
|
|
||||||
pub fn verify(self) -> DateTimeResult<Self> {
|
|
||||||
verify_in_range("year", self.year, 1400, 9999)?;
|
|
||||||
verify_in_range("month", self.month, 1, 12)?;
|
|
||||||
verify_in_range("day", self.day, 1, 31)?;
|
|
||||||
verify_in_range("hour", self.hour, 0, 23)?;
|
|
||||||
verify_in_range("minute", self.minute, 0, 59)?;
|
|
||||||
verify_in_range("second", self.second, 0, 60)?;
|
|
||||||
verify_in_range("millisecond", self.millisecond, 0, 999)?;
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_in_range<T>(name: &'static str, value: T, min: T, max: T) -> DateTimeResult<T>
|
|
||||||
where
|
|
||||||
T: PartialOrd + std::fmt::Display,
|
|
||||||
{
|
|
||||||
assert!(max > min);
|
|
||||||
if value < min || value > max {
|
|
||||||
Err(DateTimeError::OutOfRange {
|
|
||||||
name,
|
|
||||||
min: min.to_string(),
|
|
||||||
max: max.to_string(),
|
|
||||||
value: value.to_string(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Conversion methods between DateTimeValues and plain lua tables
|
|
||||||
|
|
||||||
Note that the IntoLua implementation here uses a read-only table,
|
|
||||||
since we generally want to convert into lua when we know we have
|
|
||||||
a fixed point in time, and we guarantee that it doesn't change
|
|
||||||
*/
|
|
||||||
|
|
||||||
impl FromLua<'_> for DateTimeValues {
|
|
||||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
|
||||||
if !value.is_table() {
|
|
||||||
return Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "DateTimeValues",
|
|
||||||
message: Some("value must be a table".to_string()),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let value = value.as_table().unwrap();
|
|
||||||
let values = Self {
|
|
||||||
year: value.get("year")?,
|
|
||||||
month: value.get("month")?,
|
|
||||||
day: value.get("day")?,
|
|
||||||
hour: value.get("hour")?,
|
|
||||||
minute: value.get("minute")?,
|
|
||||||
second: value.get("second")?,
|
|
||||||
millisecond: value.get("millisecond").unwrap_or(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
match values.verify() {
|
|
||||||
Ok(dt) => Ok(dt),
|
|
||||||
Err(e) => Err(LuaError::FromLuaConversionError {
|
|
||||||
from: "table",
|
|
||||||
to: "DateTimeValues",
|
|
||||||
message: Some(e.to_string()),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoLua<'_> for DateTimeValues {
|
|
||||||
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
|
|
||||||
let tab = TableBuilder::new(lua)?
|
|
||||||
.with_value("year", self.year)?
|
|
||||||
.with_values(vec![
|
|
||||||
("month", self.month),
|
|
||||||
("day", self.day),
|
|
||||||
("hour", self.hour),
|
|
||||||
("minute", self.minute),
|
|
||||||
("second", self.second),
|
|
||||||
("millisecond", self.millisecond),
|
|
||||||
])?
|
|
||||||
.build_readonly()?;
|
|
||||||
Ok(LuaValue::Table(tab))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Conversion methods between chrono's timezone-aware DateTime to
|
|
||||||
and from our non-timezone-aware DateTimeValues values struct
|
|
||||||
*/
|
|
||||||
|
|
||||||
impl<T: TimeZone> From<DateTime<T>> for DateTimeValues {
|
|
||||||
fn from(value: DateTime<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
year: value.year(),
|
|
||||||
month: value.month(),
|
|
||||||
day: value.day(),
|
|
||||||
hour: value.hour(),
|
|
||||||
minute: value.minute(),
|
|
||||||
second: value.second(),
|
|
||||||
millisecond: value.timestamp_subsec_millis(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<DateTimeValues> for DateTime<Utc> {
|
|
||||||
type Error = DateTimeError;
|
|
||||||
fn try_from(value: DateTimeValues) -> Result<Self, Self::Error> {
|
|
||||||
Utc.with_ymd_and_hms(
|
|
||||||
value.year,
|
|
||||||
value.month,
|
|
||||||
value.day,
|
|
||||||
value.hour,
|
|
||||||
value.minute,
|
|
||||||
value.second,
|
|
||||||
)
|
|
||||||
.single()
|
|
||||||
.ok_or(DateTimeError::Ambiguous)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<DateTimeValues> for DateTime<Local> {
|
|
||||||
type Error = DateTimeError;
|
|
||||||
fn try_from(value: DateTimeValues) -> Result<Self, Self::Error> {
|
|
||||||
Local
|
|
||||||
.with_ymd_and_hms(
|
|
||||||
value.year,
|
|
||||||
value.month,
|
|
||||||
value.day,
|
|
||||||
value.hour,
|
|
||||||
value.minute,
|
|
||||||
value.second,
|
|
||||||
)
|
|
||||||
.single()
|
|
||||||
.ok_or(DateTimeError::Ambiguous)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,166 +0,0 @@
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
use super::options::FsWriteOptions;
|
|
||||||
|
|
||||||
pub struct CopyContents {
|
|
||||||
// Vec<(relative depth, path)>
|
|
||||||
pub dirs: Vec<(usize, PathBuf)>,
|
|
||||||
pub files: Vec<(usize, PathBuf)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_contents_at(root: PathBuf, _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 {
|
|
||||||
let (is_dir, is_file) = match fs::metadata(&target).await {
|
|
||||||
Ok(meta) => (meta.is_dir(), meta.is_file()),
|
|
||||||
Err(e) if e.kind() == ErrorKind::NotFound => (false, false),
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
if is_dir {
|
|
||||||
fs::remove_dir_all(target).await?;
|
|
||||||
} else if is_file {
|
|
||||||
fs::remove_file(target).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::create_dir_all(target).await?;
|
|
||||||
|
|
||||||
// FUTURE: Write dirs / files concurrently
|
|
||||||
// to potentially speed these operations up
|
|
||||||
for (_, dir) in &contents.dirs {
|
|
||||||
fs::create_dir_all(target.join(dir)).await?;
|
|
||||||
}
|
|
||||||
for (_, file) in &contents.files {
|
|
||||||
fs::copy(source.join(file), target.join(file)).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
use std::{
|
|
||||||
fmt,
|
|
||||||
fs::{FileType as StdFileType, Metadata as StdMetadata, Permissions as StdPermissions},
|
|
||||||
io::Result as IoResult,
|
|
||||||
str::FromStr,
|
|
||||||
time::SystemTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::builtins::datetime::DateTime;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum FsMetadataKind {
|
|
||||||
None,
|
|
||||||
File,
|
|
||||||
Dir,
|
|
||||||
Symlink,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for FsMetadataKind {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
match self {
|
|
||||||
Self::None => "none",
|
|
||||||
Self::File => "file",
|
|
||||||
Self::Dir => "dir",
|
|
||||||
Self::Symlink => "symlink",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for FsMetadataKind {
|
|
||||||
type Err = &'static str;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.trim().to_ascii_lowercase().as_ref() {
|
|
||||||
"none" => Ok(Self::None),
|
|
||||||
"file" => Ok(Self::File),
|
|
||||||
"dir" => Ok(Self::Dir),
|
|
||||||
"symlink" => Ok(Self::Symlink),
|
|
||||||
_ => Err("Invalid metadata kind"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<StdFileType> for FsMetadataKind {
|
|
||||||
fn from(value: StdFileType) -> Self {
|
|
||||||
if value.is_file() {
|
|
||||||
Self::File
|
|
||||||
} else if value.is_dir() {
|
|
||||||
Self::Dir
|
|
||||||
} else if value.is_symlink() {
|
|
||||||
Self::Symlink
|
|
||||||
} else {
|
|
||||||
panic!("Encountered unknown filesystem filetype")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> IntoLua<'lua> for FsMetadataKind {
|
|
||||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
if self == Self::None {
|
|
||||||
Ok(LuaValue::Nil)
|
|
||||||
} else {
|
|
||||||
self.to_string().into_lua(lua)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct FsPermissions {
|
|
||||||
pub(crate) read_only: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<StdPermissions> for FsPermissions {
|
|
||||||
fn from(value: StdPermissions) -> Self {
|
|
||||||
Self {
|
|
||||||
read_only: value.readonly(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> IntoLua<'lua> for FsPermissions {
|
|
||||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
let tab = lua.create_table_with_capacity(0, 1)?;
|
|
||||||
tab.set("readOnly", self.read_only)?;
|
|
||||||
tab.set_readonly(true);
|
|
||||||
Ok(LuaValue::Table(tab))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct FsMetadata {
|
|
||||||
pub(crate) kind: FsMetadataKind,
|
|
||||||
pub(crate) exists: bool,
|
|
||||||
pub(crate) created_at: Option<DateTime>,
|
|
||||||
pub(crate) modified_at: Option<DateTime>,
|
|
||||||
pub(crate) accessed_at: Option<DateTime>,
|
|
||||||
pub(crate) permissions: Option<FsPermissions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FsMetadata {
|
|
||||||
pub fn not_found() -> Self {
|
|
||||||
Self {
|
|
||||||
kind: FsMetadataKind::None,
|
|
||||||
exists: false,
|
|
||||||
created_at: None,
|
|
||||||
modified_at: None,
|
|
||||||
accessed_at: None,
|
|
||||||
permissions: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> IntoLua<'lua> for FsMetadata {
|
|
||||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
let tab = lua.create_table_with_capacity(0, 6)?;
|
|
||||||
tab.set("kind", self.kind)?;
|
|
||||||
tab.set("exists", self.exists)?;
|
|
||||||
tab.set("createdAt", self.created_at)?;
|
|
||||||
tab.set("modifiedAt", self.modified_at)?;
|
|
||||||
tab.set("accessedAt", self.accessed_at)?;
|
|
||||||
tab.set("permissions", self.permissions)?;
|
|
||||||
tab.set_readonly(true);
|
|
||||||
Ok(LuaValue::Table(tab))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<StdMetadata> for FsMetadata {
|
|
||||||
fn from(value: StdMetadata) -> Self {
|
|
||||||
Self {
|
|
||||||
kind: value.file_type().into(),
|
|
||||||
exists: true,
|
|
||||||
created_at: system_time_to_timestamp(value.created()),
|
|
||||||
modified_at: system_time_to_timestamp(value.modified()),
|
|
||||||
accessed_at: system_time_to_timestamp(value.accessed()),
|
|
||||||
permissions: Some(FsPermissions::from(value.permissions())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn system_time_to_timestamp(res: IoResult<SystemTime>) -> Option<DateTime> {
|
|
||||||
match res {
|
|
||||||
Ok(t) => match t.duration_since(SystemTime::UNIX_EPOCH) {
|
|
||||||
Ok(d) => DateTime::from_unix_timestamp_float(d.as_secs_f64()).ok(),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
use std::io::ErrorKind as IoErrorKind;
|
|
||||||
use std::path::{PathBuf, MAIN_SEPARATOR};
|
|
||||||
|
|
||||||
use bstr::{BString, ByteSlice};
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
mod copy;
|
|
||||||
mod metadata;
|
|
||||||
mod options;
|
|
||||||
|
|
||||||
use copy::copy;
|
|
||||||
use metadata::FsMetadata;
|
|
||||||
use options::FsWriteOptions;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_async_function("readFile", fs_read_file)?
|
|
||||||
.with_async_function("readDir", fs_read_dir)?
|
|
||||||
.with_async_function("writeFile", fs_write_file)?
|
|
||||||
.with_async_function("writeDir", fs_write_dir)?
|
|
||||||
.with_async_function("removeFile", fs_remove_file)?
|
|
||||||
.with_async_function("removeDir", fs_remove_dir)?
|
|
||||||
.with_async_function("metadata", fs_metadata)?
|
|
||||||
.with_async_function("isFile", fs_is_file)?
|
|
||||||
.with_async_function("isDir", fs_is_dir)?
|
|
||||||
.with_async_function("move", fs_move)?
|
|
||||||
.with_async_function("copy", fs_copy)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_read_file(lua: &Lua, path: String) -> LuaResult<LuaString> {
|
|
||||||
let bytes = fs::read(&path).await.into_lua_err()?;
|
|
||||||
|
|
||||||
lua.create_string(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_read_dir(_: &Lua, path: String) -> LuaResult<Vec<String>> {
|
|
||||||
let mut dir_strings = Vec::new();
|
|
||||||
let mut dir = fs::read_dir(&path).await.into_lua_err()?;
|
|
||||||
while let Some(dir_entry) = dir.next_entry().await.into_lua_err()? {
|
|
||||||
if let Some(dir_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(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> {
|
|
||||||
fs::write(&path, contents.as_bytes()).await.into_lua_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> {
|
|
||||||
fs::create_dir_all(&path).await.into_lua_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_remove_file(_: &Lua, path: String) -> LuaResult<()> {
|
|
||||||
fs::remove_file(&path).await.into_lua_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_remove_dir(_: &Lua, path: String) -> LuaResult<()> {
|
|
||||||
fs::remove_dir_all(&path).await.into_lua_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_metadata(_: &Lua, path: String) -> LuaResult<FsMetadata> {
|
|
||||||
match fs::metadata(path).await {
|
|
||||||
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(FsMetadata::not_found()),
|
|
||||||
Ok(meta) => Ok(FsMetadata::from(meta)),
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_is_file(_: &Lua, path: String) -> LuaResult<bool> {
|
|
||||||
match fs::metadata(path).await {
|
|
||||||
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
|
|
||||||
Ok(meta) => Ok(meta.is_file()),
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_is_dir(_: &Lua, path: String) -> LuaResult<bool> {
|
|
||||||
match fs::metadata(path).await {
|
|
||||||
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
|
|
||||||
Ok(meta) => Ok(meta.is_dir()),
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_move(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
|
|
||||||
let path_from = PathBuf::from(from);
|
|
||||||
if !path_from.exists() {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"No file or directory exists at the path '{}'",
|
|
||||||
path_from.display()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let path_to = PathBuf::from(to);
|
|
||||||
if !options.overwrite && path_to.exists() {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"A file or directory already exists at the path '{}'",
|
|
||||||
path_to.display()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
fs::rename(path_from, path_to).await.into_lua_err()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fs_copy(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
|
|
||||||
copy(from, to, options).await
|
|
||||||
}
|
|
|
@ -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,59 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
mod options;
|
|
||||||
use options::{LuauCompileOptions, LuauLoadOptions};
|
|
||||||
|
|
||||||
const BYTECODE_ERROR_BYTE: u8 = 0;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("compile", compile_source)?
|
|
||||||
.with_function("load", load_source)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_source<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(source, options): (LuaString<'lua>, LuauCompileOptions),
|
|
||||||
) -> LuaResult<LuaString<'lua>> {
|
|
||||||
let bytecode = options.into_compiler().compile(source);
|
|
||||||
|
|
||||||
match bytecode.first() {
|
|
||||||
Some(&BYTECODE_ERROR_BYTE) => Err(LuaError::RuntimeError(
|
|
||||||
String::from_utf8_lossy(&bytecode).into_owned(),
|
|
||||||
)),
|
|
||||||
Some(_) => lua.create_string(bytecode),
|
|
||||||
None => panic!("Compiling resulted in empty bytecode"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_source<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(source, options): (LuaString<'lua>, LuauLoadOptions),
|
|
||||||
) -> LuaResult<LuaFunction<'lua>> {
|
|
||||||
let mut chunk = lua.load(source.as_bytes()).set_name(options.debug_name);
|
|
||||||
|
|
||||||
if let Some(environment) = options.environment {
|
|
||||||
let environment_with_globals = lua.create_table()?;
|
|
||||||
|
|
||||||
if let Some(meta) = environment.get_metatable() {
|
|
||||||
environment_with_globals.set_metatable(Some(meta));
|
|
||||||
}
|
|
||||||
|
|
||||||
for pair in lua.globals().pairs() {
|
|
||||||
let (key, value): (LuaValue, LuaValue) = pair?;
|
|
||||||
environment_with_globals.set(key, value)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for pair in environment.pairs() {
|
|
||||||
let (key, value): (LuaValue, LuaValue) = pair?;
|
|
||||||
environment_with_globals.set(key, value)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk = chunk.set_environment(environment_with_globals);
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk.into_function()
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua::Compiler as LuaCompiler;
|
|
||||||
|
|
||||||
const DEFAULT_DEBUG_NAME: &str = "luau.load(...)";
|
|
||||||
|
|
||||||
pub struct LuauCompileOptions {
|
|
||||||
pub(crate) optimization_level: u8,
|
|
||||||
pub(crate) coverage_level: u8,
|
|
||||||
pub(crate) debug_level: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuauCompileOptions {
|
|
||||||
pub fn into_compiler(self) -> LuaCompiler {
|
|
||||||
LuaCompiler::default()
|
|
||||||
.set_optimization_level(self.optimization_level)
|
|
||||||
.set_coverage_level(self.coverage_level)
|
|
||||||
.set_debug_level(self.debug_level)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LuauCompileOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
// NOTE: This is the same as LuaCompiler::default() values, but they are
|
|
||||||
// not accessible from outside of mlua so we need to recreate them here.
|
|
||||||
Self {
|
|
||||||
optimization_level: 1,
|
|
||||||
coverage_level: 0,
|
|
||||||
debug_level: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for LuauCompileOptions {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
Ok(match value {
|
|
||||||
LuaValue::Nil => Self::default(),
|
|
||||||
LuaValue::Table(t) => {
|
|
||||||
let mut options = Self::default();
|
|
||||||
|
|
||||||
let get_and_check = |name: &'static str| -> LuaResult<Option<u8>> {
|
|
||||||
match t.get(name)? {
|
|
||||||
Some(n @ (0..=2)) => Ok(Some(n)),
|
|
||||||
Some(n) => Err(LuaError::runtime(format!(
|
|
||||||
"'{name}' must be one of: 0, 1, or 2 - got {n}"
|
|
||||||
))),
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(optimization_level) = get_and_check("optimizationLevel")? {
|
|
||||||
options.optimization_level = optimization_level;
|
|
||||||
}
|
|
||||||
if let Some(coverage_level) = get_and_check("coverageLevel")? {
|
|
||||||
options.coverage_level = coverage_level;
|
|
||||||
}
|
|
||||||
if let Some(debug_level) = get_and_check("debugLevel")? {
|
|
||||||
options.debug_level = debug_level;
|
|
||||||
}
|
|
||||||
|
|
||||||
options
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "CompileOptions",
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid compile options - expected table, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LuauLoadOptions<'lua> {
|
|
||||||
pub(crate) debug_name: String,
|
|
||||||
pub(crate) environment: Option<LuaTable<'lua>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LuauLoadOptions<'_> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
debug_name: DEFAULT_DEBUG_NAME.to_string(),
|
|
||||||
environment: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for LuauLoadOptions<'lua> {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
Ok(match value {
|
|
||||||
LuaValue::Nil => Self::default(),
|
|
||||||
LuaValue::Table(t) => {
|
|
||||||
let mut options = Self::default();
|
|
||||||
|
|
||||||
if let Some(debug_name) = t.get("debugName")? {
|
|
||||||
options.debug_name = debug_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(environment) = t.get("environment")? {
|
|
||||||
options.environment = Some(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
options
|
|
||||||
}
|
|
||||||
LuaValue::String(s) => Self {
|
|
||||||
debug_name: s.to_string_lossy().to_string(),
|
|
||||||
environment: None,
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
return Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "LoadOptions",
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid load options - expected string or table, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
mod datetime;
|
|
||||||
mod fs;
|
|
||||||
mod luau;
|
|
||||||
mod net;
|
|
||||||
mod process;
|
|
||||||
mod regex;
|
|
||||||
mod serde;
|
|
||||||
mod stdio;
|
|
||||||
mod task;
|
|
||||||
|
|
||||||
#[cfg(feature = "roblox")]
|
|
||||||
mod roblox;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
|
||||||
pub enum LuneBuiltin {
|
|
||||||
DateTime,
|
|
||||||
Fs,
|
|
||||||
Luau,
|
|
||||||
Net,
|
|
||||||
Task,
|
|
||||||
Process,
|
|
||||||
Regex,
|
|
||||||
Serde,
|
|
||||||
Stdio,
|
|
||||||
#[cfg(feature = "roblox")]
|
|
||||||
Roblox,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuneBuiltin {
|
|
||||||
pub fn name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::DateTime => "datetime",
|
|
||||||
Self::Fs => "fs",
|
|
||||||
Self::Luau => "luau",
|
|
||||||
Self::Net => "net",
|
|
||||||
Self::Task => "task",
|
|
||||||
Self::Process => "process",
|
|
||||||
Self::Regex => "regex",
|
|
||||||
Self::Serde => "serde",
|
|
||||||
Self::Stdio => "stdio",
|
|
||||||
#[cfg(feature = "roblox")]
|
|
||||||
Self::Roblox => "roblox",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
|
|
||||||
let res = match self {
|
|
||||||
Self::DateTime => datetime::create(lua),
|
|
||||||
Self::Fs => fs::create(lua),
|
|
||||||
Self::Luau => luau::create(lua),
|
|
||||||
Self::Net => net::create(lua),
|
|
||||||
Self::Task => task::create(lua),
|
|
||||||
Self::Process => process::create(lua),
|
|
||||||
Self::Regex => regex::create(lua),
|
|
||||||
Self::Serde => serde::create(lua),
|
|
||||||
Self::Stdio => stdio::create(lua),
|
|
||||||
#[cfg(feature = "roblox")]
|
|
||||||
Self::Roblox => roblox::create(lua),
|
|
||||||
};
|
|
||||||
match res {
|
|
||||||
Ok(v) => v.into_lua_multi(lua),
|
|
||||||
Err(e) => Err(e.context(format!(
|
|
||||||
"Failed to create builtin library '{}'",
|
|
||||||
self.name()
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for LuneBuiltin {
|
|
||||||
type Err = String;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"datetime" => Ok(Self::DateTime),
|
|
||||||
"fs" => Ok(Self::Fs),
|
|
||||||
"luau" => Ok(Self::Luau),
|
|
||||||
"net" => Ok(Self::Net),
|
|
||||||
"task" => Ok(Self::Task),
|
|
||||||
"process" => Ok(Self::Process),
|
|
||||||
"regex" => Ok(Self::Regex),
|
|
||||||
"serde" => Ok(Self::Serde),
|
|
||||||
"stdio" => Ok(Self::Stdio),
|
|
||||||
#[cfg(feature = "roblox")]
|
|
||||||
"roblox" => Ok(Self::Roblox),
|
|
||||||
_ => Err(format!("Unknown builtin library '{s}'")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_ENCODING};
|
|
||||||
|
|
||||||
use crate::lune::{
|
|
||||||
builtins::serde::compress_decompress::{decompress, CompressDecompressFormat},
|
|
||||||
util::TableBuilder,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{config::RequestConfig, util::header_map_to_table};
|
|
||||||
|
|
||||||
const REGISTRY_KEY: &str = "NetClient";
|
|
||||||
|
|
||||||
pub struct NetClientBuilder {
|
|
||||||
builder: reqwest::ClientBuilder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetClientBuilder {
|
|
||||||
pub fn new() -> NetClientBuilder {
|
|
||||||
Self {
|
|
||||||
builder: reqwest::ClientBuilder::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn headers<K, V>(mut self, headers: &[(K, V)]) -> LuaResult<Self>
|
|
||||||
where
|
|
||||||
K: AsRef<str>,
|
|
||||||
V: AsRef<[u8]>,
|
|
||||||
{
|
|
||||||
let mut map = HeaderMap::new();
|
|
||||||
for (key, val) in headers {
|
|
||||||
let hkey = HeaderName::from_str(key.as_ref()).into_lua_err()?;
|
|
||||||
let hval = HeaderValue::from_bytes(val.as_ref()).into_lua_err()?;
|
|
||||||
map.insert(hkey, hval);
|
|
||||||
}
|
|
||||||
self.builder = self.builder.default_headers(map);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(self) -> LuaResult<NetClient> {
|
|
||||||
let client = self.builder.build().into_lua_err()?;
|
|
||||||
Ok(NetClient { inner: client })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct NetClient {
|
|
||||||
inner: reqwest::Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetClient {
|
|
||||||
pub fn from_registry(lua: &Lua) -> Self {
|
|
||||||
lua.named_registry_value(REGISTRY_KEY)
|
|
||||||
.expect("Failed to get NetClient from lua registry")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_registry(self, lua: &Lua) {
|
|
||||||
lua.set_named_registry_value(REGISTRY_KEY, self)
|
|
||||||
.expect("Failed to store NetClient in lua registry");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn request(&self, config: RequestConfig) -> LuaResult<NetClientResponse> {
|
|
||||||
// Create and send the request
|
|
||||||
let mut request = self.inner.request(config.method, config.url);
|
|
||||||
for (query, values) in config.query {
|
|
||||||
request = request.query(
|
|
||||||
&values
|
|
||||||
.iter()
|
|
||||||
.map(|v| (query.as_str(), v))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (header, values) in config.headers {
|
|
||||||
for value in values {
|
|
||||||
request = request.header(header.as_str(), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let res = request
|
|
||||||
.body(config.body.unwrap_or_default())
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.into_lua_err()?;
|
|
||||||
|
|
||||||
// Extract status, headers
|
|
||||||
let res_status = res.status().as_u16();
|
|
||||||
let res_status_text = res.status().canonical_reason();
|
|
||||||
let res_headers = res.headers().clone();
|
|
||||||
|
|
||||||
// Read response bytes
|
|
||||||
let mut res_bytes = res.bytes().await.into_lua_err()?.to_vec();
|
|
||||||
let mut res_decompressed = false;
|
|
||||||
|
|
||||||
// Check for extra options, decompression
|
|
||||||
if config.options.decompress {
|
|
||||||
let decompress_format = res_headers
|
|
||||||
.iter()
|
|
||||||
.find(|(name, _)| {
|
|
||||||
name.as_str()
|
|
||||||
.eq_ignore_ascii_case(CONTENT_ENCODING.as_str())
|
|
||||||
})
|
|
||||||
.and_then(|(_, value)| value.to_str().ok())
|
|
||||||
.and_then(CompressDecompressFormat::detect_from_header_str);
|
|
||||||
if let Some(format) = decompress_format {
|
|
||||||
res_bytes = decompress(format, res_bytes).await?;
|
|
||||||
res_decompressed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(NetClientResponse {
|
|
||||||
ok: (200..300).contains(&res_status),
|
|
||||||
status_code: res_status,
|
|
||||||
status_message: res_status_text.unwrap_or_default().to_string(),
|
|
||||||
headers: res_headers,
|
|
||||||
body: res_bytes,
|
|
||||||
body_decompressed: res_decompressed,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for NetClient {}
|
|
||||||
|
|
||||||
impl FromLua<'_> for NetClient {
|
|
||||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
|
||||||
if let LuaValue::UserData(ud) = value {
|
|
||||||
if let Ok(ctx) = ud.borrow::<NetClient>() {
|
|
||||||
return Ok(ctx.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unreachable!("NetClient should only be used from registry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Lua> for NetClient {
|
|
||||||
fn from(value: &Lua) -> Self {
|
|
||||||
value
|
|
||||||
.named_registry_value(REGISTRY_KEY)
|
|
||||||
.expect("Missing require context in lua registry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NetClientResponse {
|
|
||||||
ok: bool,
|
|
||||||
status_code: u16,
|
|
||||||
status_message: String,
|
|
||||||
headers: HeaderMap,
|
|
||||||
body: Vec<u8>,
|
|
||||||
body_decompressed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetClientResponse {
|
|
||||||
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_value("ok", self.ok)?
|
|
||||||
.with_value("statusCode", self.status_code)?
|
|
||||||
.with_value("statusMessage", self.status_message)?
|
|
||||||
.with_value(
|
|
||||||
"headers",
|
|
||||||
header_map_to_table(lua, self.headers, self.body_decompressed)?,
|
|
||||||
)?
|
|
||||||
.with_value("body", lua.create_string(&self.body)?)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,231 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
net::{IpAddr, Ipv4Addr},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bstr::{BString, ByteSlice};
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use reqwest::Method;
|
|
||||||
|
|
||||||
use super::util::table_to_hash_map;
|
|
||||||
|
|
||||||
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
|
||||||
|
|
||||||
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
|
|
||||||
return {
|
|
||||||
status = 426,
|
|
||||||
body = "Upgrade Required",
|
|
||||||
headers = {
|
|
||||||
Upgrade = "websocket",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
// Net request config
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RequestConfigOptions {
|
|
||||||
pub decompress: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RequestConfigOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self { decompress: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for RequestConfigOptions {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
if let LuaValue::Nil = value {
|
|
||||||
// Nil means default options
|
|
||||||
Ok(Self::default())
|
|
||||||
} else if let LuaValue::Table(tab) = value {
|
|
||||||
// Table means custom options
|
|
||||||
let decompress = match tab.get::<_, Option<bool>>("decompress") {
|
|
||||||
Ok(decomp) => Ok(decomp.unwrap_or(true)),
|
|
||||||
Err(_) => Err(LuaError::RuntimeError(
|
|
||||||
"Invalid option value for 'decompress' in request config options".to_string(),
|
|
||||||
)),
|
|
||||||
}?;
|
|
||||||
Ok(Self { decompress })
|
|
||||||
} else {
|
|
||||||
// Anything else is invalid
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "RequestConfigOptions",
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid request config options - expected table or nil, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RequestConfig {
|
|
||||||
pub url: String,
|
|
||||||
pub method: Method,
|
|
||||||
pub query: HashMap<String, Vec<String>>,
|
|
||||||
pub headers: HashMap<String, Vec<String>>,
|
|
||||||
pub body: Option<Vec<u8>>,
|
|
||||||
pub options: RequestConfigOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromLua<'_> for RequestConfig {
|
|
||||||
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
|
|
||||||
// If we just got a string we assume its a GET request to a given url
|
|
||||||
if let LuaValue::String(s) = value {
|
|
||||||
Ok(Self {
|
|
||||||
url: s.to_string_lossy().to_string(),
|
|
||||||
method: Method::GET,
|
|
||||||
query: HashMap::new(),
|
|
||||||
headers: HashMap::new(),
|
|
||||||
body: None,
|
|
||||||
options: Default::default(),
|
|
||||||
})
|
|
||||||
} else if let LuaValue::Table(tab) = value {
|
|
||||||
// If we got a table we are able to configure the entire request
|
|
||||||
// Extract url
|
|
||||||
let url = match tab.get::<_, LuaString>("url") {
|
|
||||||
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
|
|
||||||
Err(_) => Err(LuaError::runtime("Missing 'url' in request config")),
|
|
||||||
}?;
|
|
||||||
// Extract method
|
|
||||||
let method = match tab.get::<_, LuaString>("method") {
|
|
||||||
Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(),
|
|
||||||
Err(_) => "GET".to_string(),
|
|
||||||
};
|
|
||||||
// Extract query
|
|
||||||
let query = match tab.get::<_, LuaTable>("query") {
|
|
||||||
Ok(tab) => table_to_hash_map(tab, "query")?,
|
|
||||||
Err(_) => HashMap::new(),
|
|
||||||
};
|
|
||||||
// Extract headers
|
|
||||||
let headers = match tab.get::<_, LuaTable>("headers") {
|
|
||||||
Ok(tab) => table_to_hash_map(tab, "headers")?,
|
|
||||||
Err(_) => HashMap::new(),
|
|
||||||
};
|
|
||||||
// Extract body
|
|
||||||
let body = match tab.get::<_, BString>("body") {
|
|
||||||
Ok(config_body) => Some(config_body.as_bytes().to_owned()),
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert method string into proper enum
|
|
||||||
let method = method.trim().to_ascii_uppercase();
|
|
||||||
let method = match method.as_ref() {
|
|
||||||
"GET" => Ok(Method::GET),
|
|
||||||
"POST" => Ok(Method::POST),
|
|
||||||
"PUT" => Ok(Method::PUT),
|
|
||||||
"DELETE" => Ok(Method::DELETE),
|
|
||||||
"HEAD" => Ok(Method::HEAD),
|
|
||||||
"OPTIONS" => Ok(Method::OPTIONS),
|
|
||||||
"PATCH" => Ok(Method::PATCH),
|
|
||||||
_ => Err(LuaError::RuntimeError(format!(
|
|
||||||
"Invalid request config method '{}'",
|
|
||||||
&method
|
|
||||||
))),
|
|
||||||
}?;
|
|
||||||
// Parse any extra options given
|
|
||||||
let options = match tab.get::<_, LuaValue>("options") {
|
|
||||||
Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?,
|
|
||||||
Err(_) => RequestConfigOptions::default(),
|
|
||||||
};
|
|
||||||
// All good, validated and we got what we need
|
|
||||||
Ok(Self {
|
|
||||||
url,
|
|
||||||
method,
|
|
||||||
query,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
options,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Anything else is invalid
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "RequestConfig",
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid request config - expected string or table, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Net serve config
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ServeConfig<'a> {
|
|
||||||
pub address: IpAddr,
|
|
||||||
pub handle_request: LuaFunction<'a>,
|
|
||||||
pub handle_web_socket: Option<LuaFunction<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for ServeConfig<'lua> {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
if let LuaValue::Function(f) = &value {
|
|
||||||
// Single function = request handler, rest is default
|
|
||||||
Ok(ServeConfig {
|
|
||||||
handle_request: f.clone(),
|
|
||||||
handle_web_socket: None,
|
|
||||||
address: DEFAULT_IP_ADDRESS,
|
|
||||||
})
|
|
||||||
} else if let LuaValue::Table(t) = &value {
|
|
||||||
// Table means custom options
|
|
||||||
let address: Option<LuaString> = t.get("address")?;
|
|
||||||
let handle_request: Option<LuaFunction> = t.get("handleRequest")?;
|
|
||||||
let handle_web_socket: Option<LuaFunction> = t.get("handleWebSocket")?;
|
|
||||||
if handle_request.is_some() || handle_web_socket.is_some() {
|
|
||||||
let address: IpAddr = match &address {
|
|
||||||
Some(addr) => {
|
|
||||||
let addr_str = addr.to_str()?;
|
|
||||||
|
|
||||||
addr_str
|
|
||||||
.trim_start_matches("http://")
|
|
||||||
.trim_start_matches("https://")
|
|
||||||
.parse()
|
|
||||||
.map_err(|_e| LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "ServeConfig",
|
|
||||||
message: Some(format!(
|
|
||||||
"IP address format is incorrect - \
|
|
||||||
expected an IP in the form 'http://0.0.0.0' or '0.0.0.0', \
|
|
||||||
got '{addr_str}'"
|
|
||||||
)),
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
None => DEFAULT_IP_ADDRESS,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
address,
|
|
||||||
handle_request: handle_request.unwrap_or_else(|| {
|
|
||||||
lua.load(WEB_SOCKET_UPDGRADE_REQUEST_HANDLER)
|
|
||||||
.into_function()
|
|
||||||
.expect("Failed to create default http responder function")
|
|
||||||
}),
|
|
||||||
handle_web_socket,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "ServeConfig",
|
|
||||||
message: Some(String::from(
|
|
||||||
"Invalid serve config - expected table with 'handleRequest' or 'handleWebSocket' function",
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Anything else is invalid
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "ServeConfig",
|
|
||||||
message: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
#![allow(unused_variables)]
|
|
||||||
|
|
||||||
use bstr::BString;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua_luau_scheduler::LuaSpawnExt;
|
|
||||||
|
|
||||||
mod client;
|
|
||||||
mod config;
|
|
||||||
mod server;
|
|
||||||
mod util;
|
|
||||||
mod websocket;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
use self::{
|
|
||||||
client::{NetClient, NetClientBuilder},
|
|
||||||
config::{RequestConfig, ServeConfig},
|
|
||||||
server::serve,
|
|
||||||
util::create_user_agent_header,
|
|
||||||
websocket::NetWebSocket,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::serde::encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat};
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
NetClientBuilder::new()
|
|
||||||
.headers(&[("User-Agent", create_user_agent_header(lua)?)])?
|
|
||||||
.build()?
|
|
||||||
.into_registry(lua);
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("jsonEncode", net_json_encode)?
|
|
||||||
.with_function("jsonDecode", net_json_decode)?
|
|
||||||
.with_async_function("request", net_request)?
|
|
||||||
.with_async_function("socket", net_socket)?
|
|
||||||
.with_async_function("serve", net_serve)?
|
|
||||||
.with_function("urlEncode", net_url_encode)?
|
|
||||||
.with_function("urlDecode", net_url_decode)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn net_json_encode<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(val, pretty): (LuaValue<'lua>, Option<bool>),
|
|
||||||
) -> LuaResult<LuaString<'lua>> {
|
|
||||||
EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()))
|
|
||||||
.serialize_to_string(lua, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn net_json_decode(lua: &Lua, json: BString) -> LuaResult<LuaValue> {
|
|
||||||
EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
|
|
||||||
let client = NetClient::from_registry(lua);
|
|
||||||
// NOTE: We spawn the request as a background task to free up resources in lua
|
|
||||||
let res = lua.spawn(async move { client.request(config).await });
|
|
||||||
res.await?.into_lua_table(lua)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaTable> {
|
|
||||||
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
|
|
||||||
NetWebSocket::new(ws).into_lua_table(lua)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn net_serve<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(port, config): (u16, ServeConfig<'lua>),
|
|
||||||
) -> LuaResult<LuaTable<'lua>> {
|
|
||||||
serve(lua, port, config).await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn net_url_encode<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
|
|
||||||
) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
if matches!(as_binary, Some(true)) {
|
|
||||||
urlencoding::encode_binary(lua_string.as_bytes()).into_lua(lua)
|
|
||||||
} else {
|
|
||||||
urlencoding::encode(lua_string.to_str()?).into_lua(lua)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn net_url_decode<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
|
|
||||||
) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
if matches!(as_binary, Some(true)) {
|
|
||||||
urlencoding::decode_binary(lua_string.as_bytes()).into_lua(lua)
|
|
||||||
} else {
|
|
||||||
urlencoding::decode(lua_string.to_str()?)
|
|
||||||
.map_err(|e| LuaError::RuntimeError(format!("Encountered invalid encoding - {e}")))?
|
|
||||||
.into_lua(lua)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub(super) struct SvcKeys {
|
|
||||||
key_request: &'static str,
|
|
||||||
key_websocket: Option<&'static str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SvcKeys {
|
|
||||||
pub(super) fn new<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
handle_request: LuaFunction<'lua>,
|
|
||||||
handle_websocket: Option<LuaFunction<'lua>>,
|
|
||||||
) -> LuaResult<Self> {
|
|
||||||
static SERVE_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
|
||||||
let count = SERVE_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
||||||
|
|
||||||
// NOTE: We leak strings here, but this is an acceptable tradeoff since programs
|
|
||||||
// generally only start one or a couple of servers and they are usually never dropped.
|
|
||||||
// Leaking here lets us keep this struct Copy and access the request handler callbacks
|
|
||||||
// very performantly, significantly reducing the per-request overhead of the server.
|
|
||||||
let key_request: &'static str =
|
|
||||||
Box::leak(format!("__net_serve_request_{count}").into_boxed_str());
|
|
||||||
let key_websocket: Option<&'static str> = if handle_websocket.is_some() {
|
|
||||||
Some(Box::leak(
|
|
||||||
format!("__net_serve_websocket_{count}").into_boxed_str(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
lua.set_named_registry_value(key_request, handle_request)?;
|
|
||||||
if let Some(key) = key_websocket {
|
|
||||||
lua.set_named_registry_value(key, handle_websocket.unwrap())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
key_request,
|
|
||||||
key_websocket,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn has_websocket_handler(&self) -> bool {
|
|
||||||
self.key_websocket.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn request_handler<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaFunction<'lua>> {
|
|
||||||
lua.named_registry_value(self.key_request)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn websocket_handler<'lua>(
|
|
||||||
&self,
|
|
||||||
lua: &'lua Lua,
|
|
||||||
) -> LuaResult<Option<LuaFunction<'lua>>> {
|
|
||||||
self.key_websocket
|
|
||||||
.map(|key| lua.named_registry_value(key))
|
|
||||||
.transpose()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
use std::{
|
|
||||||
net::SocketAddr,
|
|
||||||
rc::{Rc, Weak},
|
|
||||||
};
|
|
||||||
|
|
||||||
use hyper::server::conn::http1;
|
|
||||||
use hyper_util::rt::TokioIo;
|
|
||||||
use tokio::{net::TcpListener, pin};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua_luau_scheduler::LuaSpawnExt;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
use super::config::ServeConfig;
|
|
||||||
|
|
||||||
mod keys;
|
|
||||||
mod request;
|
|
||||||
mod response;
|
|
||||||
mod service;
|
|
||||||
|
|
||||||
use keys::SvcKeys;
|
|
||||||
use service::Svc;
|
|
||||||
|
|
||||||
pub async fn serve<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
port: u16,
|
|
||||||
config: ServeConfig<'lua>,
|
|
||||||
) -> LuaResult<LuaTable<'lua>> {
|
|
||||||
let addr: SocketAddr = (config.address, port).into();
|
|
||||||
let listener = TcpListener::bind(addr).await?;
|
|
||||||
|
|
||||||
let (lua_svc, lua_inner) = {
|
|
||||||
let rc = lua
|
|
||||||
.app_data_ref::<Weak<Lua>>()
|
|
||||||
.expect("Missing weak lua ref")
|
|
||||||
.upgrade()
|
|
||||||
.expect("Lua was dropped unexpectedly");
|
|
||||||
(Rc::clone(&rc), rc)
|
|
||||||
};
|
|
||||||
|
|
||||||
let keys = SvcKeys::new(lua, config.handle_request, config.handle_web_socket)?;
|
|
||||||
let svc = Svc {
|
|
||||||
lua: lua_svc,
|
|
||||||
addr,
|
|
||||||
keys,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
|
|
||||||
lua.spawn_local(async move {
|
|
||||||
let mut shutdown_rx_outer = shutdown_rx.clone();
|
|
||||||
loop {
|
|
||||||
// Create futures for accepting new connections and shutting down
|
|
||||||
let fut_shutdown = shutdown_rx_outer.changed();
|
|
||||||
let fut_accept = async {
|
|
||||||
let stream = match listener.accept().await {
|
|
||||||
Err(_) => return,
|
|
||||||
Ok((s, _)) => s,
|
|
||||||
};
|
|
||||||
|
|
||||||
let io = TokioIo::new(stream);
|
|
||||||
let svc = svc.clone();
|
|
||||||
let mut shutdown_rx_inner = shutdown_rx.clone();
|
|
||||||
|
|
||||||
lua_inner.spawn_local(async move {
|
|
||||||
let conn = http1::Builder::new()
|
|
||||||
.keep_alive(true) // Web sockets need this
|
|
||||||
.serve_connection(io, svc)
|
|
||||||
.with_upgrades();
|
|
||||||
// NOTE: Because we need to use keep_alive for websockets, we need to
|
|
||||||
// also manually poll this future and handle the shutdown signal here
|
|
||||||
pin!(conn);
|
|
||||||
tokio::select! {
|
|
||||||
_ = conn.as_mut() => {}
|
|
||||||
_ = shutdown_rx_inner.changed() => {
|
|
||||||
conn.as_mut().graceful_shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wait for either a new connection or a shutdown signal
|
|
||||||
tokio::select! {
|
|
||||||
_ = fut_accept => {}
|
|
||||||
res = fut_shutdown => {
|
|
||||||
// NOTE: We will only get a RecvError here if the serve handle is dropped,
|
|
||||||
// this means lua has garbage collected it and the user does not want
|
|
||||||
// to manually stop the server using the serve handle. Run forever.
|
|
||||||
if res.is_ok() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_value("ip", addr.ip().to_string())?
|
|
||||||
.with_value("port", addr.port())?
|
|
||||||
.with_function("stop", move |lua, _: ()| match shutdown_tx.send(true) {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(_) => Err(LuaError::runtime("Server already stopped")),
|
|
||||||
})?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
use std::{collections::HashMap, net::SocketAddr};
|
|
||||||
|
|
||||||
use http::request::Parts;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
pub(super) struct LuaRequest {
|
|
||||||
pub(super) _remote_addr: SocketAddr,
|
|
||||||
pub(super) head: Parts,
|
|
||||||
pub(super) body: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaRequest {
|
|
||||||
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let method = self.head.method.as_str().to_string();
|
|
||||||
let path = self.head.uri.path().to_string();
|
|
||||||
let body = lua.create_string(&self.body)?;
|
|
||||||
|
|
||||||
let query: HashMap<LuaString, LuaString> = self
|
|
||||||
.head
|
|
||||||
.uri
|
|
||||||
.query()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.split('&')
|
|
||||||
.filter_map(|q| q.split_once('='))
|
|
||||||
.map(|(k, v)| {
|
|
||||||
let k = lua.create_string(k)?;
|
|
||||||
let v = lua.create_string(v)?;
|
|
||||||
Ok((k, v))
|
|
||||||
})
|
|
||||||
.collect::<LuaResult<_>>()?;
|
|
||||||
|
|
||||||
let headers: HashMap<LuaString, LuaString> = self
|
|
||||||
.head
|
|
||||||
.headers
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| {
|
|
||||||
let k = lua.create_string(k.as_str())?;
|
|
||||||
let v = lua.create_string(v.as_bytes())?;
|
|
||||||
Ok((k, v))
|
|
||||||
})
|
|
||||||
.collect::<LuaResult<_>>()?;
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_value("method", method)?
|
|
||||||
.with_value("path", path)?
|
|
||||||
.with_value("query", query)?
|
|
||||||
.with_value("headers", headers)?
|
|
||||||
.with_value("body", body)?
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use bstr::{BString, ByteSlice};
|
|
||||||
use http_body_util::Full;
|
|
||||||
use hyper::{
|
|
||||||
body::Bytes,
|
|
||||||
header::{HeaderName, HeaderValue},
|
|
||||||
HeaderMap, Response,
|
|
||||||
};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub(super) enum LuaResponseKind {
|
|
||||||
PlainText,
|
|
||||||
Table,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) struct LuaResponse {
|
|
||||||
pub(super) kind: LuaResponseKind,
|
|
||||||
pub(super) status: u16,
|
|
||||||
pub(super) headers: HeaderMap,
|
|
||||||
pub(super) body: Option<Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaResponse {
|
|
||||||
pub(super) fn into_response(self) -> LuaResult<Response<Full<Bytes>>> {
|
|
||||||
Ok(match self.kind {
|
|
||||||
LuaResponseKind::PlainText => Response::builder()
|
|
||||||
.status(200)
|
|
||||||
.header("Content-Type", "text/plain")
|
|
||||||
.body(Full::new(Bytes::from(self.body.unwrap())))
|
|
||||||
.into_lua_err()?,
|
|
||||||
LuaResponseKind::Table => {
|
|
||||||
let mut response = Response::builder()
|
|
||||||
.status(self.status)
|
|
||||||
.body(Full::new(Bytes::from(self.body.unwrap_or_default())))
|
|
||||||
.into_lua_err()?;
|
|
||||||
response.headers_mut().extend(self.headers);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromLua<'_> for LuaResponse {
|
|
||||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
|
||||||
match value {
|
|
||||||
// Plain strings from the handler are plaintext responses
|
|
||||||
LuaValue::String(s) => Ok(Self {
|
|
||||||
kind: LuaResponseKind::PlainText,
|
|
||||||
status: 200,
|
|
||||||
headers: HeaderMap::new(),
|
|
||||||
body: Some(s.as_bytes().to_vec()),
|
|
||||||
}),
|
|
||||||
// Tables are more detailed responses with potential status, headers, body
|
|
||||||
LuaValue::Table(t) => {
|
|
||||||
let status: Option<u16> = t.get("status")?;
|
|
||||||
let headers: Option<LuaTable> = t.get("headers")?;
|
|
||||||
let body: Option<BString> = t.get("body")?;
|
|
||||||
|
|
||||||
let mut headers_map = HeaderMap::new();
|
|
||||||
if let Some(headers) = headers {
|
|
||||||
for pair in headers.pairs::<String, LuaString>() {
|
|
||||||
let (h, v) = pair?;
|
|
||||||
let name = HeaderName::from_str(&h).into_lua_err()?;
|
|
||||||
let value = HeaderValue::from_bytes(v.as_bytes()).into_lua_err()?;
|
|
||||||
headers_map.insert(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let body_bytes = body.map(|s| s.as_bytes().to_vec());
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
kind: LuaResponseKind::Table,
|
|
||||||
status: status.unwrap_or(200),
|
|
||||||
headers: headers_map,
|
|
||||||
body: body_bytes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Anything else is an error
|
|
||||||
value => Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "NetServeResponse",
|
|
||||||
message: None,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
use std::{future::Future, net::SocketAddr, pin::Pin, rc::Rc};
|
|
||||||
|
|
||||||
use http_body_util::{BodyExt, Full};
|
|
||||||
use hyper::{
|
|
||||||
body::{Bytes, Incoming},
|
|
||||||
service::Service,
|
|
||||||
Request, Response,
|
|
||||||
};
|
|
||||||
use hyper_tungstenite::{is_upgrade_request, upgrade};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
super::websocket::NetWebSocket, keys::SvcKeys, request::LuaRequest, response::LuaResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(super) struct Svc {
|
|
||||||
pub(super) lua: Rc<Lua>,
|
|
||||||
pub(super) addr: SocketAddr,
|
|
||||||
pub(super) keys: SvcKeys,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Service<Request<Incoming>> for Svc {
|
|
||||||
type Response = Response<Full<Bytes>>;
|
|
||||||
type Error = LuaError;
|
|
||||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
|
||||||
|
|
||||||
fn call(&self, req: Request<Incoming>) -> Self::Future {
|
|
||||||
let lua = self.lua.clone();
|
|
||||||
let addr = self.addr;
|
|
||||||
let keys = self.keys;
|
|
||||||
|
|
||||||
if keys.has_websocket_handler() && is_upgrade_request(&req) {
|
|
||||||
Box::pin(async move {
|
|
||||||
let (res, sock) = upgrade(req, None).into_lua_err()?;
|
|
||||||
|
|
||||||
let lua_inner = lua.clone();
|
|
||||||
lua.spawn_local(async move {
|
|
||||||
let sock = sock.await.unwrap();
|
|
||||||
let lua_sock = NetWebSocket::new(sock);
|
|
||||||
let lua_tab = lua_sock.into_lua_table(&lua_inner).unwrap();
|
|
||||||
|
|
||||||
let handler_websocket: LuaFunction =
|
|
||||||
keys.websocket_handler(&lua_inner).unwrap().unwrap();
|
|
||||||
|
|
||||||
lua_inner
|
|
||||||
.push_thread_back(handler_websocket, lua_tab)
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let (head, body) = req.into_parts();
|
|
||||||
|
|
||||||
Box::pin(async move {
|
|
||||||
let handler_request: LuaFunction = keys.request_handler(&lua).unwrap();
|
|
||||||
|
|
||||||
let body = body.collect().await.into_lua_err()?;
|
|
||||||
let body = body.to_bytes().to_vec();
|
|
||||||
|
|
||||||
let lua_req = LuaRequest {
|
|
||||||
_remote_addr: addr,
|
|
||||||
head,
|
|
||||||
body,
|
|
||||||
};
|
|
||||||
let lua_req_table = lua_req.into_lua_table(&lua)?;
|
|
||||||
|
|
||||||
let thread_id = lua.push_thread_back(handler_request, lua_req_table)?;
|
|
||||||
lua.track_thread(thread_id);
|
|
||||||
lua.wait_for_thread(thread_id).await;
|
|
||||||
let thread_res = lua
|
|
||||||
.get_thread_result(thread_id)
|
|
||||||
.expect("Missing handler thread result")?;
|
|
||||||
|
|
||||||
LuaResponse::from_lua_multi(thread_res, &lua)?.into_response()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH};
|
|
||||||
use reqwest::header::HeaderMap;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
pub fn create_user_agent_header(lua: &Lua) -> LuaResult<String> {
|
|
||||||
let version_global = lua
|
|
||||||
.globals()
|
|
||||||
.get::<_, LuaString>("_VERSION")
|
|
||||||
.expect("Missing _VERSION global");
|
|
||||||
|
|
||||||
let version_global_str = version_global
|
|
||||||
.to_str()
|
|
||||||
.context("Invalid utf8 found in _VERSION global")?;
|
|
||||||
|
|
||||||
let (package_name, full_version) = version_global_str.split_once(' ').unwrap();
|
|
||||||
|
|
||||||
Ok(format!("{}/{}", package_name.to_lowercase(), full_version))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn header_map_to_table(
|
|
||||||
lua: &Lua,
|
|
||||||
headers: HeaderMap,
|
|
||||||
remove_content_headers: bool,
|
|
||||||
) -> LuaResult<LuaTable> {
|
|
||||||
let mut res_headers: HashMap<String, Vec<String>> = HashMap::new();
|
|
||||||
for (name, value) in headers.iter() {
|
|
||||||
let name = name.as_str();
|
|
||||||
let value = value.to_str().unwrap().to_owned();
|
|
||||||
if let Some(existing) = res_headers.get_mut(name) {
|
|
||||||
existing.push(value);
|
|
||||||
} else {
|
|
||||||
res_headers.insert(name.to_owned(), vec![value]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if remove_content_headers {
|
|
||||||
let content_encoding_header_str = CONTENT_ENCODING.as_str();
|
|
||||||
let content_length_header_str = CONTENT_LENGTH.as_str();
|
|
||||||
res_headers.retain(|name, _| {
|
|
||||||
name != content_encoding_header_str && name != content_length_header_str
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut builder = TableBuilder::new(lua)?;
|
|
||||||
for (name, mut values) in res_headers {
|
|
||||||
if values.len() == 1 {
|
|
||||||
let value = values.pop().unwrap().into_lua(lua)?;
|
|
||||||
builder = builder.with_value(name, value)?;
|
|
||||||
} else {
|
|
||||||
let values = TableBuilder::new(lua)?
|
|
||||||
.with_sequential_values(values)?
|
|
||||||
.build_readonly()?
|
|
||||||
.into_lua(lua)?;
|
|
||||||
builder = builder.with_value(name, values)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn table_to_hash_map(
|
|
||||||
tab: LuaTable,
|
|
||||||
tab_origin_key: &'static str,
|
|
||||||
) -> LuaResult<HashMap<String, Vec<String>>> {
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
|
|
||||||
for pair in tab.pairs::<String, LuaValue>() {
|
|
||||||
let (key, value) = pair?;
|
|
||||||
match value {
|
|
||||||
LuaValue::String(s) => {
|
|
||||||
map.insert(key, vec![s.to_str()?.to_owned()]);
|
|
||||||
}
|
|
||||||
LuaValue::Table(t) => {
|
|
||||||
let mut values = Vec::new();
|
|
||||||
for value in t.sequence_values::<LuaString>() {
|
|
||||||
values.push(value?.to_str()?.to_owned());
|
|
||||||
}
|
|
||||||
map.insert(key, values);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(LuaError::runtime(format!(
|
|
||||||
"Value for '{tab_origin_key}' must be a string or array of strings",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
use std::sync::{
|
|
||||||
atomic::{AtomicBool, AtomicU16, Ordering},
|
|
||||||
Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bstr::{BString, ByteSlice};
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use futures_util::{
|
|
||||||
stream::{SplitSink, SplitStream},
|
|
||||||
SinkExt, StreamExt,
|
|
||||||
};
|
|
||||||
use tokio::{
|
|
||||||
io::{AsyncRead, AsyncWrite},
|
|
||||||
sync::Mutex as AsyncMutex,
|
|
||||||
};
|
|
||||||
|
|
||||||
use hyper_tungstenite::{
|
|
||||||
tungstenite::{
|
|
||||||
protocol::{frame::coding::CloseCode as WsCloseCode, CloseFrame as WsCloseFrame},
|
|
||||||
Message as WsMessage,
|
|
||||||
},
|
|
||||||
WebSocketStream,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
// Wrapper implementation for compatibility and changing colon syntax to dot syntax
|
|
||||||
const WEB_SOCKET_IMPL_LUA: &str = r#"
|
|
||||||
return freeze(setmetatable({
|
|
||||||
close = function(...)
|
|
||||||
return websocket:close(...)
|
|
||||||
end,
|
|
||||||
send = function(...)
|
|
||||||
return websocket:send(...)
|
|
||||||
end,
|
|
||||||
next = function(...)
|
|
||||||
return websocket:next(...)
|
|
||||||
end,
|
|
||||||
}, {
|
|
||||||
__index = function(self, key)
|
|
||||||
if key == "closeCode" then
|
|
||||||
return websocket.closeCode
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}))
|
|
||||||
"#;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct NetWebSocket<T> {
|
|
||||||
close_code_exists: Arc<AtomicBool>,
|
|
||||||
close_code_value: Arc<AtomicU16>,
|
|
||||||
read_stream: Arc<AsyncMutex<SplitStream<WebSocketStream<T>>>>,
|
|
||||||
write_stream: Arc<AsyncMutex<SplitSink<WebSocketStream<T>, WsMessage>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Clone for NetWebSocket<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
close_code_exists: Arc::clone(&self.close_code_exists),
|
|
||||||
close_code_value: Arc::clone(&self.close_code_value),
|
|
||||||
read_stream: Arc::clone(&self.read_stream),
|
|
||||||
write_stream: Arc::clone(&self.write_stream),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> NetWebSocket<T>
|
|
||||||
where
|
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
pub fn new(value: WebSocketStream<T>) -> Self {
|
|
||||||
let (write, read) = value.split();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
close_code_exists: Arc::new(AtomicBool::new(false)),
|
|
||||||
close_code_value: Arc::new(AtomicU16::new(0)),
|
|
||||||
read_stream: Arc::new(AsyncMutex::new(read)),
|
|
||||||
write_stream: Arc::new(AsyncMutex::new(write)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_close_code(&self) -> Option<u16> {
|
|
||||||
if self.close_code_exists.load(Ordering::Relaxed) {
|
|
||||||
Some(self.close_code_value.load(Ordering::Relaxed))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_close_code(&self, code: u16) {
|
|
||||||
self.close_code_exists.store(true, Ordering::Relaxed);
|
|
||||||
self.close_code_value.store(code, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(&self, msg: WsMessage) -> LuaResult<()> {
|
|
||||||
let mut ws = self.write_stream.lock().await;
|
|
||||||
ws.send(msg).await.into_lua_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn next(&self) -> LuaResult<Option<WsMessage>> {
|
|
||||||
let mut ws = self.read_stream.lock().await;
|
|
||||||
ws.next().await.transpose().into_lua_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn close(&self, code: Option<u16>) -> LuaResult<()> {
|
|
||||||
if self.close_code_exists.load(Ordering::Relaxed) {
|
|
||||||
return Err(LuaError::runtime("Socket has already been closed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.send(WsMessage::Close(Some(WsCloseFrame {
|
|
||||||
code: match code {
|
|
||||||
Some(code) if (1000..=4999).contains(&code) => WsCloseCode::from(code),
|
|
||||||
Some(code) => {
|
|
||||||
return Err(LuaError::runtime(format!(
|
|
||||||
"Close code must be between 1000 and 4999, got {code}"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
None => WsCloseCode::Normal,
|
|
||||||
},
|
|
||||||
reason: "".into(),
|
|
||||||
})))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut ws = self.write_stream.lock().await;
|
|
||||||
ws.close().await.into_lua_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?;
|
|
||||||
let table_freeze = lua
|
|
||||||
.globals()
|
|
||||||
.get::<_, LuaTable>("table")?
|
|
||||||
.get::<_, LuaFunction>("freeze")?;
|
|
||||||
|
|
||||||
let env = TableBuilder::new(lua)?
|
|
||||||
.with_value("websocket", self.clone())?
|
|
||||||
.with_value("setmetatable", setmetatable)?
|
|
||||||
.with_value("freeze", table_freeze)?
|
|
||||||
.build_readonly()?;
|
|
||||||
|
|
||||||
lua.load(WEB_SOCKET_IMPL_LUA)
|
|
||||||
.set_name("websocket")
|
|
||||||
.set_environment(env)
|
|
||||||
.eval()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> LuaUserData for NetWebSocket<T>
|
|
||||||
where
|
|
||||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
|
||||||
{
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("closeCode", |_, this| Ok(this.get_close_code()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_async_method("close", |lua, this, code: Option<u16>| async move {
|
|
||||||
this.close(code).await
|
|
||||||
});
|
|
||||||
|
|
||||||
methods.add_async_method(
|
|
||||||
"send",
|
|
||||||
|_, this, (string, as_binary): (BString, Option<bool>)| async move {
|
|
||||||
this.send(if as_binary.unwrap_or_default() {
|
|
||||||
WsMessage::Binary(string.as_bytes().to_vec())
|
|
||||||
} else {
|
|
||||||
let s = string.to_str().into_lua_err()?;
|
|
||||||
WsMessage::Text(s.to_string())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
methods.add_async_method("next", |lua, this, _: ()| async move {
|
|
||||||
let msg = this.next().await?;
|
|
||||||
|
|
||||||
if let Some(WsMessage::Close(Some(frame))) = msg.as_ref() {
|
|
||||||
this.set_close_code(frame.code.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(match msg {
|
|
||||||
Some(WsMessage::Binary(bin)) => LuaValue::String(lua.create_string(bin)?),
|
|
||||||
Some(WsMessage::Text(txt)) => LuaValue::String(lua.create_string(txt)?),
|
|
||||||
Some(WsMessage::Close(_)) | None => LuaValue::Nil,
|
|
||||||
// Ignore ping/pong/frame messages, they are handled by tungstenite
|
|
||||||
msg => unreachable!("Unhandled message: {:?}", msg),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
use std::{
|
|
||||||
env::{self, consts},
|
|
||||||
path,
|
|
||||||
process::Stdio,
|
|
||||||
};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
|
|
||||||
use os_str_bytes::RawOsString;
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
|
|
||||||
use crate::lune::util::{paths::CWD, TableBuilder};
|
|
||||||
|
|
||||||
mod tee_writer;
|
|
||||||
|
|
||||||
mod options;
|
|
||||||
use options::ProcessSpawnOptions;
|
|
||||||
|
|
||||||
mod wait_for_child;
|
|
||||||
use wait_for_child::{wait_for_child, WaitForChildResult};
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let cwd_str = {
|
|
||||||
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_vec = lua
|
|
||||||
.app_data_ref::<Vec<String>>()
|
|
||||||
.ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))?
|
|
||||||
.clone();
|
|
||||||
let args_tab = TableBuilder::new(lua)?
|
|
||||||
.with_sequential_values(args_vec)?
|
|
||||||
.build_readonly()?;
|
|
||||||
// Create proxied table for env that gets & sets real env vars
|
|
||||||
let env_tab = TableBuilder::new(lua)?
|
|
||||||
.with_metatable(
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function(LuaMetaMethod::Index.name(), process_env_get)?
|
|
||||||
.with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
|
|
||||||
.with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
|
|
||||||
.build_readonly()?,
|
|
||||||
)?
|
|
||||||
.build_readonly()?;
|
|
||||||
// Create our process exit function, the scheduler crate provides this
|
|
||||||
let fns = Functions::new(lua)?;
|
|
||||||
let process_exit = fns.exit;
|
|
||||||
// Create the full process table
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_value("os", os)?
|
|
||||||
.with_value("arch", arch)?
|
|
||||||
.with_value("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<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(_, key): (LuaValue<'lua>, String),
|
|
||||||
) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
match env::var_os(key) {
|
|
||||||
Some(value) => {
|
|
||||||
let raw_value = RawOsString::new(value);
|
|
||||||
Ok(LuaValue::String(
|
|
||||||
lua.create_string(raw_value.to_raw_bytes())?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
None => Ok(LuaValue::Nil),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_env_set<'lua>(
|
|
||||||
_: &'lua Lua,
|
|
||||||
(_, key, value): (LuaValue<'lua>, String, Option<String>),
|
|
||||||
) -> LuaResult<()> {
|
|
||||||
// Make sure key is valid, otherwise set_var will panic
|
|
||||||
if key.is_empty() {
|
|
||||||
Err(LuaError::RuntimeError("Key must not be empty".to_string()))
|
|
||||||
} else if key.contains('=') {
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Key must not contain the equals character '='".to_string(),
|
|
||||||
))
|
|
||||||
} else if key.contains('\0') {
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Key must not contain the NUL character".to_string(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
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.to_raw_bytes())?),
|
|
||||||
LuaValue::String(lua.create_string(raw_value.to_raw_bytes())?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
None => Ok((LuaValue::Nil, LuaValue::Nil)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn process_spawn(
|
|
||||||
lua: &Lua,
|
|
||||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
|
||||||
) -> LuaResult<LuaTable> {
|
|
||||||
let res = lua
|
|
||||||
.spawn(spawn_command(program, args, options))
|
|
||||||
.await
|
|
||||||
.expect("Failed to receive result of spawned process");
|
|
||||||
|
|
||||||
/*
|
|
||||||
NOTE: If an exit code was not given by the child process,
|
|
||||||
we default to 1 if it yielded any error output, otherwise 0
|
|
||||||
|
|
||||||
An exit code may be missing if the process was terminated by
|
|
||||||
some external signal, which is the only time we use this default
|
|
||||||
*/
|
|
||||||
let code = res.status.code().unwrap_or(match res.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(&res.stdout)?)?
|
|
||||||
.with_value("stderr", lua.create_string(&res.stderr)?)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn spawn_command(
|
|
||||||
program: String,
|
|
||||||
args: Option<Vec<String>>,
|
|
||||||
mut options: ProcessSpawnOptions,
|
|
||||||
) -> LuaResult<WaitForChildResult> {
|
|
||||||
let stdout = options.stdio.stdout;
|
|
||||||
let stderr = options.stdio.stderr;
|
|
||||||
let stdin = options.stdio.stdin.take();
|
|
||||||
|
|
||||||
let mut child = options
|
|
||||||
.into_command(program, args)
|
|
||||||
.stdin(match stdin.is_some() {
|
|
||||||
true => Stdio::piped(),
|
|
||||||
false => Stdio::null(),
|
|
||||||
})
|
|
||||||
.stdout(stdout.as_stdio())
|
|
||||||
.stderr(stderr.as_stdio())
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
if let Some(stdin) = stdin {
|
|
||||||
let mut child_stdin = child.stdin.take().unwrap();
|
|
||||||
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_child(child, stdout, stderr).await
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
use std::{fmt, process::Stdio, str::FromStr};
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
||||||
pub enum ProcessSpawnOptionsStdioKind {
|
|
||||||
// TODO: We need better more obvious names
|
|
||||||
// for these, but that is a breaking change
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
Forward,
|
|
||||||
Inherit,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcessSpawnOptionsStdioKind {
|
|
||||||
pub fn all() -> &'static [Self] {
|
|
||||||
&[Self::Default, Self::Forward, Self::Inherit, Self::None]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_stdio(self) -> Stdio {
|
|
||||||
match self {
|
|
||||||
Self::None => Stdio::null(),
|
|
||||||
Self::Forward => Stdio::inherit(),
|
|
||||||
_ => Stdio::piped(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ProcessSpawnOptionsStdioKind {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let s = match *self {
|
|
||||||
Self::Default => "default",
|
|
||||||
Self::Forward => "forward",
|
|
||||||
Self::Inherit => "inherit",
|
|
||||||
Self::None => "none",
|
|
||||||
};
|
|
||||||
f.write_str(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ProcessSpawnOptionsStdioKind {
|
|
||||||
type Err = LuaError;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(match s.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"default" => Self::Default,
|
|
||||||
"forward" => Self::Forward,
|
|
||||||
"inherit" => Self::Inherit,
|
|
||||||
"none" => Self::None,
|
|
||||||
_ => {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"Invalid spawn options stdio kind - got '{}', expected one of {}",
|
|
||||||
s,
|
|
||||||
ProcessSpawnOptionsStdioKind::all()
|
|
||||||
.iter()
|
|
||||||
.map(|k| format!("'{k}'"))
|
|
||||||
.join(", ")
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for ProcessSpawnOptionsStdioKind {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
match value {
|
|
||||||
LuaValue::Nil => Ok(Self::default()),
|
|
||||||
LuaValue::String(s) => s.to_str()?.parse(),
|
|
||||||
_ => Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "ProcessSpawnOptionsStdioKind",
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid spawn options stdio kind - expected string, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,177 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
env::{self},
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
|
|
||||||
use directories::UserDirs;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use tokio::process::Command;
|
|
||||||
|
|
||||||
mod kind;
|
|
||||||
mod stdio;
|
|
||||||
|
|
||||||
pub(super) use kind::*;
|
|
||||||
pub(super) use stdio::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub(super) struct ProcessSpawnOptions {
|
|
||||||
pub cwd: Option<PathBuf>,
|
|
||||||
pub envs: HashMap<String, String>,
|
|
||||||
pub shell: Option<String>,
|
|
||||||
pub stdio: ProcessSpawnOptionsStdio,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
let mut this = Self::default();
|
|
||||||
let value = match value {
|
|
||||||
LuaValue::Nil => return Ok(this),
|
|
||||||
LuaValue::Table(t) => t,
|
|
||||||
_ => {
|
|
||||||
return Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "ProcessSpawnOptions",
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid spawn options - expected table, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
If we got a working directory to use:
|
|
||||||
|
|
||||||
1. Substitute leading tilde (~) for the users home dir
|
|
||||||
2. Make sure it exists
|
|
||||||
*/
|
|
||||||
match value.get("cwd")? {
|
|
||||||
LuaValue::Nil => {}
|
|
||||||
LuaValue::String(s) => {
|
|
||||||
let mut cwd = PathBuf::from(s.to_str()?);
|
|
||||||
if let Ok(stripped) = cwd.strip_prefix("~") {
|
|
||||||
let user_dirs = UserDirs::new().ok_or_else(|| {
|
|
||||||
LuaError::runtime(
|
|
||||||
"Invalid value for option 'cwd' - failed to get home directory",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
cwd = user_dirs.home_dir().join(stripped)
|
|
||||||
}
|
|
||||||
if !cwd.exists() {
|
|
||||||
return Err(LuaError::runtime(
|
|
||||||
"Invalid value for option 'cwd' - path does not exist",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
this.cwd = Some(cwd);
|
|
||||||
}
|
|
||||||
value => {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"Invalid type for option 'cwd' - expected string, got '{}'",
|
|
||||||
value.type_name()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
If we got environment variables, make sure they are strings
|
|
||||||
*/
|
|
||||||
match value.get("env")? {
|
|
||||||
LuaValue::Nil => {}
|
|
||||||
LuaValue::Table(e) => {
|
|
||||||
for pair in e.pairs::<String, String>() {
|
|
||||||
let (k, v) = pair.context("Environment variables must be strings")?;
|
|
||||||
this.envs.insert(k, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value => {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"Invalid type for option 'env' - expected table, got '{}'",
|
|
||||||
value.type_name()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
If we got a shell to use:
|
|
||||||
|
|
||||||
1. When given as a string, use that literally
|
|
||||||
2. When set to true, use a default shell for the platform
|
|
||||||
*/
|
|
||||||
match value.get("shell")? {
|
|
||||||
LuaValue::Nil => {}
|
|
||||||
LuaValue::String(s) => this.shell = Some(s.to_string_lossy().to_string()),
|
|
||||||
LuaValue::Boolean(true) => {
|
|
||||||
this.shell = match env::consts::FAMILY {
|
|
||||||
"unix" => Some("/bin/sh".to_string()),
|
|
||||||
"windows" => Some("powershell".to_string()),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
value => {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"Invalid type for option 'shell' - expected 'true' or 'string', got '{}'",
|
|
||||||
value.type_name()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
If we got options for stdio handling, parse those as well - note that
|
|
||||||
we accept a separate "stdin" value here for compatibility with older
|
|
||||||
scripts, but the user should preferrably pass it in the stdio table
|
|
||||||
*/
|
|
||||||
this.stdio = value.get("stdio")?;
|
|
||||||
match value.get("stdin")? {
|
|
||||||
LuaValue::Nil => {}
|
|
||||||
LuaValue::String(s) => this.stdio.stdin = Some(s.as_bytes().to_vec()),
|
|
||||||
value => {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"Invalid type for option 'stdin' - expected 'string', got '{}'",
|
|
||||||
value.type_name()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcessSpawnOptions {
|
|
||||||
pub fn into_command(self, program: impl Into<String>, args: Option<Vec<String>>) -> Command {
|
|
||||||
let mut program = program.into();
|
|
||||||
|
|
||||||
// Run a shell using the command param if wanted
|
|
||||||
let pargs = match self.shell {
|
|
||||||
None => args,
|
|
||||||
Some(shell) => {
|
|
||||||
let shell_args = match args {
|
|
||||||
Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))],
|
|
||||||
None => vec!["-c".to_string(), program.to_string()],
|
|
||||||
};
|
|
||||||
program = shell.to_string();
|
|
||||||
Some(shell_args)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create command with the wanted options
|
|
||||||
let mut cmd = match pargs {
|
|
||||||
None => Command::new(program),
|
|
||||||
Some(args) => {
|
|
||||||
let mut cmd = Command::new(program);
|
|
||||||
cmd.args(args);
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set dir to run in and env variables
|
|
||||||
if let Some(cwd) = self.cwd {
|
|
||||||
cmd.current_dir(cwd);
|
|
||||||
}
|
|
||||||
if !self.envs.is_empty() {
|
|
||||||
cmd.envs(self.envs);
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use super::kind::ProcessSpawnOptionsStdioKind;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct ProcessSpawnOptionsStdio {
|
|
||||||
pub stdout: ProcessSpawnOptionsStdioKind,
|
|
||||||
pub stderr: ProcessSpawnOptionsStdioKind,
|
|
||||||
pub stdin: Option<Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ProcessSpawnOptionsStdioKind> for ProcessSpawnOptionsStdio {
|
|
||||||
fn from(value: ProcessSpawnOptionsStdioKind) -> Self {
|
|
||||||
Self {
|
|
||||||
stdout: value,
|
|
||||||
stderr: value,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for ProcessSpawnOptionsStdio {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
match value {
|
|
||||||
LuaValue::Nil => Ok(Self::default()),
|
|
||||||
LuaValue::String(s) => {
|
|
||||||
Ok(ProcessSpawnOptionsStdioKind::from_lua(LuaValue::String(s), lua)?.into())
|
|
||||||
}
|
|
||||||
LuaValue::Table(t) => {
|
|
||||||
let mut this = Self::default();
|
|
||||||
|
|
||||||
if let Some(stdin) = t.get("stdin")? {
|
|
||||||
this.stdin = stdin;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(stdout) = t.get("stdout")? {
|
|
||||||
this.stdout = stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(stderr) = t.get("stderr")? {
|
|
||||||
this.stderr = stderr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(this)
|
|
||||||
}
|
|
||||||
_ => Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "ProcessSpawnOptionsStdio",
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid spawn options stdio - expected string or table, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,74 +0,0 @@
|
||||||
use std::process::ExitStatus;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use tokio::{
|
|
||||||
io::{self, AsyncRead, AsyncReadExt},
|
|
||||||
process::Child,
|
|
||||||
task,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{options::ProcessSpawnOptionsStdioKind, tee_writer::AsyncTeeWriter};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(super) struct WaitForChildResult {
|
|
||||||
pub status: ExitStatus,
|
|
||||||
pub stdout: Vec<u8>,
|
|
||||||
pub stderr: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_with_stdio_kind<R>(
|
|
||||||
read_from: Option<R>,
|
|
||||||
kind: ProcessSpawnOptionsStdioKind,
|
|
||||||
) -> LuaResult<Vec<u8>>
|
|
||||||
where
|
|
||||||
R: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
Ok(match kind {
|
|
||||||
ProcessSpawnOptionsStdioKind::None => Vec::new(),
|
|
||||||
ProcessSpawnOptionsStdioKind::Forward => Vec::new(),
|
|
||||||
ProcessSpawnOptionsStdioKind::Default => {
|
|
||||||
let mut read_from =
|
|
||||||
read_from.expect("read_from must be Some when stdio kind is Default");
|
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
|
|
||||||
read_from.read_to_end(&mut buffer).await.into_lua_err()?;
|
|
||||||
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
ProcessSpawnOptionsStdioKind::Inherit => {
|
|
||||||
let mut read_from =
|
|
||||||
read_from.expect("read_from must be Some when stdio kind is Inherit");
|
|
||||||
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
let mut tee = AsyncTeeWriter::new(&mut stdout);
|
|
||||||
|
|
||||||
io::copy(&mut read_from, &mut tee).await.into_lua_err()?;
|
|
||||||
|
|
||||||
tee.into_vec()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn wait_for_child(
|
|
||||||
mut child: Child,
|
|
||||||
stdout_kind: ProcessSpawnOptionsStdioKind,
|
|
||||||
stderr_kind: ProcessSpawnOptionsStdioKind,
|
|
||||||
) -> LuaResult<WaitForChildResult> {
|
|
||||||
let stdout_opt = child.stdout.take();
|
|
||||||
let stderr_opt = child.stderr.take();
|
|
||||||
|
|
||||||
let stdout_task = task::spawn(read_with_stdio_kind(stdout_opt, stdout_kind));
|
|
||||||
let stderr_task = task::spawn(read_with_stdio_kind(stderr_opt, stderr_kind));
|
|
||||||
|
|
||||||
let status = child.wait().await.expect("Child process failed to start");
|
|
||||||
|
|
||||||
let stdout_buffer = stdout_task.await.into_lua_err()??;
|
|
||||||
let stderr_buffer = stderr_task.await.into_lua_err()??;
|
|
||||||
|
|
||||||
Ok(WaitForChildResult {
|
|
||||||
status,
|
|
||||||
stdout: stdout_buffer,
|
|
||||||
stderr: stderr_buffer,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use regex::{Captures, Regex};
|
|
||||||
use self_cell::self_cell;
|
|
||||||
|
|
||||||
use super::matches::LuaMatch;
|
|
||||||
|
|
||||||
type OptionalCaptures<'a> = Option<Captures<'a>>;
|
|
||||||
|
|
||||||
self_cell! {
|
|
||||||
struct LuaCapturesInner {
|
|
||||||
owner: Arc<String>,
|
|
||||||
#[covariant]
|
|
||||||
dependent: OptionalCaptures,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
A wrapper over the `regex::Captures` struct that can be used from Lua.
|
|
||||||
*/
|
|
||||||
pub struct LuaCaptures {
|
|
||||||
inner: LuaCapturesInner,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaCaptures {
|
|
||||||
/**
|
|
||||||
Create a new `LuaCaptures` instance from a `Regex` pattern and a `String` text.
|
|
||||||
|
|
||||||
Returns `Some(_)` if captures were found, `None` if no captures were found.
|
|
||||||
*/
|
|
||||||
pub fn new(pattern: &Regex, text: String) -> Option<Self> {
|
|
||||||
let inner =
|
|
||||||
LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str()));
|
|
||||||
if inner.borrow_dependent().is_some() {
|
|
||||||
Some(Self { inner })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn captures(&self) -> &Captures {
|
|
||||||
self.inner
|
|
||||||
.borrow_dependent()
|
|
||||||
.as_ref()
|
|
||||||
.expect("None captures should not be used")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn num_captures(&self) -> usize {
|
|
||||||
// NOTE: Here we exclude the match for the entire regex
|
|
||||||
// pattern, only counting the named and numbered captures
|
|
||||||
self.captures().len() - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fn text(&self) -> Arc<String> {
|
|
||||||
Arc::clone(self.inner.borrow_owner())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for LuaCaptures {
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_method("get", |_, this, index: usize| {
|
|
||||||
Ok(this
|
|
||||||
.captures()
|
|
||||||
.get(index)
|
|
||||||
.map(|m| LuaMatch::new(this.text(), m)))
|
|
||||||
});
|
|
||||||
|
|
||||||
methods.add_method("group", |_, this, group: String| {
|
|
||||||
Ok(this
|
|
||||||
.captures()
|
|
||||||
.name(&group)
|
|
||||||
.map(|m| LuaMatch::new(this.text(), m)))
|
|
||||||
});
|
|
||||||
|
|
||||||
methods.add_method("format", |_, this, format: String| {
|
|
||||||
let mut new = String::new();
|
|
||||||
this.captures().expand(&format, &mut new);
|
|
||||||
Ok(new)
|
|
||||||
});
|
|
||||||
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures()));
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
|
||||||
Ok(format!("RegexCaptures({})", this.num_captures()))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
use std::{ops::Range, sync::Arc};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use regex::Match;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A wrapper over the `regex::Match` struct that can be used from Lua.
|
|
||||||
*/
|
|
||||||
pub struct LuaMatch {
|
|
||||||
text: Arc<String>,
|
|
||||||
start: usize,
|
|
||||||
end: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaMatch {
|
|
||||||
/**
|
|
||||||
Create a new `LuaMatch` instance from a `String` text and a `regex::Match`.
|
|
||||||
*/
|
|
||||||
pub fn new(text: Arc<String>, matched: Match) -> Self {
|
|
||||||
Self {
|
|
||||||
text,
|
|
||||||
start: matched.start(),
|
|
||||||
end: matched.end(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn range(&self) -> Range<usize> {
|
|
||||||
self.start..self.end
|
|
||||||
}
|
|
||||||
|
|
||||||
fn slice(&self) -> &str {
|
|
||||||
&self.text[self.range()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for LuaMatch {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
// NOTE: Strings are 0 based in Rust but 1 based in Luau, and end of range in Rust is exclusive
|
|
||||||
fields.add_field_method_get("start", |_, this| Ok(this.start.saturating_add(1)));
|
|
||||||
fields.add_field_method_get("finish", |_, this| Ok(this.end));
|
|
||||||
fields.add_field_method_get("len", |_, this| Ok(this.range().len()));
|
|
||||||
fields.add_field_method_get("text", |_, this| Ok(this.slice().to_string()));
|
|
||||||
|
|
||||||
fields.add_meta_field(LuaMetaMethod::Type, "RegexMatch");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len()));
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
|
||||||
Ok(format!("RegexMatch({})", this.slice()))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
#![allow(clippy::module_inception)]
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
mod captures;
|
|
||||||
mod matches;
|
|
||||||
mod regex;
|
|
||||||
|
|
||||||
use self::regex::LuaRegex;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", new_regex)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_regex(_: &Lua, pattern: String) -> LuaResult<LuaRegex> {
|
|
||||||
LuaRegex::new(pattern)
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use super::{captures::LuaCaptures, matches::LuaMatch};
|
|
||||||
|
|
||||||
/**
|
|
||||||
A wrapper over the `regex::Regex` struct that can be used from Lua.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LuaRegex {
|
|
||||||
inner: Regex,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaRegex {
|
|
||||||
/**
|
|
||||||
Create a new `LuaRegex` instance from a `String` pattern.
|
|
||||||
*/
|
|
||||||
pub fn new(pattern: String) -> LuaResult<Self> {
|
|
||||||
Regex::new(&pattern)
|
|
||||||
.map(|inner| Self { inner })
|
|
||||||
.map_err(LuaError::external)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for LuaRegex {
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_method("isMatch", |_, this, text: String| {
|
|
||||||
Ok(this.inner.is_match(&text))
|
|
||||||
});
|
|
||||||
|
|
||||||
methods.add_method("find", |_, this, text: String| {
|
|
||||||
let arc = Arc::new(text);
|
|
||||||
Ok(this
|
|
||||||
.inner
|
|
||||||
.find(&arc)
|
|
||||||
.map(|m| LuaMatch::new(Arc::clone(&arc), m)))
|
|
||||||
});
|
|
||||||
|
|
||||||
methods.add_method("captures", |_, this, text: String| {
|
|
||||||
Ok(LuaCaptures::new(&this.inner, text))
|
|
||||||
});
|
|
||||||
|
|
||||||
methods.add_method("split", |_, this, text: String| {
|
|
||||||
Ok(this
|
|
||||||
.inner
|
|
||||||
.split(&text)
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect::<Vec<_>>())
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Determine whether it's desirable and / or feasible to support
|
|
||||||
// using a function or table for `replace` like in the lua string library
|
|
||||||
methods.add_method(
|
|
||||||
"replace",
|
|
||||||
|_, this, (haystack, replacer): (String, String)| {
|
|
||||||
Ok(this.inner.replace(&haystack, replacer).to_string())
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method(
|
|
||||||
"replaceAll",
|
|
||||||
|_, this, (haystack, replacer): (String, String)| {
|
|
||||||
Ok(this.inner.replace_all(&haystack, replacer).to_string())
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
|
|
||||||
Ok(format!("Regex({})", this.inner.as_str()))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_meta_field(LuaMetaMethod::Type, "Regex");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua_luau_scheduler::LuaSpawnExt;
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
lune::util::TableBuilder,
|
|
||||||
roblox::{
|
|
||||||
self,
|
|
||||||
document::{Document, DocumentError, DocumentFormat, DocumentKind},
|
|
||||||
instance::{registry::InstanceRegistry, Instance},
|
|
||||||
reflection::Database as ReflectionDatabase,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
|
|
||||||
|
|
||||||
pub fn create(lua: &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)?
|
|
||||||
.with_function("implementProperty", implement_property)?
|
|
||||||
.with_function("implementMethod", implement_method)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn deserialize_place<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
contents: LuaString<'lua>,
|
|
||||||
) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
let bytes = contents.as_bytes().to_vec();
|
|
||||||
let fut = lua.spawn_blocking(move || {
|
|
||||||
let doc = Document::from_bytes(bytes, DocumentKind::Place)?;
|
|
||||||
let data_model = doc.into_data_model_instance()?;
|
|
||||||
Ok::<_, DocumentError>(data_model)
|
|
||||||
});
|
|
||||||
fut.await.into_lua_err()?.into_lua(lua)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn deserialize_model<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
contents: LuaString<'lua>,
|
|
||||||
) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
let bytes = contents.as_bytes().to_vec();
|
|
||||||
let fut = lua.spawn_blocking(move || {
|
|
||||||
let doc = Document::from_bytes(bytes, DocumentKind::Model)?;
|
|
||||||
let instance_array = doc.into_instance_array()?;
|
|
||||||
Ok::<_, DocumentError>(instance_array)
|
|
||||||
});
|
|
||||||
fut.await.into_lua_err()?.into_lua(lua)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serialize_place<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>),
|
|
||||||
) -> LuaResult<LuaString<'lua>> {
|
|
||||||
let data_model = (*data_model).clone();
|
|
||||||
let fut = lua.spawn_blocking(move || {
|
|
||||||
let doc = Document::from_data_model_instance(data_model)?;
|
|
||||||
let bytes = doc.to_bytes_with_format(match as_xml {
|
|
||||||
Some(true) => DocumentFormat::Xml,
|
|
||||||
_ => DocumentFormat::Binary,
|
|
||||||
})?;
|
|
||||||
Ok::<_, DocumentError>(bytes)
|
|
||||||
});
|
|
||||||
let bytes = fut.await.into_lua_err()?;
|
|
||||||
lua.create_string(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serialize_model<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>),
|
|
||||||
) -> LuaResult<LuaString<'lua>> {
|
|
||||||
let instances = instances.iter().map(|i| (*i).clone()).collect();
|
|
||||||
let fut = lua.spawn_blocking(move || {
|
|
||||||
let doc = Document::from_instance_array(instances)?;
|
|
||||||
let bytes = doc.to_bytes_with_format(match as_xml {
|
|
||||||
Some(true) => DocumentFormat::Xml,
|
|
||||||
_ => DocumentFormat::Binary,
|
|
||||||
})?;
|
|
||||||
Ok::<_, DocumentError>(bytes)
|
|
||||||
});
|
|
||||||
let bytes = fut.await.into_lua_err()?;
|
|
||||||
lua.create_string(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
|
|
||||||
if matches!(raw, Some(true)) {
|
|
||||||
Ok(rbx_cookie::get_value())
|
|
||||||
} else {
|
|
||||||
Ok(rbx_cookie::get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
|
|
||||||
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn implement_property(
|
|
||||||
lua: &Lua,
|
|
||||||
(class_name, property_name, property_getter, property_setter): (
|
|
||||||
String,
|
|
||||||
String,
|
|
||||||
LuaFunction,
|
|
||||||
Option<LuaFunction>,
|
|
||||||
),
|
|
||||||
) -> LuaResult<()> {
|
|
||||||
let property_setter = match property_setter {
|
|
||||||
Some(setter) => setter,
|
|
||||||
None => {
|
|
||||||
let property_name = property_name.clone();
|
|
||||||
lua.create_function(move |_, _: LuaMultiValue| {
|
|
||||||
Err::<(), _>(LuaError::runtime(format!(
|
|
||||||
"Property '{property_name}' is read-only"
|
|
||||||
)))
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
InstanceRegistry::insert_property_getter(lua, &class_name, &property_name, property_getter)
|
|
||||||
.into_lua_err()?;
|
|
||||||
InstanceRegistry::insert_property_setter(lua, &class_name, &property_name, property_setter)
|
|
||||||
.into_lua_err()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn implement_method(
|
|
||||||
lua: &Lua,
|
|
||||||
(class_name, method_name, method): (String, String, LuaFunction),
|
|
||||||
) -> LuaResult<()> {
|
|
||||||
InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use lz4_flex::{compress_prepend_size, decompress_size_prepended};
|
|
||||||
use tokio::io::{copy, BufReader};
|
|
||||||
|
|
||||||
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 Ok(blocking::unblock(move || compress_prepend_size(&source)).await);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 blocking::unblock(move || decompress_size_prepended(&source))
|
|
||||||
.await
|
|
||||||
.into_lua_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
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,131 +0,0 @@
|
||||||
use bstr::{BString, ByteSlice};
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use serde_yaml::Value as YamlValue;
|
|
||||||
use toml::Value as TomlValue;
|
|
||||||
|
|
||||||
const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new()
|
|
||||||
.set_array_metatable(false)
|
|
||||||
.serialize_none_to_null(false)
|
|
||||||
.serialize_unit_to_null(false);
|
|
||||||
|
|
||||||
const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new()
|
|
||||||
.sort_keys(true)
|
|
||||||
.deny_recursive_tables(false)
|
|
||||||
.deny_unsupported_types(true);
|
|
||||||
|
|
||||||
#[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 => {
|
|
||||||
let serialized: JsonValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
|
|
||||||
if self.pretty {
|
|
||||||
serde_json::to_vec_pretty(&serialized).into_lua_err()?
|
|
||||||
} else {
|
|
||||||
serde_json::to_vec(&serialized).into_lua_err()?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EncodeDecodeFormat::Yaml => {
|
|
||||||
let serialized: YamlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
|
|
||||||
let mut writer = Vec::with_capacity(128);
|
|
||||||
serde_yaml::to_writer(&mut writer, &serialized).into_lua_err()?;
|
|
||||||
writer
|
|
||||||
}
|
|
||||||
EncodeDecodeFormat::Toml => {
|
|
||||||
let serialized: TomlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
|
|
||||||
let s = if self.pretty {
|
|
||||||
toml::to_string_pretty(&serialized).into_lua_err()?
|
|
||||||
} else {
|
|
||||||
toml::to_string(&serialized).into_lua_err()?
|
|
||||||
};
|
|
||||||
s.as_bytes().to_vec()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
lua.create_string(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize_from_string(self, lua: &Lua, string: BString) -> LuaResult<LuaValue> {
|
|
||||||
let bytes = string.as_bytes();
|
|
||||||
match self.format {
|
|
||||||
EncodeDecodeFormat::Json => {
|
|
||||||
let value: JsonValue = serde_json::from_slice(bytes).into_lua_err()?;
|
|
||||||
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
|
|
||||||
}
|
|
||||||
EncodeDecodeFormat::Yaml => {
|
|
||||||
let value: YamlValue = serde_yaml::from_slice(bytes).into_lua_err()?;
|
|
||||||
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
|
|
||||||
}
|
|
||||||
EncodeDecodeFormat::Toml => {
|
|
||||||
if let Ok(s) = string.to_str() {
|
|
||||||
let value: TomlValue = toml::from_str(s).into_lua_err()?;
|
|
||||||
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"TOML must be valid utf-8".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,48 +0,0 @@
|
||||||
use bstr::BString;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
pub(super) mod compress_decompress;
|
|
||||||
pub(super) mod encode_decode;
|
|
||||||
|
|
||||||
use compress_decompress::{compress, decompress, CompressDecompressFormat};
|
|
||||||
use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat};
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("encode", serde_encode)?
|
|
||||||
.with_function("decode", serde_decode)?
|
|
||||||
.with_async_function("compress", serde_compress)?
|
|
||||||
.with_async_function("decompress", serde_decompress)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serde_encode<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(format, val, pretty): (EncodeDecodeFormat, LuaValue<'lua>, Option<bool>),
|
|
||||||
) -> LuaResult<LuaString<'lua>> {
|
|
||||||
let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default()));
|
|
||||||
config.serialize_to_string(lua, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serde_decode(lua: &Lua, (format, str): (EncodeDecodeFormat, BString)) -> LuaResult<LuaValue> {
|
|
||||||
let config = EncodeDecodeConfig::from(format);
|
|
||||||
config.deserialize_from_string(lua, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serde_compress(
|
|
||||||
lua: &Lua,
|
|
||||||
(format, str): (CompressDecompressFormat, BString),
|
|
||||||
) -> LuaResult<LuaString> {
|
|
||||||
let bytes = compress(format, str).await?;
|
|
||||||
lua.create_string(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serde_decompress(
|
|
||||||
lua: &Lua,
|
|
||||||
(format, str): (CompressDecompressFormat, BString),
|
|
||||||
) -> LuaResult<LuaString> {
|
|
||||||
let bytes = decompress(format, str).await?;
|
|
||||||
lua.create_string(bytes)
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
|
|
||||||
use mlua_luau_scheduler::LuaSpawnExt;
|
|
||||||
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
|
|
||||||
|
|
||||||
use crate::lune::util::{
|
|
||||||
formatting::{
|
|
||||||
format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str,
|
|
||||||
},
|
|
||||||
TableBuilder,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod prompt;
|
|
||||||
use prompt::{PromptKind, PromptOptions, PromptResult};
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable<'_>> {
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("color", stdio_color)?
|
|
||||||
.with_function("style", stdio_style)?
|
|
||||||
.with_function("format", stdio_format)?
|
|
||||||
.with_async_function("write", stdio_write)?
|
|
||||||
.with_async_function("ewrite", stdio_ewrite)?
|
|
||||||
.with_async_function("readToEnd", stdio_read_to_end)?
|
|
||||||
.with_async_function("prompt", stdio_prompt)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stdio_color(_: &Lua, color: String) -> LuaResult<String> {
|
|
||||||
let ansi_string = format_style(style_from_color_str(&color)?);
|
|
||||||
Ok(ansi_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stdio_style(_: &Lua, color: String) -> LuaResult<String> {
|
|
||||||
let ansi_string = format_style(style_from_style_str(&color)?);
|
|
||||||
Ok(ansi_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult<String> {
|
|
||||||
pretty_format_multi_value(&args)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
stdout.write_all(s.as_bytes()).await?;
|
|
||||||
stdout.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
|
|
||||||
let mut stderr = io::stderr();
|
|
||||||
stderr.write_all(s.as_bytes()).await?;
|
|
||||||
stderr.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader.
|
|
||||||
|
|
||||||
This is a bit tricky since we would want to be able to use **both** readLine and readToEnd
|
|
||||||
in the same script, doing something like readLine, readLine, readToEnd from lua, and
|
|
||||||
having that capture the first two lines and then read the rest of the input.
|
|
||||||
*/
|
|
||||||
|
|
||||||
async fn stdio_read_to_end(lua: &Lua, _: ()) -> LuaResult<LuaString> {
|
|
||||||
let mut input = Vec::new();
|
|
||||||
let mut stdin = io::stdin();
|
|
||||||
stdin.read_to_end(&mut input).await?;
|
|
||||||
lua.create_string(&input)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult<PromptResult> {
|
|
||||||
lua.spawn_blocking(move || prompt(options))
|
|
||||||
.await
|
|
||||||
.into_lua_err()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
|
|
||||||
let theme = ColorfulTheme::default();
|
|
||||||
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()
|
|
||||||
.into_lua_err()?;
|
|
||||||
Ok(PromptResult::String(input))
|
|
||||||
}
|
|
||||||
PromptKind::Confirm => {
|
|
||||||
let mut prompt = Confirm::with_theme(&theme);
|
|
||||||
if let Some(b) = options.default_bool {
|
|
||||||
prompt = prompt.default(b);
|
|
||||||
};
|
|
||||||
let result = prompt
|
|
||||||
.with_prompt(&options.text.expect("Missing text in prompt options"))
|
|
||||||
.interact()
|
|
||||||
.into_lua_err()?;
|
|
||||||
Ok(PromptResult::Boolean(result))
|
|
||||||
}
|
|
||||||
PromptKind::Select => {
|
|
||||||
let chosen = Select::with_theme(&theme)
|
|
||||||
.with_prompt(&options.text.unwrap_or_default())
|
|
||||||
.items(&options.options.expect("Missing options in prompt options"))
|
|
||||||
.interact_opt()
|
|
||||||
.into_lua_err()?;
|
|
||||||
Ok(match chosen {
|
|
||||||
Some(idx) => PromptResult::Index(idx + 1),
|
|
||||||
None => PromptResult::None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
PromptKind::MultiSelect => {
|
|
||||||
let chosen = MultiSelect::with_theme(&theme)
|
|
||||||
.with_prompt(&options.text.unwrap_or_default())
|
|
||||||
.items(&options.options.expect("Missing options in prompt options"))
|
|
||||||
.interact_opt()
|
|
||||||
.into_lua_err()?;
|
|
||||||
Ok(match chosen {
|
|
||||||
None => PromptResult::None,
|
|
||||||
Some(indices) => {
|
|
||||||
PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,57 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use mlua_luau_scheduler::Functions;
|
|
||||||
use tokio::time::{self, Instant};
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
const DELAY_IMPL_LUA: &str = r#"
|
|
||||||
return defer(function(...)
|
|
||||||
wait(select(1, ...))
|
|
||||||
spawn(select(2, ...))
|
|
||||||
end, ...)
|
|
||||||
"#;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<LuaTable<'_>> {
|
|
||||||
let fns = Functions::new(lua)?;
|
|
||||||
|
|
||||||
// Create wait & delay functions
|
|
||||||
let task_wait = lua.create_async_function(wait)?;
|
|
||||||
let task_delay_env = TableBuilder::new(lua)?
|
|
||||||
.with_value("select", lua.globals().get::<_, LuaFunction>("select")?)?
|
|
||||||
.with_value("spawn", fns.spawn.clone())?
|
|
||||||
.with_value("defer", fns.defer.clone())?
|
|
||||||
.with_value("wait", task_wait.clone())?
|
|
||||||
.build_readonly()?;
|
|
||||||
let task_delay = lua
|
|
||||||
.load(DELAY_IMPL_LUA)
|
|
||||||
.set_name("task.delay")
|
|
||||||
.set_environment(task_delay_env)
|
|
||||||
.into_function()?;
|
|
||||||
|
|
||||||
// Overwrite resume & wrap functions on the coroutine global
|
|
||||||
// with ones that are compatible with our scheduler
|
|
||||||
let co = lua.globals().get::<_, LuaTable>("coroutine")?;
|
|
||||||
co.set("resume", fns.resume.clone())?;
|
|
||||||
co.set("wrap", fns.wrap.clone())?;
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_value("cancel", fns.cancel)?
|
|
||||||
.with_value("defer", fns.defer)?
|
|
||||||
.with_value("delay", task_delay)?
|
|
||||||
.with_value("spawn", fns.spawn)?
|
|
||||||
.with_value("wait", task_wait)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait(_: &Lua, secs: Option<f64>) -> LuaResult<f64> {
|
|
||||||
let duration = Duration::from_secs_f64(secs.unwrap_or_default());
|
|
||||||
|
|
||||||
let before = Instant::now();
|
|
||||||
time::sleep(duration).await;
|
|
||||||
let after = Instant::now();
|
|
||||||
|
|
||||||
Ok((after - before).as_secs_f64())
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
|
|
||||||
lua.create_table()
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use super::util::TableBuilder;
|
|
||||||
|
|
||||||
mod g_table;
|
|
||||||
mod print;
|
|
||||||
mod require;
|
|
||||||
mod version;
|
|
||||||
mod warn;
|
|
||||||
|
|
||||||
pub fn inject_all(lua: &Lua) -> LuaResult<()> {
|
|
||||||
let all = TableBuilder::new(lua)?
|
|
||||||
.with_value("_G", g_table::create(lua)?)?
|
|
||||||
.with_value("_VERSION", version::create(lua)?)?
|
|
||||||
.with_value("print", print::create(lua)?)?
|
|
||||||
.with_value("require", require::create(lua)?)?
|
|
||||||
.with_value("warn", warn::create(lua)?)?
|
|
||||||
.build_readonly()?;
|
|
||||||
|
|
||||||
for res in all.pairs() {
|
|
||||||
let (key, value): (LuaValue, LuaValue) = res.unwrap();
|
|
||||||
lua.globals().set(key, value)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
use std::io::Write as _;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::formatting::pretty_format_multi_value;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
|
|
||||||
lua.create_function(|_, args: LuaMultiValue| {
|
|
||||||
let formatted = format!("{}\n", pretty_format_multi_value(&args)?);
|
|
||||||
let mut stdout = std::io::stdout();
|
|
||||||
stdout.write_all(formatted.as_bytes())?;
|
|
||||||
stdout.flush()?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
use console::style;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::{
|
|
||||||
luaurc::LuauRc,
|
|
||||||
paths::{make_absolute_and_clean, CWD},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::context::*;
|
|
||||||
|
|
||||||
pub(super) async fn require<'lua, 'ctx>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
ctx: &'ctx RequireContext,
|
|
||||||
source: &str,
|
|
||||||
alias: &str,
|
|
||||||
path: &str,
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>>
|
|
||||||
where
|
|
||||||
'lua: 'ctx,
|
|
||||||
{
|
|
||||||
let alias = alias.to_ascii_lowercase();
|
|
||||||
|
|
||||||
let parent = make_absolute_and_clean(source)
|
|
||||||
.parent()
|
|
||||||
.expect("how did a root path end up here..")
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
// Try to gather the first luaurc and / or error we
|
|
||||||
// encounter to display better error messages to users
|
|
||||||
let mut first_luaurc = None;
|
|
||||||
let mut first_error = None;
|
|
||||||
let predicate = |rc: &LuauRc| {
|
|
||||||
if first_luaurc.is_none() {
|
|
||||||
first_luaurc.replace(rc.clone());
|
|
||||||
}
|
|
||||||
if let Err(e) = rc.validate() {
|
|
||||||
if first_error.is_none() {
|
|
||||||
first_error.replace(e);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
rc.find_alias(&alias).is_some()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to find a luaurc that contains the alias we're searching for
|
|
||||||
let luaurc = LuauRc::read_recursive(parent, predicate)
|
|
||||||
.await
|
|
||||||
.ok_or_else(|| {
|
|
||||||
if let Some(error) = first_error {
|
|
||||||
LuaError::runtime(format!("error while parsing .luaurc file: {error}"))
|
|
||||||
} else if let Some(luaurc) = first_luaurc {
|
|
||||||
LuaError::runtime(format!(
|
|
||||||
"failed to find alias '{alias}' - known aliases:\n{}",
|
|
||||||
luaurc
|
|
||||||
.aliases()
|
|
||||||
.iter()
|
|
||||||
.map(|(name, path)| format!(" {name} {} {path}", style(">").dim()))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
LuaError::runtime(format!("failed to find alias '{alias}' (no .luaurc)"))
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// We now have our aliased path, our path require function just needs it
|
|
||||||
// in a slightly different format with both absolute + relative to cwd
|
|
||||||
let abs_path = luaurc.find_alias(&alias).unwrap().join(path);
|
|
||||||
let rel_path = pathdiff::diff_paths(&abs_path, CWD.as_path()).ok_or_else(|| {
|
|
||||||
LuaError::runtime(format!("failed to find relative path for alias '{alias}'"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
super::path::require_abs_rel(lua, ctx, abs_path, rel_path).await
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use super::context::*;
|
|
||||||
|
|
||||||
pub(super) async fn require<'lua, 'ctx>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
ctx: &'ctx RequireContext,
|
|
||||||
name: &str,
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>>
|
|
||||||
where
|
|
||||||
'lua: 'ctx,
|
|
||||||
{
|
|
||||||
ctx.load_builtin(lua, name)
|
|
||||||
}
|
|
|
@ -1,291 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua_luau_scheduler::LuaSchedulerExt;
|
|
||||||
use tokio::{
|
|
||||||
fs,
|
|
||||||
sync::{
|
|
||||||
broadcast::{self, Sender},
|
|
||||||
Mutex as AsyncMutex,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::lune::{builtins::LuneBuiltin, util::paths::CWD};
|
|
||||||
|
|
||||||
/**
|
|
||||||
Context containing cached results for all `require` operations.
|
|
||||||
|
|
||||||
The cache uses absolute paths, so any given relative
|
|
||||||
path will first be transformed into an absolute path.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(super) struct RequireContext {
|
|
||||||
cache_builtins: Arc<AsyncMutex<HashMap<LuneBuiltin, LuaResult<LuaRegistryKey>>>>,
|
|
||||||
cache_results: Arc<AsyncMutex<HashMap<PathBuf, LuaResult<LuaRegistryKey>>>>,
|
|
||||||
cache_pending: Arc<AsyncMutex<HashMap<PathBuf, Sender<()>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequireContext {
|
|
||||||
/**
|
|
||||||
Creates a new require context for the given [`Lua`] struct.
|
|
||||||
|
|
||||||
Note that this require context is global and only one require
|
|
||||||
context should be created per [`Lua`] struct, creating more
|
|
||||||
than one context may lead to undefined require-behavior.
|
|
||||||
*/
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
cache_builtins: Arc::new(AsyncMutex::new(HashMap::new())),
|
|
||||||
cache_results: Arc::new(AsyncMutex::new(HashMap::new())),
|
|
||||||
cache_pending: Arc::new(AsyncMutex::new(HashMap::new())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Resolves the given `source` and `path` into require paths
|
|
||||||
to use, based on the current require context settings.
|
|
||||||
|
|
||||||
This will resolve path segments such as `./`, `../`, ..., and
|
|
||||||
if the resolved path is not an absolute path, will create an
|
|
||||||
absolute path by prepending the current working directory.
|
|
||||||
*/
|
|
||||||
pub fn resolve_paths(
|
|
||||||
&self,
|
|
||||||
source: impl AsRef<str>,
|
|
||||||
path: impl AsRef<str>,
|
|
||||||
) -> LuaResult<(PathBuf, PathBuf)> {
|
|
||||||
let path = PathBuf::from(source.as_ref())
|
|
||||||
.parent()
|
|
||||||
.ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))?
|
|
||||||
.join(path.as_ref());
|
|
||||||
|
|
||||||
let rel_path = path_clean::clean(path);
|
|
||||||
let abs_path = if rel_path.is_absolute() {
|
|
||||||
rel_path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
CWD.join(&rel_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((abs_path, rel_path))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Checks if the given path has a cached require result.
|
|
||||||
*/
|
|
||||||
pub fn is_cached(&self, abs_path: impl AsRef<Path>) -> LuaResult<bool> {
|
|
||||||
let is_cached = self
|
|
||||||
.cache_results
|
|
||||||
.try_lock()
|
|
||||||
.expect("RequireContext may not be used from multiple threads")
|
|
||||||
.contains_key(abs_path.as_ref());
|
|
||||||
Ok(is_cached)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Checks if the given path is currently being used in `require`.
|
|
||||||
*/
|
|
||||||
pub fn is_pending(&self, abs_path: impl AsRef<Path>) -> LuaResult<bool> {
|
|
||||||
let is_pending = self
|
|
||||||
.cache_pending
|
|
||||||
.try_lock()
|
|
||||||
.expect("RequireContext may not be used from multiple threads")
|
|
||||||
.contains_key(abs_path.as_ref());
|
|
||||||
Ok(is_pending)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets the resulting value from the require cache.
|
|
||||||
|
|
||||||
Will panic if the path has not been cached, use [`is_cached`] first.
|
|
||||||
*/
|
|
||||||
pub fn get_from_cache<'lua>(
|
|
||||||
&self,
|
|
||||||
lua: &'lua Lua,
|
|
||||||
abs_path: impl AsRef<Path>,
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>> {
|
|
||||||
let results = self
|
|
||||||
.cache_results
|
|
||||||
.try_lock()
|
|
||||||
.expect("RequireContext may not be used from multiple threads");
|
|
||||||
|
|
||||||
let cached = results
|
|
||||||
.get(abs_path.as_ref())
|
|
||||||
.expect("Path does not exist in results cache");
|
|
||||||
match cached {
|
|
||||||
Err(e) => Err(e.clone()),
|
|
||||||
Ok(k) => {
|
|
||||||
let multi_vec = lua
|
|
||||||
.registry_value::<Vec<LuaValue>>(k)
|
|
||||||
.expect("Missing require result in lua registry");
|
|
||||||
Ok(LuaMultiValue::from_vec(multi_vec))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Waits for the resulting value from the require cache.
|
|
||||||
|
|
||||||
Will panic if the path has not been cached, use [`is_cached`] first.
|
|
||||||
*/
|
|
||||||
pub async fn wait_for_cache<'lua>(
|
|
||||||
&self,
|
|
||||||
lua: &'lua Lua,
|
|
||||||
abs_path: impl AsRef<Path>,
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>> {
|
|
||||||
let mut thread_recv = {
|
|
||||||
let pending = self
|
|
||||||
.cache_pending
|
|
||||||
.try_lock()
|
|
||||||
.expect("RequireContext may not be used from multiple threads");
|
|
||||||
let thread_id = pending
|
|
||||||
.get(abs_path.as_ref())
|
|
||||||
.expect("Path is not currently pending require");
|
|
||||||
thread_id.subscribe()
|
|
||||||
};
|
|
||||||
|
|
||||||
thread_recv.recv().await.into_lua_err()?;
|
|
||||||
|
|
||||||
self.get_from_cache(lua, abs_path.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load<'lua>(
|
|
||||||
&self,
|
|
||||||
lua: &'lua Lua,
|
|
||||||
abs_path: impl AsRef<Path>,
|
|
||||||
rel_path: impl AsRef<Path>,
|
|
||||||
) -> LuaResult<LuaRegistryKey> {
|
|
||||||
let abs_path = abs_path.as_ref();
|
|
||||||
let rel_path = rel_path.as_ref();
|
|
||||||
|
|
||||||
// Read the file at the given path, try to parse and
|
|
||||||
// load it into a new lua thread that we can schedule
|
|
||||||
let file_contents = fs::read(&abs_path).await?;
|
|
||||||
let file_thread = lua
|
|
||||||
.load(file_contents)
|
|
||||||
.set_name(rel_path.to_string_lossy().to_string());
|
|
||||||
|
|
||||||
// Schedule the thread to run, wait for it to finish running
|
|
||||||
let thread_id = lua.push_thread_back(file_thread, ())?;
|
|
||||||
lua.track_thread(thread_id);
|
|
||||||
lua.wait_for_thread(thread_id).await;
|
|
||||||
let thread_res = lua.get_thread_result(thread_id).unwrap();
|
|
||||||
|
|
||||||
// Return the result of the thread, storing any lua value(s) in the registry
|
|
||||||
match thread_res {
|
|
||||||
Err(e) => Err(e),
|
|
||||||
Ok(v) => {
|
|
||||||
let multi_vec = v.into_vec();
|
|
||||||
let multi_key = lua
|
|
||||||
.create_registry_value(multi_vec)
|
|
||||||
.expect("Failed to store require result in registry - out of memory");
|
|
||||||
Ok(multi_key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Loads (requires) the file at the given path.
|
|
||||||
*/
|
|
||||||
pub async fn load_with_caching<'lua>(
|
|
||||||
&self,
|
|
||||||
lua: &'lua Lua,
|
|
||||||
abs_path: impl AsRef<Path>,
|
|
||||||
rel_path: impl AsRef<Path>,
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>> {
|
|
||||||
let abs_path = abs_path.as_ref();
|
|
||||||
let rel_path = rel_path.as_ref();
|
|
||||||
|
|
||||||
// Set this abs path as currently pending
|
|
||||||
let (broadcast_tx, _) = broadcast::channel(1);
|
|
||||||
self.cache_pending
|
|
||||||
.try_lock()
|
|
||||||
.expect("RequireContext may not be used from multiple threads")
|
|
||||||
.insert(abs_path.to_path_buf(), broadcast_tx);
|
|
||||||
|
|
||||||
// Try to load at this abs path
|
|
||||||
let load_res = self.load(lua, abs_path, rel_path).await;
|
|
||||||
let load_val = match &load_res {
|
|
||||||
Err(e) => Err(e.clone()),
|
|
||||||
Ok(k) => {
|
|
||||||
let multi_vec = lua
|
|
||||||
.registry_value::<Vec<LuaValue>>(k)
|
|
||||||
.expect("Failed to fetch require result from registry");
|
|
||||||
Ok(LuaMultiValue::from_vec(multi_vec))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// NOTE: We use the async lock and not try_lock here because
|
|
||||||
// some other thread may be wanting to insert into the require
|
|
||||||
// cache at the same time, and that's not an actual error case
|
|
||||||
self.cache_results
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.insert(abs_path.to_path_buf(), load_res);
|
|
||||||
|
|
||||||
// Remove the pending thread id from the require context,
|
|
||||||
// broadcast a message to let any listeners know that this
|
|
||||||
// path has now finished the require process and is cached
|
|
||||||
let broadcast_tx = self
|
|
||||||
.cache_pending
|
|
||||||
.try_lock()
|
|
||||||
.expect("RequireContext may not be used from multiple threads")
|
|
||||||
.remove(abs_path)
|
|
||||||
.expect("Pending require broadcaster was unexpectedly removed");
|
|
||||||
broadcast_tx.send(()).ok();
|
|
||||||
|
|
||||||
load_val
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Loads (requires) the builtin with the given name.
|
|
||||||
*/
|
|
||||||
pub fn load_builtin<'lua>(
|
|
||||||
&self,
|
|
||||||
lua: &'lua Lua,
|
|
||||||
name: impl AsRef<str>,
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>> {
|
|
||||||
let builtin: LuneBuiltin = match name.as_ref().parse() {
|
|
||||||
Err(e) => return Err(LuaError::runtime(e)),
|
|
||||||
Ok(b) => b,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cache = self
|
|
||||||
.cache_builtins
|
|
||||||
.try_lock()
|
|
||||||
.expect("RequireContext may not be used from multiple threads");
|
|
||||||
|
|
||||||
if let Some(res) = cache.get(&builtin) {
|
|
||||||
return match res {
|
|
||||||
Err(e) => return Err(e.clone()),
|
|
||||||
Ok(key) => {
|
|
||||||
let multi_vec = lua
|
|
||||||
.registry_value::<Vec<LuaValue>>(key)
|
|
||||||
.expect("Missing builtin result in lua registry");
|
|
||||||
Ok(LuaMultiValue::from_vec(multi_vec))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = builtin.create(lua);
|
|
||||||
|
|
||||||
cache.insert(
|
|
||||||
builtin,
|
|
||||||
match result.clone() {
|
|
||||||
Err(e) => Err(e),
|
|
||||||
Ok(multi) => {
|
|
||||||
let multi_vec = multi.into_vec();
|
|
||||||
let multi_key = lua
|
|
||||||
.create_registry_value(multi_vec)
|
|
||||||
.expect("Failed to store require result in registry - out of memory");
|
|
||||||
Ok(multi_key)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::TableBuilder;
|
|
||||||
|
|
||||||
mod context;
|
|
||||||
use context::RequireContext;
|
|
||||||
|
|
||||||
mod alias;
|
|
||||||
mod builtin;
|
|
||||||
mod path;
|
|
||||||
|
|
||||||
const REQUIRE_IMPL: &str = r#"
|
|
||||||
return require(source(), ...)
|
|
||||||
"#;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
|
|
||||||
lua.set_app_data(RequireContext::new());
|
|
||||||
|
|
||||||
/*
|
|
||||||
Require implementation needs a few workarounds:
|
|
||||||
|
|
||||||
- Async functions run outside of the lua resumption cycle,
|
|
||||||
so the current lua thread, as well as its stack/debug info
|
|
||||||
is not available, meaning we have to use a normal function
|
|
||||||
|
|
||||||
- Using the async require function directly in another lua function
|
|
||||||
would mean yielding across the metamethod/c-call boundary, meaning
|
|
||||||
we have to first load our two functions into a normal lua chunk
|
|
||||||
and then load that new chunk into our final require function
|
|
||||||
|
|
||||||
Also note that we inspect the stack at level 2:
|
|
||||||
|
|
||||||
1. The current c / rust function
|
|
||||||
2. The wrapper lua chunk defined above
|
|
||||||
3. The lua chunk we are require-ing from
|
|
||||||
*/
|
|
||||||
|
|
||||||
let require_fn = lua.create_async_function(require)?;
|
|
||||||
let get_source_fn = lua.create_function(move |lua, _: ()| match lua.inspect_stack(2) {
|
|
||||||
None => Err(LuaError::runtime(
|
|
||||||
"Failed to get stack info for require source",
|
|
||||||
)),
|
|
||||||
Some(info) => match info.source().source {
|
|
||||||
None => Err(LuaError::runtime(
|
|
||||||
"Stack info is missing source for require",
|
|
||||||
)),
|
|
||||||
Some(source) => lua.create_string(source.as_bytes()),
|
|
||||||
},
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let require_env = TableBuilder::new(lua)?
|
|
||||||
.with_value("source", get_source_fn)?
|
|
||||||
.with_value("require", require_fn)?
|
|
||||||
.build_readonly()?;
|
|
||||||
|
|
||||||
lua.load(REQUIRE_IMPL)
|
|
||||||
.set_name("require")
|
|
||||||
.set_environment(require_env)
|
|
||||||
.into_function()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn require<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
(source, path): (LuaString<'lua>, LuaString<'lua>),
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>> {
|
|
||||||
let source = source
|
|
||||||
.to_str()
|
|
||||||
.into_lua_err()
|
|
||||||
.context("Failed to parse require source as string")?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let path = path
|
|
||||||
.to_str()
|
|
||||||
.into_lua_err()
|
|
||||||
.context("Failed to parse require path as string")?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let context = lua
|
|
||||||
.app_data_ref()
|
|
||||||
.expect("Failed to get RequireContext from app data");
|
|
||||||
|
|
||||||
if let Some(builtin_name) = path
|
|
||||||
.strip_prefix("@lune/")
|
|
||||||
.map(|name| name.to_ascii_lowercase())
|
|
||||||
{
|
|
||||||
builtin::require(lua, &context, &builtin_name).await
|
|
||||||
} else if let Some(aliased_path) = path.strip_prefix('@') {
|
|
||||||
let (alias, path) = aliased_path.split_once('/').ok_or(LuaError::runtime(
|
|
||||||
"Require with custom alias must contain '/' delimiter",
|
|
||||||
))?;
|
|
||||||
alias::require(lua, &context, &source, alias, path).await
|
|
||||||
} else {
|
|
||||||
path::require(lua, &context, &source, &path).await
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use mlua::Error::ExternalError;
|
|
||||||
|
|
||||||
use super::context::*;
|
|
||||||
|
|
||||||
pub(super) async fn require<'lua, 'ctx>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
ctx: &'ctx RequireContext,
|
|
||||||
source: &str,
|
|
||||||
path: &str,
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>>
|
|
||||||
where
|
|
||||||
'lua: 'ctx,
|
|
||||||
{
|
|
||||||
let (abs_path, rel_path) = ctx.resolve_paths(source, path)?;
|
|
||||||
require_abs_rel(lua, ctx, abs_path, rel_path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn require_abs_rel<'lua, 'ctx>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
ctx: &'ctx RequireContext,
|
|
||||||
abs_path: PathBuf, // Absolute to filesystem
|
|
||||||
rel_path: PathBuf, // Relative to CWD (for displaying)
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>>
|
|
||||||
where
|
|
||||||
'lua: 'ctx,
|
|
||||||
{
|
|
||||||
// 1. Try to require the exact path
|
|
||||||
match require_inner(lua, ctx, &abs_path, &rel_path).await {
|
|
||||||
Ok(res) => return Ok(res),
|
|
||||||
Err(err) => {
|
|
||||||
if !is_file_not_found_error(&err) {
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Try to require the path with an added "luau" extension
|
|
||||||
// 3. Try to require the path with an added "lua" extension
|
|
||||||
for extension in ["luau", "lua"] {
|
|
||||||
match require_inner(
|
|
||||||
lua,
|
|
||||||
ctx,
|
|
||||||
&append_extension(&abs_path, extension),
|
|
||||||
&append_extension(&rel_path, extension),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(res) => return Ok(res),
|
|
||||||
Err(err) => {
|
|
||||||
if !is_file_not_found_error(&err) {
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We didn't find any direct file paths, look
|
|
||||||
// for directories with "init" files in them...
|
|
||||||
let abs_init = abs_path.join("init");
|
|
||||||
let rel_init = rel_path.join("init");
|
|
||||||
|
|
||||||
// 4. Try to require the init path with an added "luau" extension
|
|
||||||
// 5. Try to require the init path with an added "lua" extension
|
|
||||||
for extension in ["luau", "lua"] {
|
|
||||||
match require_inner(
|
|
||||||
lua,
|
|
||||||
ctx,
|
|
||||||
&append_extension(&abs_init, extension),
|
|
||||||
&append_extension(&rel_init, extension),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(res) => return Ok(res),
|
|
||||||
Err(err) => {
|
|
||||||
if !is_file_not_found_error(&err) {
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nothing left to try, throw an error
|
|
||||||
Err(LuaError::runtime(format!(
|
|
||||||
"No file exists at the path '{}'",
|
|
||||||
rel_path.display()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn require_inner<'lua, 'ctx>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
ctx: &'ctx RequireContext,
|
|
||||||
abs_path: impl AsRef<Path>,
|
|
||||||
rel_path: impl AsRef<Path>,
|
|
||||||
) -> LuaResult<LuaMultiValue<'lua>>
|
|
||||||
where
|
|
||||||
'lua: 'ctx,
|
|
||||||
{
|
|
||||||
let abs_path = abs_path.as_ref();
|
|
||||||
let rel_path = rel_path.as_ref();
|
|
||||||
|
|
||||||
if ctx.is_cached(abs_path)? {
|
|
||||||
ctx.get_from_cache(lua, abs_path)
|
|
||||||
} else if ctx.is_pending(abs_path)? {
|
|
||||||
ctx.wait_for_cache(lua, &abs_path).await
|
|
||||||
} else {
|
|
||||||
ctx.load_with_caching(lua, &abs_path, &rel_path).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_extension(path: impl Into<PathBuf>, ext: &'static str) -> PathBuf {
|
|
||||||
let mut new = path.into();
|
|
||||||
match new.extension() {
|
|
||||||
// FUTURE: There's probably a better way to do this than converting to a lossy string
|
|
||||||
Some(e) => new.set_extension(format!("{}.{ext}", e.to_string_lossy())),
|
|
||||||
None => new.set_extension(ext),
|
|
||||||
};
|
|
||||||
new
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_file_not_found_error(err: &LuaError) -> bool {
|
|
||||||
if let ExternalError(err) = err {
|
|
||||||
err.as_ref().downcast_ref::<std::io::Error>().is_some()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
|
|
||||||
let lune_version = format!("Lune {}", env!("CARGO_PKG_VERSION"));
|
|
||||||
|
|
||||||
let luau_version_full = lua
|
|
||||||
.globals()
|
|
||||||
.get::<_, LuaString>("_VERSION")
|
|
||||||
.expect("Missing _VERSION global");
|
|
||||||
let luau_version_str = luau_version_full
|
|
||||||
.to_str()
|
|
||||||
.context("Invalid utf8 found in _VERSION global")?;
|
|
||||||
|
|
||||||
// If this function runs more than once, we
|
|
||||||
// may get an already formatted lune version.
|
|
||||||
if luau_version_str.starts_with(lune_version.as_str()) {
|
|
||||||
return Ok(luau_version_full);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Luau version is expected to be in the format "Luau 0.x" and sometimes "Luau 0.x.y"
|
|
||||||
if !luau_version_str.starts_with("Luau 0.") {
|
|
||||||
panic!("_VERSION global is formatted incorrectly\nGot: '{luau_version_str}'")
|
|
||||||
}
|
|
||||||
let luau_version = luau_version_str.strip_prefix("Luau 0.").unwrap().trim();
|
|
||||||
|
|
||||||
// We make some guarantees about the format of the _VERSION global,
|
|
||||||
// so make sure that the luau version also follows those rules.
|
|
||||||
if luau_version.is_empty() {
|
|
||||||
panic!("_VERSION global is missing version number\nGot: '{luau_version_str}'")
|
|
||||||
} else if !luau_version.chars().all(is_valid_version_char) {
|
|
||||||
panic!("_VERSION global contains invalid characters\nGot: '{luau_version_str}'")
|
|
||||||
}
|
|
||||||
|
|
||||||
lua.create_string(format!("{lune_version}+{luau_version}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_version_char(c: char) -> bool {
|
|
||||||
matches!(c, '0'..='9' | '.')
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
use std::io::Write as _;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::lune::util::formatting::{format_label, pretty_format_multi_value};
|
|
||||||
|
|
||||||
pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
|
|
||||||
lua.create_function(|_, args: LuaMultiValue| {
|
|
||||||
let formatted = format!(
|
|
||||||
"{}\n{}\n",
|
|
||||||
format_label("warn"),
|
|
||||||
pretty_format_multi_value(&args)?
|
|
||||||
);
|
|
||||||
let mut stderr = std::io::stderr();
|
|
||||||
stderr.write_all(formatted.as_bytes())?;
|
|
||||||
stderr.flush()?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,477 +0,0 @@
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use console::{colors_enabled, set_colors_enabled, style, Style};
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
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,
|
|
||||||
parent_table_addr: Option<String>,
|
|
||||||
depth: usize,
|
|
||||||
) -> std::fmt::Result {
|
|
||||||
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) => {
|
|
||||||
let table_addr = Some(format!("{:p}", tab.to_pointer()));
|
|
||||||
|
|
||||||
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 if depth >= 1 && parent_table_addr.eq(&table_addr) {
|
|
||||||
write!(buffer, "{}", STYLE_DIM.apply_to("<self>"))?;
|
|
||||||
} 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,
|
|
||||||
parent_table_addr.clone(),
|
|
||||||
depth + 1,
|
|
||||||
)?;
|
|
||||||
write!(buffer, "] {} ", STYLE_DIM.apply_to("="))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pretty_format_value(buffer, &value, parent_table_addr.clone(), 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 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()).into_lua_err()?;
|
|
||||||
} else {
|
|
||||||
let addr = format!("{:p}", value.to_pointer());
|
|
||||||
|
|
||||||
pretty_format_value(&mut buffer, value, Some(addr), 0).into_lua_err()?;
|
|
||||||
}
|
|
||||||
if counter < multi.len() {
|
|
||||||
write!(&mut buffer, " ").into_lua_err()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,123 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
|
||||||
};
|
|
||||||
|
|
||||||
use path_clean::PathClean;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
use super::paths::make_absolute_and_clean;
|
|
||||||
|
|
||||||
const LUAURC_FILE: &str = ".luaurc";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum LuauLanguageMode {
|
|
||||||
NoCheck,
|
|
||||||
NonStrict,
|
|
||||||
Strict,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct LuauRcConfig {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
language_mode: Option<LuauLanguageMode>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
lint: Option<HashMap<String, JsonValue>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
lint_errors: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
type_errors: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
globals: Option<Vec<String>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
paths: Option<Vec<String>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
aliases: Option<HashMap<String, String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LuauRc {
|
|
||||||
dir: PathBuf,
|
|
||||||
config: LuauRcConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuauRc {
|
|
||||||
pub async fn read(dir: impl AsRef<Path>) -> Option<Self> {
|
|
||||||
let dir = make_absolute_and_clean(dir);
|
|
||||||
let path = dir.join(LUAURC_FILE);
|
|
||||||
let bytes = fs::read(&path).await.ok()?;
|
|
||||||
let config = serde_json::from_slice(&bytes).ok()?;
|
|
||||||
Some(Self { dir, config })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_recursive(
|
|
||||||
dir: impl AsRef<Path>,
|
|
||||||
mut predicate: impl FnMut(&Self) -> bool,
|
|
||||||
) -> Option<Self> {
|
|
||||||
let mut current = make_absolute_and_clean(dir);
|
|
||||||
loop {
|
|
||||||
if let Some(rc) = Self::read(¤t).await {
|
|
||||||
if predicate(&rc) {
|
|
||||||
return Some(rc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(parent) = current.parent() {
|
|
||||||
current = parent.to_path_buf();
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate(&self) -> Result<(), String> {
|
|
||||||
if let Some(aliases) = &self.config.aliases {
|
|
||||||
for alias in aliases.keys() {
|
|
||||||
if !is_valid_alias_key(alias) {
|
|
||||||
return Err(format!("invalid alias key: {}", alias));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn aliases(&self) -> HashMap<String, String> {
|
|
||||||
self.config.aliases.clone().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_alias(&self, name: &str) -> Option<PathBuf> {
|
|
||||||
self.config.aliases.as_ref().and_then(|aliases| {
|
|
||||||
aliases.iter().find_map(|(alias, path)| {
|
|
||||||
if alias
|
|
||||||
.trim_end_matches(MAIN_SEPARATOR)
|
|
||||||
.eq_ignore_ascii_case(name)
|
|
||||||
&& is_valid_alias_key(alias)
|
|
||||||
{
|
|
||||||
Some(self.dir.join(path).clean())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_alias_key(alias: impl AsRef<str>) -> bool {
|
|
||||||
let alias = alias.as_ref();
|
|
||||||
if alias.is_empty()
|
|
||||||
|| alias.starts_with('.')
|
|
||||||
|| alias.starts_with("..")
|
|
||||||
|| alias.chars().any(|c| c == MAIN_SEPARATOR)
|
|
||||||
{
|
|
||||||
false // Paths are not valid alias keys
|
|
||||||
} else {
|
|
||||||
alias.chars().all(is_valid_alias_char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_alias_char(c: char) -> bool {
|
|
||||||
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.'
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
mod table_builder;
|
|
||||||
|
|
||||||
pub mod formatting;
|
|
||||||
pub mod luaurc;
|
|
||||||
pub mod paths;
|
|
||||||
pub mod traits;
|
|
||||||
|
|
||||||
pub use table_builder::TableBuilder;
|
|
|
@ -1,21 +0,0 @@
|
||||||
use std::{
|
|
||||||
env::current_dir,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use path_clean::PathClean;
|
|
||||||
|
|
||||||
pub static CWD: Lazy<PathBuf> = Lazy::new(|| {
|
|
||||||
let cwd = current_dir().expect("failed to find current working directory");
|
|
||||||
dunce::canonicalize(cwd).expect("failed to canonicalize current working directory")
|
|
||||||
});
|
|
||||||
|
|
||||||
pub fn make_absolute_and_clean(path: impl AsRef<Path>) -> PathBuf {
|
|
||||||
let path = path.as_ref();
|
|
||||||
if path.is_relative() {
|
|
||||||
CWD.join(path).clean()
|
|
||||||
} else {
|
|
||||||
path.clean()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use std::future::Future;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
pub struct TableBuilder<'lua> {
|
|
||||||
lua: &'lua Lua,
|
|
||||||
tab: LuaTable<'lua>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> TableBuilder<'lua> {
|
|
||||||
pub fn new(lua: &'lua 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<'lua>,
|
|
||||||
V: IntoLua<'lua>,
|
|
||||||
{
|
|
||||||
self.tab.raw_set(key, value)?;
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_values<K, V>(self, values: Vec<(K, V)>) -> LuaResult<Self>
|
|
||||||
where
|
|
||||||
K: IntoLua<'lua>,
|
|
||||||
V: IntoLua<'lua>,
|
|
||||||
{
|
|
||||||
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<'lua>,
|
|
||||||
{
|
|
||||||
self.tab.raw_push(value)?;
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_sequential_values<V>(self, values: Vec<V>) -> LuaResult<Self>
|
|
||||||
where
|
|
||||||
V: IntoLua<'lua>,
|
|
||||||
{
|
|
||||||
for value in values {
|
|
||||||
self.tab.raw_push(value)?;
|
|
||||||
}
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_function<K, A, R, F>(self, key: K, func: F) -> LuaResult<Self>
|
|
||||||
where
|
|
||||||
K: IntoLua<'lua>,
|
|
||||||
A: FromLuaMulti<'lua>,
|
|
||||||
R: IntoLuaMulti<'lua>,
|
|
||||||
F: Fn(&'lua Lua, A) -> LuaResult<R> + 'static,
|
|
||||||
{
|
|
||||||
let f = self.lua.create_function(func)?;
|
|
||||||
self.with_value(key, LuaValue::Function(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_metatable(self, table: LuaTable) -> LuaResult<Self> {
|
|
||||||
self.tab.set_metatable(Some(table));
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_readonly(self) -> LuaResult<LuaTable<'lua>> {
|
|
||||||
self.tab.set_readonly(true);
|
|
||||||
Ok(self.tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(self) -> LuaResult<LuaTable<'lua>> {
|
|
||||||
Ok(self.tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_async_function<K, A, R, F, FR>(self, key: K, func: F) -> LuaResult<Self>
|
|
||||||
where
|
|
||||||
K: IntoLua<'lua>,
|
|
||||||
A: FromLuaMulti<'lua>,
|
|
||||||
R: IntoLuaMulti<'lua>,
|
|
||||||
F: Fn(&'lua Lua, A) -> FR + 'static,
|
|
||||||
FR: Future<Output = LuaResult<R>> + 'lua,
|
|
||||||
{
|
|
||||||
let f = self.lua.create_async_function(func)?;
|
|
||||||
self.with_value(key, LuaValue::Function(f))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use super::formatting::format_label;
|
|
||||||
use crate::RuntimeError;
|
|
||||||
|
|
||||||
pub trait LuaEmitErrorExt {
|
|
||||||
fn emit_error(&self, err: LuaError);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaEmitErrorExt for Lua {
|
|
||||||
fn emit_error(&self, err: LuaError) {
|
|
||||||
// NOTE: LuneError will pretty-format this error
|
|
||||||
eprintln!("{}\n{}", format_label("error"), RuntimeError::from(err));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,7 +16,8 @@ pub(crate) mod cli;
|
||||||
pub(crate) mod standalone;
|
pub(crate) mod standalone;
|
||||||
|
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
use console::style;
|
|
||||||
|
use lune_utils::fmt::Label;
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
|
@ -35,12 +36,7 @@ async fn main() -> ExitCode {
|
||||||
match Cli::new().run().await {
|
match Cli::new().run().await {
|
||||||
Ok(code) => code,
|
Ok(code) => code,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!(
|
eprintln!("{}\n{err:?}", Label::Error);
|
||||||
"{}{}{}\n{err:?}",
|
|
||||||
style("[").dim(),
|
|
||||||
style("ERROR").red(),
|
|
||||||
style("]").dim(),
|
|
||||||
);
|
|
||||||
ExitCode::FAILURE
|
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 => {
|
|
||||||
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,58 +0,0 @@
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub(crate) trait DomValueExt {
|
|
||||||
fn variant_name(&self) -> Option<&'static str>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DomValueExt for DomType {
|
|
||||||
fn variant_name(&self) -> Option<&'static str> {
|
|
||||||
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",
|
|
||||||
MaterialColors => "MaterialColors",
|
|
||||||
NumberRange => "NumberRange",
|
|
||||||
NumberSequence => "NumberSequence",
|
|
||||||
PhysicalProperties => "PhysicalProperties",
|
|
||||||
Ray => "Ray",
|
|
||||||
Rect => "Rect",
|
|
||||||
Ref => "Ref",
|
|
||||||
Region3 => "Region3",
|
|
||||||
Region3int16 => "Region3int16",
|
|
||||||
SharedString => "SharedString",
|
|
||||||
String => "String",
|
|
||||||
Tags => "Tags",
|
|
||||||
UDim => "UDim",
|
|
||||||
UDim2 => "UDim2",
|
|
||||||
UniqueId => "UniqueId",
|
|
||||||
Vector2 => "Vector2",
|
|
||||||
Vector2int16 => "Vector2int16",
|
|
||||||
Vector3 => "Vector3",
|
|
||||||
Vector3int16 => "Vector3int16",
|
|
||||||
OptionalCFrame => "OptionalCFrame",
|
|
||||||
SecurityCapabilities => "SecurityCapabilities",
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DomValueExt for DomValue {
|
|
||||||
fn variant_name(&self) -> Option<&'static str> {
|
|
||||||
self.ty().variant_name()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +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;
|
|
||||||
|
|
||||||
mod util;
|
|
||||||
|
|
||||||
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,125 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Axes as DomAxes;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
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 LuaExportsTable<'_> for Axes {
|
|
||||||
const EXPORT_NAME: &'static str = "Axes";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let axes_new = |_, 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 })
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", axes_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,439 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
use rbx_dom_weak::types::BrickColor as DomBrickColor;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, Color3};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [BrickColor](https://create.roblox.com/docs/reference/engine/datatypes/BrickColor) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the BrickColor class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[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 LuaExportsTable<'_> for BrickColor {
|
|
||||||
const EXPORT_NAME: &'static str = "BrickColor";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
type ArgsNumber = u16;
|
|
||||||
type ArgsName = String;
|
|
||||||
type ArgsRgb = (u8, u8, u8);
|
|
||||||
type ArgsColor3<'lua> = LuaUserDataRef<'lua, Color3>;
|
|
||||||
|
|
||||||
let brick_color_new = |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(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let brick_color_palette = |_, 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()))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let brick_color_random = |_, ()| {
|
|
||||||
let number = BRICK_COLOR_PALETTE.choose(&mut rand::thread_rng());
|
|
||||||
Ok(color_from_number(*number.unwrap()))
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut builder = TableBuilder::new(lua)?
|
|
||||||
.with_function("new", brick_color_new)?
|
|
||||||
.with_function("palette", brick_color_palette)?
|
|
||||||
.with_function("random", brick_color_random)?;
|
|
||||||
|
|
||||||
for (name, number) in BRICK_COLOR_CONSTRUCTORS {
|
|
||||||
let f = |_, ()| Ok(color_from_number(*number));
|
|
||||||
builder = builder.with_function(*name, f)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,471 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use glam::{EulerRot, Mat3, Mat4, Quat, Vec3};
|
|
||||||
use mlua::{prelude::*, Variadic};
|
|
||||||
use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3};
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, Vector3};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [CFrame](https://create.roblox.com/docs/reference/engine/datatypes/CFrame)
|
|
||||||
Roblox datatype, backed by [`glam::Mat4`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods &
|
|
||||||
constructors of the CFrame class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct CFrame(pub Mat4);
|
|
||||||
|
|
||||||
impl CFrame {
|
|
||||||
pub const IDENTITY: Self = Self(Mat4::IDENTITY);
|
|
||||||
|
|
||||||
fn position(&self) -> Vec3 {
|
|
||||||
self.0.w_axis.truncate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn orientation(&self) -> Mat3 {
|
|
||||||
Mat3::from_cols(
|
|
||||||
self.0.x_axis.truncate(),
|
|
||||||
self.0.y_axis.truncate(),
|
|
||||||
self.0.z_axis.truncate(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inverse(&self) -> Self {
|
|
||||||
Self(self.0.inverse())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for CFrame {
|
|
||||||
const EXPORT_NAME: &'static str = "CFrame";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let cframe_angles = |_, (rx, ry, rz): (f32, f32, f32)| {
|
|
||||||
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
|
|
||||||
};
|
|
||||||
|
|
||||||
let cframe_from_axis_angle =
|
|
||||||
|_, (v, r): (LuaUserDataRef<Vector3>, f32)| Ok(CFrame(Mat4::from_axis_angle(v.0, r)));
|
|
||||||
|
|
||||||
let cframe_from_euler_angles_xyz = |_, (rx, ry, rz): (f32, f32, f32)| {
|
|
||||||
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
|
|
||||||
};
|
|
||||||
|
|
||||||
let cframe_from_euler_angles_yxz = |_, (rx, ry, rz): (f32, f32, f32)| {
|
|
||||||
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
|
|
||||||
};
|
|
||||||
|
|
||||||
let cframe_from_matrix = |_,
|
|
||||||
(pos, rx, ry, rz): (
|
|
||||||
LuaUserDataRef<Vector3>,
|
|
||||||
LuaUserDataRef<Vector3>,
|
|
||||||
LuaUserDataRef<Vector3>,
|
|
||||||
Option<LuaUserDataRef<Vector3>>,
|
|
||||||
)| {
|
|
||||||
Ok(CFrame(Mat4::from_cols(
|
|
||||||
rx.0.extend(0.0),
|
|
||||||
ry.0.extend(0.0),
|
|
||||||
rz.map(|r| r.0)
|
|
||||||
.unwrap_or_else(|| rx.0.cross(ry.0).normalize())
|
|
||||||
.extend(0.0),
|
|
||||||
pos.0.extend(1.0),
|
|
||||||
)))
|
|
||||||
};
|
|
||||||
|
|
||||||
let cframe_from_orientation = |_, (rx, ry, rz): (f32, f32, f32)| {
|
|
||||||
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
|
|
||||||
};
|
|
||||||
|
|
||||||
let cframe_look_at = |_,
|
|
||||||
(from, to, up): (
|
|
||||||
LuaUserDataRef<Vector3>,
|
|
||||||
LuaUserDataRef<Vector3>,
|
|
||||||
Option<LuaUserDataRef<Vector3>>,
|
|
||||||
)| {
|
|
||||||
Ok(CFrame(look_at(
|
|
||||||
from.0,
|
|
||||||
to.0,
|
|
||||||
up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0,
|
|
||||||
)))
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dynamic args constructor
|
|
||||||
type ArgsPos<'lua> = LuaUserDataRef<'lua, Vector3>;
|
|
||||||
type ArgsLook<'lua> = (
|
|
||||||
LuaUserDataRef<'lua, Vector3>,
|
|
||||||
LuaUserDataRef<'lua, Vector3>,
|
|
||||||
Option<LuaUserDataRef<'lua, Vector3>>,
|
|
||||||
);
|
|
||||||
|
|
||||||
type ArgsPosXYZ = (f32, f32, f32);
|
|
||||||
type ArgsPosXYZQuat = (f32, f32, f32, f32, f32, f32, f32);
|
|
||||||
type ArgsMatrix = (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32);
|
|
||||||
|
|
||||||
let cframe_new = |lua, args: LuaMultiValue| match args.len() {
|
|
||||||
0 => Ok(CFrame(Mat4::IDENTITY)),
|
|
||||||
|
|
||||||
1 => match ArgsPos::from_lua_multi(args, lua) {
|
|
||||||
Ok(pos) => Ok(CFrame(Mat4::from_translation(pos.0))),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
},
|
|
||||||
|
|
||||||
3 => {
|
|
||||||
if let Ok((from, to, up)) = ArgsLook::from_lua_multi(args.clone(), lua) {
|
|
||||||
Ok(CFrame(look_at(
|
|
||||||
from.0,
|
|
||||||
to.0,
|
|
||||||
up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0,
|
|
||||||
)))
|
|
||||||
} else if let Ok((x, y, z)) = ArgsPosXYZ::from_lua_multi(args, lua) {
|
|
||||||
Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z))))
|
|
||||||
} else {
|
|
||||||
// TODO: Make this error message better
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Invalid arguments to constructor".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
7 => match ArgsPosXYZQuat::from_lua_multi(args, lua) {
|
|
||||||
Ok((x, y, z, qx, qy, qz, qw)) => Ok(CFrame(Mat4::from_rotation_translation(
|
|
||||||
Quat::from_array([qx, qy, qz, qw]),
|
|
||||||
Vec3::new(x, y, z),
|
|
||||||
))),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
},
|
|
||||||
|
|
||||||
12 => match ArgsMatrix::from_lua_multi(args, lua) {
|
|
||||||
Ok((x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)) => {
|
|
||||||
Ok(CFrame(Mat4::from_cols_array_2d(&[
|
|
||||||
[r00, r10, r20, 0.0],
|
|
||||||
[r01, r11, r21, 0.0],
|
|
||||||
[r02, r12, r22, 0.0],
|
|
||||||
[x, y, z, 1.0],
|
|
||||||
])))
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => Err(LuaError::RuntimeError(format!(
|
|
||||||
"Invalid number of arguments: expected 0, 1, 3, 7, or 12, got {}",
|
|
||||||
args.len()
|
|
||||||
))),
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("Angles", cframe_angles)?
|
|
||||||
.with_value("identity", CFrame(Mat4::IDENTITY))?
|
|
||||||
.with_function("fromAxisAngle", cframe_from_axis_angle)?
|
|
||||||
.with_function("fromEulerAnglesXYZ", cframe_from_euler_angles_xyz)?
|
|
||||||
.with_function("fromEulerAnglesYXZ", cframe_from_euler_angles_yxz)?
|
|
||||||
.with_function("fromMatrix", cframe_from_matrix)?
|
|
||||||
.with_function("fromOrientation", cframe_from_orientation)?
|
|
||||||
.with_function("lookAt", cframe_look_at)?
|
|
||||||
.with_function("new", cframe_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for CFrame {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Position", |_, this| Ok(Vector3(this.position())));
|
|
||||||
fields.add_field_method_get("Rotation", |_, this| {
|
|
||||||
Ok(CFrame(Mat4::from_cols(
|
|
||||||
this.0.x_axis,
|
|
||||||
this.0.y_axis,
|
|
||||||
this.0.z_axis,
|
|
||||||
Vec3::ZERO.extend(1.0),
|
|
||||||
)))
|
|
||||||
});
|
|
||||||
fields.add_field_method_get("X", |_, this| Ok(this.position().x));
|
|
||||||
fields.add_field_method_get("Y", |_, this| Ok(this.position().y));
|
|
||||||
fields.add_field_method_get("Z", |_, this| Ok(this.position().z));
|
|
||||||
fields.add_field_method_get("XVector", |_, this| Ok(Vector3(this.orientation().x_axis)));
|
|
||||||
fields.add_field_method_get("YVector", |_, this| Ok(Vector3(this.orientation().y_axis)));
|
|
||||||
fields.add_field_method_get("ZVector", |_, this| Ok(Vector3(this.orientation().z_axis)));
|
|
||||||
fields.add_field_method_get("RightVector", |_, this| {
|
|
||||||
Ok(Vector3(this.orientation().x_axis))
|
|
||||||
});
|
|
||||||
fields.add_field_method_get("UpVector", |_, this| Ok(Vector3(this.orientation().y_axis)));
|
|
||||||
fields.add_field_method_get("LookVector", |_, this| {
|
|
||||||
Ok(Vector3(-this.orientation().z_axis))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
// Methods
|
|
||||||
methods.add_method("Inverse", |_, this, ()| Ok(this.inverse()));
|
|
||||||
methods.add_method(
|
|
||||||
"Lerp",
|
|
||||||
|_, this, (goal, alpha): (LuaUserDataRef<CFrame>, f32)| {
|
|
||||||
let quat_this = Quat::from_mat4(&this.0);
|
|
||||||
let quat_goal = Quat::from_mat4(&goal.0);
|
|
||||||
let translation = this
|
|
||||||
.0
|
|
||||||
.w_axis
|
|
||||||
.truncate()
|
|
||||||
.lerp(goal.0.w_axis.truncate(), alpha);
|
|
||||||
let rotation = quat_this.slerp(quat_goal, alpha);
|
|
||||||
Ok(CFrame(Mat4::from_rotation_translation(
|
|
||||||
rotation,
|
|
||||||
translation,
|
|
||||||
)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method("Orthonormalize", |_, this, ()| {
|
|
||||||
let rotation = Quat::from_mat4(&this.0);
|
|
||||||
let translation = this.0.w_axis.truncate();
|
|
||||||
Ok(CFrame(Mat4::from_rotation_translation(
|
|
||||||
rotation.normalize(),
|
|
||||||
translation,
|
|
||||||
)))
|
|
||||||
});
|
|
||||||
methods.add_method(
|
|
||||||
"ToWorldSpace",
|
|
||||||
|_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
|
|
||||||
Ok(Variadic::from_iter(rhs.into_iter().map(|cf| *this * *cf)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method(
|
|
||||||
"ToObjectSpace",
|
|
||||||
|_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
|
|
||||||
let inverse = this.inverse();
|
|
||||||
Ok(Variadic::from_iter(rhs.into_iter().map(|cf| inverse * *cf)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method(
|
|
||||||
"PointToWorldSpace",
|
|
||||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
|
||||||
Ok(Variadic::from_iter(rhs.into_iter().map(|v3| *this * *v3)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method(
|
|
||||||
"PointToObjectSpace",
|
|
||||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
|
||||||
let inverse = this.inverse();
|
|
||||||
Ok(Variadic::from_iter(rhs.into_iter().map(|v3| inverse * *v3)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method(
|
|
||||||
"VectorToWorldSpace",
|
|
||||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
|
||||||
let result = *this - Vector3(this.position());
|
|
||||||
Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method(
|
|
||||||
"VectorToObjectSpace",
|
|
||||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
|
||||||
let inverse = this.inverse();
|
|
||||||
let result = inverse - Vector3(inverse.position());
|
|
||||||
|
|
||||||
Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
#[rustfmt::skip]
|
|
||||||
methods.add_method("GetComponents", |_, this, ()| {
|
|
||||||
let pos = this.position();
|
|
||||||
let transposed = this.orientation().transpose();
|
|
||||||
Ok((
|
|
||||||
pos.x, pos.y, pos.z,
|
|
||||||
transposed.x_axis.x, transposed.x_axis.y, transposed.x_axis.z,
|
|
||||||
transposed.y_axis.x, transposed.y_axis.y, transposed.y_axis.z,
|
|
||||||
transposed.z_axis.x, transposed.z_axis.y, transposed.z_axis.z,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
methods.add_method("ToEulerAnglesXYZ", |_, this, ()| {
|
|
||||||
Ok(Quat::from_mat4(&this.0).to_euler(EulerRot::XYZ))
|
|
||||||
});
|
|
||||||
methods.add_method("ToEulerAnglesYXZ", |_, this, ()| {
|
|
||||||
let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ);
|
|
||||||
Ok((rx, ry, rz))
|
|
||||||
});
|
|
||||||
methods.add_method("ToOrientation", |_, this, ()| {
|
|
||||||
let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ);
|
|
||||||
Ok((rx, ry, rz))
|
|
||||||
});
|
|
||||||
methods.add_method("ToAxisAngle", |_, this, ()| {
|
|
||||||
let (axis, angle) = Quat::from_mat4(&this.0).to_axis_angle();
|
|
||||||
Ok((Vector3(axis), angle))
|
|
||||||
});
|
|
||||||
// Metamethods
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Mul, |lua, this, rhs: LuaValue| {
|
|
||||||
if let LuaValue::UserData(ud) = &rhs {
|
|
||||||
if let Ok(cf) = ud.borrow::<CFrame>() {
|
|
||||||
return lua.create_userdata(*this * *cf);
|
|
||||||
} else if let Ok(vec) = ud.borrow::<Vector3>() {
|
|
||||||
return lua.create_userdata(*this * *vec);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: rhs.type_name(),
|
|
||||||
to: "userdata",
|
|
||||||
message: Some(format!(
|
|
||||||
"Expected CFrame or Vector3, got {}",
|
|
||||||
rhs.type_name()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
methods.add_meta_method(
|
|
||||||
LuaMetaMethod::Add,
|
|
||||||
|_, this, vec: LuaUserDataRef<Vector3>| Ok(*this + *vec),
|
|
||||||
);
|
|
||||||
methods.add_meta_method(
|
|
||||||
LuaMetaMethod::Sub,
|
|
||||||
|_, this, vec: LuaUserDataRef<Vector3>| Ok(*this - *vec),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CFrame {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let pos = self.position();
|
|
||||||
let transposed = self.orientation().transpose();
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}, {}, {}, {}",
|
|
||||||
Vector3(pos),
|
|
||||||
Vector3(transposed.x_axis),
|
|
||||||
Vector3(transposed.y_axis),
|
|
||||||
Vector3(transposed.z_axis)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul for CFrame {
|
|
||||||
type Output = Self;
|
|
||||||
fn mul(self, rhs: Self) -> Self::Output {
|
|
||||||
CFrame(self.0 * rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul<Vector3> for CFrame {
|
|
||||||
type Output = Vector3;
|
|
||||||
fn mul(self, rhs: Vector3) -> Self::Output {
|
|
||||||
Vector3(self.0.project_point3(rhs.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add<Vector3> for CFrame {
|
|
||||||
type Output = Self;
|
|
||||||
fn add(self, rhs: Vector3) -> Self::Output {
|
|
||||||
CFrame(Mat4::from_cols(
|
|
||||||
self.0.x_axis,
|
|
||||||
self.0.y_axis,
|
|
||||||
self.0.z_axis,
|
|
||||||
self.0.w_axis + rhs.0.extend(0.0),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub<Vector3> for CFrame {
|
|
||||||
type Output = Self;
|
|
||||||
fn sub(self, rhs: Vector3) -> Self::Output {
|
|
||||||
CFrame(Mat4::from_cols(
|
|
||||||
self.0.x_axis,
|
|
||||||
self.0.y_axis,
|
|
||||||
self.0.z_axis,
|
|
||||||
self.0.w_axis - rhs.0.extend(0.0),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomCFrame> for CFrame {
|
|
||||||
fn from(v: DomCFrame) -> Self {
|
|
||||||
let transposed = v.orientation.transpose();
|
|
||||||
CFrame(Mat4::from_cols(
|
|
||||||
Vector3::from(transposed.x).0.extend(0.0),
|
|
||||||
Vector3::from(transposed.y).0.extend(0.0),
|
|
||||||
Vector3::from(transposed.z).0.extend(0.0),
|
|
||||||
Vector3::from(v.position).0.extend(1.0),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CFrame> for DomCFrame {
|
|
||||||
fn from(v: CFrame) -> Self {
|
|
||||||
let transposed = v.orientation().transpose();
|
|
||||||
DomCFrame {
|
|
||||||
position: DomVector3::from(Vector3(v.position())),
|
|
||||||
orientation: DomMatrix3::new(
|
|
||||||
DomVector3::from(Vector3(transposed.x_axis)),
|
|
||||||
DomVector3::from(Vector3(transposed.y_axis)),
|
|
||||||
DomVector3::from(Vector3(transposed.z_axis)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates a matrix at the position `from`, looking towards `to`.
|
|
||||||
|
|
||||||
[`glam`] does provide functions such as [`look_at_lh`], [`look_at_rh`] and more but
|
|
||||||
they all create view matrices for camera transforms which is not what we want here.
|
|
||||||
*/
|
|
||||||
fn look_at(from: Vec3, to: Vec3, up: Vec3) -> Mat4 {
|
|
||||||
let dir = (to - from).normalize();
|
|
||||||
let xaxis = up.cross(dir).normalize();
|
|
||||||
let yaxis = dir.cross(xaxis).normalize();
|
|
||||||
|
|
||||||
Mat4::from_cols(
|
|
||||||
Vec3::new(xaxis.x, yaxis.x, dir.x).extend(0.0),
|
|
||||||
Vec3::new(xaxis.y, yaxis.y, dir.y).extend(0.0),
|
|
||||||
Vec3::new(xaxis.z, yaxis.z, dir.z).extend(0.0),
|
|
||||||
from.extend(1.0),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod cframe_test {
|
|
||||||
use glam::{Mat4, Vec3};
|
|
||||||
use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3};
|
|
||||||
|
|
||||||
use super::CFrame;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dom_cframe_from_cframe() {
|
|
||||||
let dom_cframe = DomCFrame::new(
|
|
||||||
DomVector3::new(1.0, 2.0, 3.0),
|
|
||||||
DomMatrix3::new(
|
|
||||||
DomVector3::new(1.0, 2.0, 3.0),
|
|
||||||
DomVector3::new(1.0, 2.0, 3.0),
|
|
||||||
DomVector3::new(1.0, 2.0, 3.0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let cframe = CFrame(Mat4::from_cols(
|
|
||||||
Vec3::new(1.0, 1.0, 1.0).extend(0.0),
|
|
||||||
Vec3::new(2.0, 2.0, 2.0).extend(0.0),
|
|
||||||
Vec3::new(3.0, 3.0, 3.0).extend(0.0),
|
|
||||||
Vec3::new(1.0, 2.0, 3.0).extend(1.0),
|
|
||||||
));
|
|
||||||
|
|
||||||
assert_eq!(CFrame::from(dom_cframe), cframe)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cframe_from_dom_cframe() {
|
|
||||||
let cframe = CFrame(Mat4::from_cols(
|
|
||||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
|
||||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
|
||||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
|
||||||
Vec3::new(1.0, 2.0, 3.0).extend(1.0),
|
|
||||||
));
|
|
||||||
|
|
||||||
let dom_cframe = DomCFrame::new(
|
|
||||||
DomVector3::new(1.0, 2.0, 3.0),
|
|
||||||
DomMatrix3::new(
|
|
||||||
DomVector3::new(1.0, 1.0, 1.0),
|
|
||||||
DomVector3::new(2.0, 2.0, 2.0),
|
|
||||||
DomVector3::new(3.0, 3.0, 3.0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(DomCFrame::from(cframe), dom_cframe)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,312 +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 crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
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 LuaExportsTable<'_> for Color3 {
|
|
||||||
const EXPORT_NAME: &'static str = "Color3";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let color3_from_rgb = |_, (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,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let color3_from_hsv = |_, (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 })
|
|
||||||
};
|
|
||||||
|
|
||||||
let color3_from_hex = |_, 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
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let color3_new = |_, (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(),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("fromRGB", color3_from_rgb)?
|
|
||||||
.with_function("fromHSV", color3_from_hsv)?
|
|
||||||
.with_function("fromHex", color3_from_hex)?
|
|
||||||
.with_function("new", color3_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for Color3 {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
if let LuaValue::UserData(ud) = value {
|
|
||||||
Ok(*ud.borrow::<Color3>()?)
|
|
||||||
} else {
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "Color3",
|
|
||||||
message: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
Color3::from(DomColor3::from(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Color3> for DomColor3uint8 {
|
|
||||||
fn from(v: Color3) -> Self {
|
|
||||||
DomColor3uint8::from(DomColor3::from(v))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::{
|
|
||||||
ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, Color3, ColorSequenceKeypoint};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [ColorSequence](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequence) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the ColorSequence class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct ColorSequence {
|
|
||||||
pub(crate) keypoints: Vec<ColorSequenceKeypoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for ColorSequence {
|
|
||||||
const EXPORT_NAME: &'static str = "ColorSequence";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
type ArgsColor<'lua> = LuaUserDataRef<'lua, Color3>;
|
|
||||||
type ArgsColors<'lua> = (LuaUserDataRef<'lua, Color3>, LuaUserDataRef<'lua, Color3>);
|
|
||||||
type ArgsKeypoints<'lua> = Vec<LuaUserDataRef<'lua, ColorSequenceKeypoint>>;
|
|
||||||
|
|
||||||
let color_sequence_new = |lua, args: LuaMultiValue| {
|
|
||||||
if let Ok(color) = ArgsColor::from_lua_multi(args.clone(), lua) {
|
|
||||||
Ok(ColorSequence {
|
|
||||||
keypoints: vec![
|
|
||||||
ColorSequenceKeypoint {
|
|
||||||
time: 0.0,
|
|
||||||
color: *color,
|
|
||||||
},
|
|
||||||
ColorSequenceKeypoint {
|
|
||||||
time: 1.0,
|
|
||||||
color: *color,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} else if let Ok((c0, c1)) = ArgsColors::from_lua_multi(args.clone(), lua) {
|
|
||||||
Ok(ColorSequence {
|
|
||||||
keypoints: vec![
|
|
||||||
ColorSequenceKeypoint {
|
|
||||||
time: 0.0,
|
|
||||||
color: *c0,
|
|
||||||
},
|
|
||||||
ColorSequenceKeypoint {
|
|
||||||
time: 1.0,
|
|
||||||
color: *c1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) {
|
|
||||||
Ok(ColorSequence {
|
|
||||||
keypoints: keypoints.iter().map(|k| **k).collect(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// FUTURE: Better error message here using given arg types
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Invalid arguments to constructor".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", color_sequence_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for ColorSequence {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ColorSequence {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
for (index, keypoint) in self.keypoints.iter().enumerate() {
|
|
||||||
if index < self.keypoints.len() - 1 {
|
|
||||||
write!(f, "{}, ", keypoint)?;
|
|
||||||
} else {
|
|
||||||
write!(f, "{}", keypoint)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomColorSequence> for ColorSequence {
|
|
||||||
fn from(v: DomColorSequence) -> Self {
|
|
||||||
Self {
|
|
||||||
keypoints: v
|
|
||||||
.keypoints
|
|
||||||
.iter()
|
|
||||||
.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,72 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::ColorSequenceKeypoint as DomColorSequenceKeypoint;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, Color3};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [ColorSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequenceKeypoint) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the ColorSequenceKeypoint class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct ColorSequenceKeypoint {
|
|
||||||
pub(crate) time: f32,
|
|
||||||
pub(crate) color: Color3,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for ColorSequenceKeypoint {
|
|
||||||
const EXPORT_NAME: &'static str = "ColorSequenceKeypoint";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let color_sequence_keypoint_new = |_, (time, color): (f32, LuaUserDataRef<Color3>)| {
|
|
||||||
Ok(ColorSequenceKeypoint {
|
|
||||||
time,
|
|
||||||
color: *color,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", color_sequence_keypoint_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Enum as DomEnum;
|
|
||||||
|
|
||||||
use super::{super::*, Enum};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [EnumItem](https://create.roblox.com/docs/reference/engine/datatypes/EnumItem) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the EnumItem class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct EnumItem {
|
|
||||||
pub(crate) parent: Enum,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) value: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EnumItem {
|
|
||||||
pub(crate) fn from_enum_and_name(parent: &Enum, name: impl AsRef<str>) -> Option<Self> {
|
|
||||||
let enum_name = name.as_ref();
|
|
||||||
parent.desc.items.iter().find_map(|(name, v)| {
|
|
||||||
if *name == enum_name {
|
|
||||||
Some(Self {
|
|
||||||
parent: parent.clone(),
|
|
||||||
name: enum_name.to_string(),
|
|
||||||
value: *v,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_enum_and_value(parent: &Enum, value: u32) -> Option<Self> {
|
|
||||||
parent.desc.items.iter().find_map(|(name, v)| {
|
|
||||||
if *v == value {
|
|
||||||
Some(Self {
|
|
||||||
parent: parent.clone(),
|
|
||||||
name: name.to_string(),
|
|
||||||
value,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_enum_name_and_name(
|
|
||||||
enum_name: impl AsRef<str>,
|
|
||||||
name: impl AsRef<str>,
|
|
||||||
) -> Option<Self> {
|
|
||||||
let parent = Enum::from_name(enum_name)?;
|
|
||||||
Self::from_enum_and_name(&parent, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_enum_name_and_value(enum_name: impl AsRef<str>, value: u32) -> Option<Self> {
|
|
||||||
let parent = Enum::from_name(enum_name)?;
|
|
||||||
Self::from_enum_and_value(&parent, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for EnumItem {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Name", |_, this| Ok(this.name.clone()));
|
|
||||||
fields.add_field_method_get("Value", |_, this| Ok(this.value));
|
|
||||||
fields.add_field_method_get("EnumType", |_, this| Ok(this.parent.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<'lua> FromLua<'lua> for EnumItem {
|
|
||||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
|
||||||
if let LuaValue::UserData(ud) = value {
|
|
||||||
Ok(ud.borrow::<EnumItem>()?.to_owned())
|
|
||||||
} else {
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "EnumItem",
|
|
||||||
message: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for EnumItem {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}.{}", self.parent, self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for EnumItem {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.parent == other.parent && self.value == other.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<EnumItem> for DomEnum {
|
|
||||||
fn from(v: EnumItem) -> Self {
|
|
||||||
DomEnum::from_u32(v.value)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use super::{super::*, Enum};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Enums](https://create.roblox.com/docs/reference/engine/datatypes/Enums) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the Enums class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Enums;
|
|
||||||
|
|
||||||
impl LuaUserData for Enums {
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
// Methods
|
|
||||||
methods.add_method("GetEnums", |_, _, ()| {
|
|
||||||
let db = rbx_reflection_database::get();
|
|
||||||
Ok(db.enums.values().map(Enum::from).collect::<Vec<_>>())
|
|
||||||
});
|
|
||||||
methods.add_meta_method(
|
|
||||||
LuaMetaMethod::Index,
|
|
||||||
|_, _, name: String| match Enum::from_name(&name) {
|
|
||||||
Some(e) => Ok(e),
|
|
||||||
None => Err(LuaError::RuntimeError(format!(
|
|
||||||
"The enum '{}' does not exist",
|
|
||||||
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 Enums {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "Enum")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Faces as DomFaces;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, EnumItem};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Faces](https://create.roblox.com/docs/reference/engine/datatypes/Faces) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the Faces class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Faces {
|
|
||||||
pub(crate) right: bool,
|
|
||||||
pub(crate) top: bool,
|
|
||||||
pub(crate) back: bool,
|
|
||||||
pub(crate) left: bool,
|
|
||||||
pub(crate) bottom: bool,
|
|
||||||
pub(crate) front: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Faces {
|
|
||||||
const EXPORT_NAME: &'static str = "Faces";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let faces_new = |_, args: LuaMultiValue| {
|
|
||||||
let mut right = false;
|
|
||||||
let mut top = false;
|
|
||||||
let mut back = false;
|
|
||||||
let mut left = false;
|
|
||||||
let mut bottom = false;
|
|
||||||
let mut front = false;
|
|
||||||
|
|
||||||
let mut check = |e: &EnumItem| {
|
|
||||||
if e.parent.desc.name == "NormalId" {
|
|
||||||
match &e.name {
|
|
||||||
name if name == "Right" => right = true,
|
|
||||||
name if name == "Top" => top = true,
|
|
||||||
name if name == "Back" => back = true,
|
|
||||||
name if name == "Left" => left = true,
|
|
||||||
name if name == "Bottom" => bottom = true,
|
|
||||||
name if name == "Front" => front = 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(Faces {
|
|
||||||
right,
|
|
||||||
top,
|
|
||||||
back,
|
|
||||||
left,
|
|
||||||
bottom,
|
|
||||||
front,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", faces_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Faces {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Right", |_, this| Ok(this.right));
|
|
||||||
fields.add_field_method_get("Top", |_, this| Ok(this.top));
|
|
||||||
fields.add_field_method_get("Back", |_, this| Ok(this.back));
|
|
||||||
fields.add_field_method_get("Left", |_, this| Ok(this.left));
|
|
||||||
fields.add_field_method_get("Bottom", |_, this| Ok(this.bottom));
|
|
||||||
fields.add_field_method_get("Front", |_, this| Ok(this.front));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Faces {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let write = make_list_writer();
|
|
||||||
write(f, self.right, "Right")?;
|
|
||||||
write(f, self.top, "Top")?;
|
|
||||||
write(f, self.back, "Back")?;
|
|
||||||
write(f, self.left, "Left")?;
|
|
||||||
write(f, self.bottom, "Bottom")?;
|
|
||||||
write(f, self.front, "Front")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomFaces> for Faces {
|
|
||||||
fn from(v: DomFaces) -> Self {
|
|
||||||
let bits = v.bits();
|
|
||||||
Self {
|
|
||||||
right: (bits & 1) == 1,
|
|
||||||
top: ((bits >> 1) & 1) == 1,
|
|
||||||
back: ((bits >> 2) & 1) == 1,
|
|
||||||
left: ((bits >> 3) & 1) == 1,
|
|
||||||
bottom: ((bits >> 4) & 1) == 1,
|
|
||||||
front: ((bits >> 5) & 1) == 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Faces> for DomFaces {
|
|
||||||
fn from(v: Faces) -> Self {
|
|
||||||
let mut bits = 0;
|
|
||||||
bits += v.right as u8;
|
|
||||||
bits += (v.top as u8) << 1;
|
|
||||||
bits += (v.back as u8) << 2;
|
|
||||||
bits += (v.left as u8) << 3;
|
|
||||||
bits += (v.bottom as u8) << 4;
|
|
||||||
bits += (v.front as u8) << 5;
|
|
||||||
DomFaces::from_bits(bits).expect("Invalid bits")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,469 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::{
|
|
||||||
Font as DomFont, FontStyle as DomFontStyle, FontWeight as DomFontWeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, EnumItem};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Font](https://create.roblox.com/docs/reference/engine/datatypes/Font) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the Font class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Font {
|
|
||||||
pub(crate) family: String,
|
|
||||||
pub(crate) weight: FontWeight,
|
|
||||||
pub(crate) style: FontStyle,
|
|
||||||
pub(crate) cached_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Font {
|
|
||||||
pub(crate) fn from_enum_item(material_enum_item: &EnumItem) -> Option<Font> {
|
|
||||||
FONT_ENUM_MAP
|
|
||||||
.iter()
|
|
||||||
.find(|props| props.0 == material_enum_item.name && props.1.is_some())
|
|
||||||
.map(|props| props.1.as_ref().unwrap())
|
|
||||||
.map(|props| Font {
|
|
||||||
family: props.0.to_string(),
|
|
||||||
weight: props.1,
|
|
||||||
style: props.2,
|
|
||||||
cached_id: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Font {
|
|
||||||
const EXPORT_NAME: &'static str = "Font";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let font_from_enum = |_, value: LuaUserDataRef<EnumItem>| {
|
|
||||||
if value.parent.desc.name == "Font" {
|
|
||||||
match Font::from_enum_item(&value) {
|
|
||||||
Some(props) => Ok(props),
|
|
||||||
None => Err(LuaError::RuntimeError(format!(
|
|
||||||
"Found unknown Font '{}'",
|
|
||||||
value.name
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"Expected argument #1 to be a Font, got {}",
|
|
||||||
value.parent.desc.name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let font_from_name =
|
|
||||||
|_, (file, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
|
|
||||||
Ok(Font {
|
|
||||||
family: format!("rbxasset://fonts/families/{}.json", file),
|
|
||||||
weight: weight.unwrap_or_default(),
|
|
||||||
style: style.unwrap_or_default(),
|
|
||||||
cached_id: None,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let font_from_id =
|
|
||||||
|_, (id, weight, style): (i32, Option<FontWeight>, Option<FontStyle>)| {
|
|
||||||
Ok(Font {
|
|
||||||
family: format!("rbxassetid://{}", id),
|
|
||||||
weight: weight.unwrap_or_default(),
|
|
||||||
style: style.unwrap_or_default(),
|
|
||||||
cached_id: None,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let font_new =
|
|
||||||
|_, (family, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
|
|
||||||
Ok(Font {
|
|
||||||
family,
|
|
||||||
weight: weight.unwrap_or_default(),
|
|
||||||
style: style.unwrap_or_default(),
|
|
||||||
cached_id: None,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("fromEnum", font_from_enum)?
|
|
||||||
.with_function("fromName", font_from_name)?
|
|
||||||
.with_function("fromId", font_from_id)?
|
|
||||||
.with_function("new", font_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Font {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
// Getters
|
|
||||||
fields.add_field_method_get("Family", |_, this| Ok(this.family.clone()));
|
|
||||||
fields.add_field_method_get("Weight", |_, this| Ok(this.weight));
|
|
||||||
fields.add_field_method_get("Style", |_, this| Ok(this.style));
|
|
||||||
fields.add_field_method_get("Bold", |_, this| Ok(this.weight.as_u16() >= 600));
|
|
||||||
// Setters
|
|
||||||
fields.add_field_method_set("Weight", |_, this, value: FontWeight| {
|
|
||||||
this.weight = value;
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
fields.add_field_method_set("Style", |_, this, value: FontStyle| {
|
|
||||||
this.style = value;
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
fields.add_field_method_set("Bold", |_, this, value: bool| {
|
|
||||||
if value {
|
|
||||||
this.weight = FontWeight::Bold;
|
|
||||||
} else {
|
|
||||||
this.weight = FontWeight::Regular;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Font {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}, {}", self.family, self.weight, self.style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomFont> for Font {
|
|
||||||
fn from(v: DomFont) -> Self {
|
|
||||||
Self {
|
|
||||||
family: v.family,
|
|
||||||
weight: v.weight.into(),
|
|
||||||
style: v.style.into(),
|
|
||||||
cached_id: v.cached_face_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Font> for DomFont {
|
|
||||||
fn from(v: Font) -> Self {
|
|
||||||
DomFont {
|
|
||||||
family: v.family,
|
|
||||||
weight: v.weight.into(),
|
|
||||||
style: v.style.into(),
|
|
||||||
cached_face_id: v.cached_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomFontWeight> for FontWeight {
|
|
||||||
fn from(v: DomFontWeight) -> Self {
|
|
||||||
FontWeight::from_u16(v.as_u16()).expect("Missing font weight")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<FontWeight> for DomFontWeight {
|
|
||||||
fn from(v: FontWeight) -> Self {
|
|
||||||
DomFontWeight::from_u16(v.as_u16()).expect("Missing rbx font weight")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomFontStyle> for FontStyle {
|
|
||||||
fn from(v: DomFontStyle) -> Self {
|
|
||||||
FontStyle::from_u8(v.as_u8()).expect("Missing font weight")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<FontStyle> for DomFontStyle {
|
|
||||||
fn from(v: FontStyle) -> Self {
|
|
||||||
DomFontStyle::from_u8(v.as_u8()).expect("Missing rbx font weight")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
NOTE: The font code below is all generated using the
|
|
||||||
font_enum_map script in the scripts dir next to src,
|
|
||||||
which can be ran in the Roblox Studio command bar
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
type FontData = (&'static str, FontWeight, FontStyle);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub(crate) enum FontWeight {
|
|
||||||
Thin,
|
|
||||||
ExtraLight,
|
|
||||||
Light,
|
|
||||||
Regular,
|
|
||||||
Medium,
|
|
||||||
SemiBold,
|
|
||||||
Bold,
|
|
||||||
ExtraBold,
|
|
||||||
Heavy,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FontWeight {
|
|
||||||
pub(crate) fn as_u16(&self) -> u16 {
|
|
||||||
match self {
|
|
||||||
Self::Thin => 100,
|
|
||||||
Self::ExtraLight => 200,
|
|
||||||
Self::Light => 300,
|
|
||||||
Self::Regular => 400,
|
|
||||||
Self::Medium => 500,
|
|
||||||
Self::SemiBold => 600,
|
|
||||||
Self::Bold => 700,
|
|
||||||
Self::ExtraBold => 800,
|
|
||||||
Self::Heavy => 900,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_u16(n: u16) -> Option<Self> {
|
|
||||||
match n {
|
|
||||||
100 => Some(Self::Thin),
|
|
||||||
200 => Some(Self::ExtraLight),
|
|
||||||
300 => Some(Self::Light),
|
|
||||||
400 => Some(Self::Regular),
|
|
||||||
500 => Some(Self::Medium),
|
|
||||||
600 => Some(Self::SemiBold),
|
|
||||||
700 => Some(Self::Bold),
|
|
||||||
800 => Some(Self::ExtraBold),
|
|
||||||
900 => Some(Self::Heavy),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FontWeight {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Regular
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for FontWeight {
|
|
||||||
type Err = &'static str;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"Thin" => Ok(Self::Thin),
|
|
||||||
"ExtraLight" => Ok(Self::ExtraLight),
|
|
||||||
"Light" => Ok(Self::Light),
|
|
||||||
"Regular" => Ok(Self::Regular),
|
|
||||||
"Medium" => Ok(Self::Medium),
|
|
||||||
"SemiBold" => Ok(Self::SemiBold),
|
|
||||||
"Bold" => Ok(Self::Bold),
|
|
||||||
"ExtraBold" => Ok(Self::ExtraBold),
|
|
||||||
"Heavy" => Ok(Self::Heavy),
|
|
||||||
_ => Err("Unknown FontWeight"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for FontWeight {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
match self {
|
|
||||||
Self::Thin => "Thin",
|
|
||||||
Self::ExtraLight => "ExtraLight",
|
|
||||||
Self::Light => "Light",
|
|
||||||
Self::Regular => "Regular",
|
|
||||||
Self::Medium => "Medium",
|
|
||||||
Self::SemiBold => "SemiBold",
|
|
||||||
Self::Bold => "Bold",
|
|
||||||
Self::ExtraBold => "ExtraBold",
|
|
||||||
Self::Heavy => "Heavy",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for FontWeight {
|
|
||||||
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 == "FontWeight" {
|
|
||||||
if let Ok(value) = FontWeight::from_str(&value.name) {
|
|
||||||
return Ok(value);
|
|
||||||
} else {
|
|
||||||
message = Some(format!(
|
|
||||||
"Found unknown Enum.FontWeight value '{}'",
|
|
||||||
value.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message = Some(format!(
|
|
||||||
"Expected Enum.FontWeight, got Enum.{}",
|
|
||||||
value.parent.desc.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: lua_value.type_name(),
|
|
||||||
to: "Enum.FontWeight",
|
|
||||||
message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> IntoLua<'lua> for FontWeight {
|
|
||||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
match EnumItem::from_enum_name_and_name("FontWeight", self.to_string()) {
|
|
||||||
Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)),
|
|
||||||
None => Err(LuaError::ToLuaConversionError {
|
|
||||||
from: "FontWeight",
|
|
||||||
to: "EnumItem",
|
|
||||||
message: Some(format!("Found unknown Enum.FontWeight value '{}'", self)),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub(crate) enum FontStyle {
|
|
||||||
Normal,
|
|
||||||
Italic,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FontStyle {
|
|
||||||
pub(crate) fn as_u8(&self) -> u8 {
|
|
||||||
match self {
|
|
||||||
Self::Normal => 0,
|
|
||||||
Self::Italic => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_u8(n: u8) -> Option<Self> {
|
|
||||||
match n {
|
|
||||||
0 => Some(Self::Normal),
|
|
||||||
1 => Some(Self::Italic),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FontStyle {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for FontStyle {
|
|
||||||
type Err = &'static str;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"Normal" => Ok(Self::Normal),
|
|
||||||
"Italic" => Ok(Self::Italic),
|
|
||||||
_ => Err("Unknown FontStyle"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for FontStyle {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
match self {
|
|
||||||
Self::Normal => "Normal",
|
|
||||||
Self::Italic => "Italic",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for FontStyle {
|
|
||||||
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 == "FontStyle" {
|
|
||||||
if let Ok(value) = FontStyle::from_str(&value.name) {
|
|
||||||
return Ok(value);
|
|
||||||
} else {
|
|
||||||
message = Some(format!(
|
|
||||||
"Found unknown Enum.FontStyle value '{}'",
|
|
||||||
value.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message = Some(format!(
|
|
||||||
"Expected Enum.FontStyle, got Enum.{}",
|
|
||||||
value.parent.desc.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: lua_value.type_name(),
|
|
||||||
to: "Enum.FontStyle",
|
|
||||||
message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'lua> IntoLua<'lua> for FontStyle {
|
|
||||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
match EnumItem::from_enum_name_and_name("FontStyle", self.to_string()) {
|
|
||||||
Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)),
|
|
||||||
None => Err(LuaError::ToLuaConversionError {
|
|
||||||
from: "FontStyle",
|
|
||||||
to: "EnumItem",
|
|
||||||
message: Some(format!("Found unknown Enum.FontStyle value '{}'", self)),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
const FONT_ENUM_MAP: &[(&str, Option<FontData>)] = &[
|
|
||||||
("Legacy", Some(("rbxasset://fonts/families/LegacyArial.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Arial", Some(("rbxasset://fonts/families/Arial.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("ArialBold", Some(("rbxasset://fonts/families/Arial.json", FontWeight::Bold, FontStyle::Normal))),
|
|
||||||
("SourceSans", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("SourceSansBold", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Bold, FontStyle::Normal))),
|
|
||||||
("SourceSansSemibold", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::SemiBold, FontStyle::Normal))),
|
|
||||||
("SourceSansLight", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Light, FontStyle::Normal))),
|
|
||||||
("SourceSansItalic", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Regular, FontStyle::Italic))),
|
|
||||||
("Bodoni", Some(("rbxasset://fonts/families/AccanthisADFStd.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Garamond", Some(("rbxasset://fonts/families/Guru.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Cartoon", Some(("rbxasset://fonts/families/ComicNeueAngular.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Code", Some(("rbxasset://fonts/families/Inconsolata.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Highway", Some(("rbxasset://fonts/families/HighwayGothic.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("SciFi", Some(("rbxasset://fonts/families/Zekton.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Arcade", Some(("rbxasset://fonts/families/PressStart2P.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Fantasy", Some(("rbxasset://fonts/families/Balthazar.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Antique", Some(("rbxasset://fonts/families/RomanAntique.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Gotham", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("GothamMedium", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Medium, FontStyle::Normal))),
|
|
||||||
("GothamBold", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Bold, FontStyle::Normal))),
|
|
||||||
("GothamBlack", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Heavy, FontStyle::Normal))),
|
|
||||||
("AmaticSC", Some(("rbxasset://fonts/families/AmaticSC.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Bangers", Some(("rbxasset://fonts/families/Bangers.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Creepster", Some(("rbxasset://fonts/families/Creepster.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("DenkOne", Some(("rbxasset://fonts/families/DenkOne.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Fondamento", Some(("rbxasset://fonts/families/Fondamento.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("FredokaOne", Some(("rbxasset://fonts/families/FredokaOne.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("GrenzeGotisch", Some(("rbxasset://fonts/families/GrenzeGotisch.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("IndieFlower", Some(("rbxasset://fonts/families/IndieFlower.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("JosefinSans", Some(("rbxasset://fonts/families/JosefinSans.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Jura", Some(("rbxasset://fonts/families/Jura.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Kalam", Some(("rbxasset://fonts/families/Kalam.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("LuckiestGuy", Some(("rbxasset://fonts/families/LuckiestGuy.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Merriweather", Some(("rbxasset://fonts/families/Merriweather.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Michroma", Some(("rbxasset://fonts/families/Michroma.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Nunito", Some(("rbxasset://fonts/families/Nunito.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Oswald", Some(("rbxasset://fonts/families/Oswald.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("PatrickHand", Some(("rbxasset://fonts/families/PatrickHand.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("PermanentMarker", Some(("rbxasset://fonts/families/PermanentMarker.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Roboto", Some(("rbxasset://fonts/families/Roboto.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("RobotoCondensed", Some(("rbxasset://fonts/families/RobotoCondensed.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("RobotoMono", Some(("rbxasset://fonts/families/RobotoMono.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Sarpanch", Some(("rbxasset://fonts/families/Sarpanch.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("SpecialElite", Some(("rbxasset://fonts/families/SpecialElite.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("TitilliumWeb", Some(("rbxasset://fonts/families/TitilliumWeb.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Ubuntu", Some(("rbxasset://fonts/families/Ubuntu.json", FontWeight::Regular, FontStyle::Normal))),
|
|
||||||
("Unknown", None),
|
|
||||||
];
|
|
|
@ -1,51 +0,0 @@
|
||||||
mod axes;
|
|
||||||
mod brick_color;
|
|
||||||
mod cframe;
|
|
||||||
mod color3;
|
|
||||||
mod color_sequence;
|
|
||||||
mod color_sequence_keypoint;
|
|
||||||
mod r#enum;
|
|
||||||
mod r#enum_item;
|
|
||||||
mod r#enums;
|
|
||||||
mod faces;
|
|
||||||
mod font;
|
|
||||||
mod number_range;
|
|
||||||
mod number_sequence;
|
|
||||||
mod number_sequence_keypoint;
|
|
||||||
mod physical_properties;
|
|
||||||
mod ray;
|
|
||||||
mod rect;
|
|
||||||
mod region3;
|
|
||||||
mod region3int16;
|
|
||||||
mod udim;
|
|
||||||
mod udim2;
|
|
||||||
mod vector2;
|
|
||||||
mod vector2int16;
|
|
||||||
mod vector3;
|
|
||||||
mod vector3int16;
|
|
||||||
|
|
||||||
pub use axes::Axes;
|
|
||||||
pub use brick_color::BrickColor;
|
|
||||||
pub use cframe::CFrame;
|
|
||||||
pub use color3::Color3;
|
|
||||||
pub use color_sequence::ColorSequence;
|
|
||||||
pub use color_sequence_keypoint::ColorSequenceKeypoint;
|
|
||||||
pub use faces::Faces;
|
|
||||||
pub use font::Font;
|
|
||||||
pub use number_range::NumberRange;
|
|
||||||
pub use number_sequence::NumberSequence;
|
|
||||||
pub use number_sequence_keypoint::NumberSequenceKeypoint;
|
|
||||||
pub use physical_properties::PhysicalProperties;
|
|
||||||
pub use r#enum::Enum;
|
|
||||||
pub use r#enum_item::EnumItem;
|
|
||||||
pub use r#enums::Enums;
|
|
||||||
pub use ray::Ray;
|
|
||||||
pub use rect::Rect;
|
|
||||||
pub use region3::Region3;
|
|
||||||
pub use region3int16::Region3int16;
|
|
||||||
pub use udim::UDim;
|
|
||||||
pub use udim2::UDim2;
|
|
||||||
pub use vector2::Vector2;
|
|
||||||
pub use vector2int16::Vector2int16;
|
|
||||||
pub use vector3::Vector3;
|
|
||||||
pub use vector3int16::Vector3int16;
|
|
|
@ -1,75 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::NumberRange as DomNumberRange;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::super::*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [NumberRange](https://create.roblox.com/docs/reference/engine/datatypes/NumberRange) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the NumberRange class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct NumberRange {
|
|
||||||
pub(crate) min: f32,
|
|
||||||
pub(crate) max: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for NumberRange {
|
|
||||||
const EXPORT_NAME: &'static str = "NumberRange";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let number_range_new = |_, (min, max): (f32, Option<f32>)| {
|
|
||||||
Ok(match max {
|
|
||||||
Some(max) => NumberRange {
|
|
||||||
min: min.min(max),
|
|
||||||
max: min.max(max),
|
|
||||||
},
|
|
||||||
None => NumberRange { min, max: min },
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", number_range_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for NumberRange {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Min", |_, this| Ok(this.min));
|
|
||||||
fields.add_field_method_get("Max", |_, this| Ok(this.max));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 NumberRange {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", self.min, self.max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomNumberRange> for NumberRange {
|
|
||||||
fn from(v: DomNumberRange) -> Self {
|
|
||||||
Self {
|
|
||||||
min: v.min,
|
|
||||||
max: v.max,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NumberRange> for DomNumberRange {
|
|
||||||
fn from(v: NumberRange) -> Self {
|
|
||||||
Self {
|
|
||||||
min: v.min,
|
|
||||||
max: v.max,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::{
|
|
||||||
NumberSequence as DomNumberSequence, NumberSequenceKeypoint as DomNumberSequenceKeypoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, NumberSequenceKeypoint};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [NumberSequence](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequence) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the NumberSequence class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct NumberSequence {
|
|
||||||
pub(crate) keypoints: Vec<NumberSequenceKeypoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for NumberSequence {
|
|
||||||
const EXPORT_NAME: &'static str = "NumberSequence";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
type ArgsColor = f32;
|
|
||||||
type ArgsColors = (f32, f32);
|
|
||||||
type ArgsKeypoints<'lua> = Vec<LuaUserDataRef<'lua, NumberSequenceKeypoint>>;
|
|
||||||
|
|
||||||
let number_sequence_new = |lua, args: LuaMultiValue| {
|
|
||||||
if let Ok(value) = ArgsColor::from_lua_multi(args.clone(), lua) {
|
|
||||||
Ok(NumberSequence {
|
|
||||||
keypoints: vec![
|
|
||||||
NumberSequenceKeypoint {
|
|
||||||
time: 0.0,
|
|
||||||
value,
|
|
||||||
envelope: 0.0,
|
|
||||||
},
|
|
||||||
NumberSequenceKeypoint {
|
|
||||||
time: 1.0,
|
|
||||||
value,
|
|
||||||
envelope: 0.0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} else if let Ok((v0, v1)) = ArgsColors::from_lua_multi(args.clone(), lua) {
|
|
||||||
Ok(NumberSequence {
|
|
||||||
keypoints: vec![
|
|
||||||
NumberSequenceKeypoint {
|
|
||||||
time: 0.0,
|
|
||||||
value: v0,
|
|
||||||
envelope: 0.0,
|
|
||||||
},
|
|
||||||
NumberSequenceKeypoint {
|
|
||||||
time: 1.0,
|
|
||||||
value: v1,
|
|
||||||
envelope: 0.0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) {
|
|
||||||
Ok(NumberSequence {
|
|
||||||
keypoints: keypoints.iter().map(|k| **k).collect(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// FUTURE: Better error message here using given arg types
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Invalid arguments to constructor".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", number_sequence_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for NumberSequence {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for NumberSequence {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
for (index, keypoint) in self.keypoints.iter().enumerate() {
|
|
||||||
if index < self.keypoints.len() - 1 {
|
|
||||||
write!(f, "{}, ", keypoint)?;
|
|
||||||
} else {
|
|
||||||
write!(f, "{}", keypoint)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomNumberSequence> for NumberSequence {
|
|
||||||
fn from(v: DomNumberSequence) -> Self {
|
|
||||||
Self {
|
|
||||||
keypoints: v
|
|
||||||
.keypoints
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(NumberSequenceKeypoint::from)
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NumberSequence> for DomNumberSequence {
|
|
||||||
fn from(v: NumberSequence) -> Self {
|
|
||||||
Self {
|
|
||||||
keypoints: v
|
|
||||||
.keypoints
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(DomNumberSequenceKeypoint::from)
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::NumberSequenceKeypoint as DomNumberSequenceKeypoint;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::super::*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [NumberSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequenceKeypoint) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the NumberSequenceKeypoint class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct NumberSequenceKeypoint {
|
|
||||||
pub(crate) time: f32,
|
|
||||||
pub(crate) value: f32,
|
|
||||||
pub(crate) envelope: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for NumberSequenceKeypoint {
|
|
||||||
const EXPORT_NAME: &'static str = "NumberSequenceKeypoint";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let number_sequence_keypoint_new = |_, (time, value, envelope): (f32, f32, Option<f32>)| {
|
|
||||||
Ok(NumberSequenceKeypoint {
|
|
||||||
time,
|
|
||||||
value,
|
|
||||||
envelope: envelope.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", number_sequence_keypoint_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for NumberSequenceKeypoint {
|
|
||||||
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.value));
|
|
||||||
fields.add_field_method_get("Envelope", |_, this| Ok(this.envelope));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 NumberSequenceKeypoint {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{} > {}", self.time, self.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomNumberSequenceKeypoint> for NumberSequenceKeypoint {
|
|
||||||
fn from(v: DomNumberSequenceKeypoint) -> Self {
|
|
||||||
Self {
|
|
||||||
time: v.time,
|
|
||||||
value: v.value,
|
|
||||||
envelope: v.envelope,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NumberSequenceKeypoint> for DomNumberSequenceKeypoint {
|
|
||||||
fn from(v: NumberSequenceKeypoint) -> Self {
|
|
||||||
Self {
|
|
||||||
time: v.time,
|
|
||||||
value: v.value,
|
|
||||||
envelope: v.envelope,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::CustomPhysicalProperties as DomCustomPhysicalProperties;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, EnumItem};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [PhysicalProperties](https://create.roblox.com/docs/reference/engine/datatypes/PhysicalProperties) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the PhysicalProperties class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct PhysicalProperties {
|
|
||||||
pub(crate) density: f32,
|
|
||||||
pub(crate) friction: f32,
|
|
||||||
pub(crate) friction_weight: f32,
|
|
||||||
pub(crate) elasticity: f32,
|
|
||||||
pub(crate) elasticity_weight: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PhysicalProperties {
|
|
||||||
pub(crate) fn from_material(material_enum_item: &EnumItem) -> Option<PhysicalProperties> {
|
|
||||||
MATERIAL_ENUM_MAP
|
|
||||||
.iter()
|
|
||||||
.find(|props| props.0 == material_enum_item.name)
|
|
||||||
.map(|props| PhysicalProperties {
|
|
||||||
density: props.1,
|
|
||||||
friction: props.2,
|
|
||||||
elasticity: props.3,
|
|
||||||
friction_weight: props.4,
|
|
||||||
elasticity_weight: props.5,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for PhysicalProperties {
|
|
||||||
const EXPORT_NAME: &'static str = "PhysicalProperties";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
type ArgsMaterial<'lua> = LuaUserDataRef<'lua, EnumItem>;
|
|
||||||
type ArgsNumbers = (f32, f32, f32, Option<f32>, Option<f32>);
|
|
||||||
|
|
||||||
let physical_properties_new = |lua, args: LuaMultiValue| {
|
|
||||||
if let Ok(value) = ArgsMaterial::from_lua_multi(args.clone(), lua) {
|
|
||||||
if value.parent.desc.name == "Material" {
|
|
||||||
match PhysicalProperties::from_material(&value) {
|
|
||||||
Some(props) => Ok(props),
|
|
||||||
None => Err(LuaError::RuntimeError(format!(
|
|
||||||
"Found unknown Material '{}'",
|
|
||||||
value.name
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"Expected argument #1 to be a Material, got {}",
|
|
||||||
value.parent.desc.name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else if let Ok((density, friction, elasticity, friction_weight, elasticity_weight)) =
|
|
||||||
ArgsNumbers::from_lua_multi(args, lua)
|
|
||||||
{
|
|
||||||
Ok(PhysicalProperties {
|
|
||||||
density,
|
|
||||||
friction,
|
|
||||||
friction_weight: friction_weight.unwrap_or(1.0),
|
|
||||||
elasticity,
|
|
||||||
elasticity_weight: elasticity_weight.unwrap_or(1.0),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// FUTURE: Better error message here using given arg types
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Invalid arguments to constructor".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", physical_properties_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for PhysicalProperties {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Density", |_, this| Ok(this.density));
|
|
||||||
fields.add_field_method_get("Friction", |_, this| Ok(this.friction));
|
|
||||||
fields.add_field_method_get("FrictionWeight", |_, this| Ok(this.friction_weight));
|
|
||||||
fields.add_field_method_get("Elasticity", |_, this| Ok(this.elasticity));
|
|
||||||
fields.add_field_method_get("ElasticityWeight", |_, this| Ok(this.elasticity_weight));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 PhysicalProperties {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}, {}, {}, {}, {}",
|
|
||||||
self.density,
|
|
||||||
self.friction,
|
|
||||||
self.elasticity,
|
|
||||||
self.friction_weight,
|
|
||||||
self.elasticity_weight
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomCustomPhysicalProperties> for PhysicalProperties {
|
|
||||||
fn from(v: DomCustomPhysicalProperties) -> Self {
|
|
||||||
Self {
|
|
||||||
density: v.density,
|
|
||||||
friction: v.friction,
|
|
||||||
friction_weight: v.friction_weight,
|
|
||||||
elasticity: v.elasticity,
|
|
||||||
elasticity_weight: v.elasticity_weight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PhysicalProperties> for DomCustomPhysicalProperties {
|
|
||||||
fn from(v: PhysicalProperties) -> Self {
|
|
||||||
DomCustomPhysicalProperties {
|
|
||||||
density: v.density,
|
|
||||||
friction: v.friction,
|
|
||||||
friction_weight: v.friction_weight,
|
|
||||||
elasticity: v.elasticity,
|
|
||||||
elasticity_weight: v.elasticity_weight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
NOTE: The material definitions below are generated using the
|
|
||||||
physical_properties_enum_map script in the scripts dir next
|
|
||||||
to src, which can be ran in the Roblox Studio command bar
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
const MATERIAL_ENUM_MAP: &[(&str, f32, f32, f32, f32, f32)] = &[
|
|
||||||
("Plastic", 0.70, 0.30, 0.50, 1.00, 1.00),
|
|
||||||
("Wood", 0.35, 0.48, 0.20, 1.00, 1.00),
|
|
||||||
("Slate", 2.69, 0.40, 0.20, 1.00, 1.00),
|
|
||||||
("Concrete", 2.40, 0.70, 0.20, 0.30, 1.00),
|
|
||||||
("CorrodedMetal", 7.85, 0.70, 0.20, 1.00, 1.00),
|
|
||||||
("DiamondPlate", 7.85, 0.35, 0.25, 1.00, 1.00),
|
|
||||||
("Foil", 2.70, 0.40, 0.25, 1.00, 1.00),
|
|
||||||
("Grass", 0.90, 0.40, 0.10, 1.00, 1.50),
|
|
||||||
("Ice", 0.92, 0.02, 0.15, 3.00, 1.00),
|
|
||||||
("Marble", 2.56, 0.20, 0.17, 1.00, 1.00),
|
|
||||||
("Granite", 2.69, 0.40, 0.20, 1.00, 1.00),
|
|
||||||
("Brick", 1.92, 0.80, 0.15, 0.30, 1.00),
|
|
||||||
("Pebble", 2.40, 0.40, 0.17, 1.00, 1.50),
|
|
||||||
("Sand", 1.60, 0.50, 0.05, 5.00, 2.50),
|
|
||||||
("Fabric", 0.70, 0.35, 0.05, 1.00, 1.00),
|
|
||||||
("SmoothPlastic", 0.70, 0.20, 0.50, 1.00, 1.00),
|
|
||||||
("Metal", 7.85, 0.40, 0.25, 1.00, 1.00),
|
|
||||||
("WoodPlanks", 0.35, 0.48, 0.20, 1.00, 1.00),
|
|
||||||
("Cobblestone", 2.69, 0.50, 0.17, 1.00, 1.00),
|
|
||||||
("Air", 0.01, 0.01, 0.01, 1.00, 1.00),
|
|
||||||
("Water", 1.00, 0.00, 0.01, 1.00, 1.00),
|
|
||||||
("Rock", 2.69, 0.50, 0.17, 1.00, 1.00),
|
|
||||||
("Glacier", 0.92, 0.05, 0.15, 2.00, 1.00),
|
|
||||||
("Snow", 0.90, 0.30, 0.03, 3.00, 4.00),
|
|
||||||
("Sandstone", 2.69, 0.50, 0.15, 5.00, 1.00),
|
|
||||||
("Mud", 0.90, 0.30, 0.07, 3.00, 4.00),
|
|
||||||
("Basalt", 2.69, 0.70, 0.15, 0.30, 1.00),
|
|
||||||
("Ground", 0.90, 0.45, 0.10, 1.00, 1.00),
|
|
||||||
("CrackedLava", 2.69, 0.65, 0.15, 1.00, 1.00),
|
|
||||||
("Neon", 0.70, 0.30, 0.20, 1.00, 1.00),
|
|
||||||
("Glass", 2.40, 0.25, 0.20, 1.00, 1.00),
|
|
||||||
("Asphalt", 2.36, 0.80, 0.20, 0.30, 1.00),
|
|
||||||
("LeafyGrass", 0.90, 0.40, 0.10, 2.00, 2.00),
|
|
||||||
("Salt", 2.16, 0.50, 0.05, 1.00, 1.00),
|
|
||||||
("Limestone", 2.69, 0.50, 0.15, 1.00, 1.00),
|
|
||||||
("Pavement", 2.69, 0.50, 0.17, 0.30, 1.00),
|
|
||||||
("ForceField", 2.40, 0.25, 0.20, 1.00, 1.00),
|
|
||||||
];
|
|
|
@ -1,100 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use glam::Vec3;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Ray as DomRay;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, Vector3};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Ray](https://create.roblox.com/docs/reference/engine/datatypes/Ray)
|
|
||||||
Roblox datatype, backed by [`glam::Vec3`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the Ray class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Ray {
|
|
||||||
pub(crate) origin: Vec3,
|
|
||||||
pub(crate) direction: Vec3,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ray {
|
|
||||||
fn closest_point(&self, point: Vec3) -> Vec3 {
|
|
||||||
let norm = self.direction.normalize();
|
|
||||||
let lhs = point - self.origin;
|
|
||||||
|
|
||||||
let dot_product = lhs.dot(norm).max(0.0);
|
|
||||||
self.origin + norm * dot_product
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Ray {
|
|
||||||
const EXPORT_NAME: &'static str = "Ray";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let ray_new =
|
|
||||||
|_, (origin, direction): (LuaUserDataRef<Vector3>, LuaUserDataRef<Vector3>)| {
|
|
||||||
Ok(Ray {
|
|
||||||
origin: origin.0,
|
|
||||||
direction: direction.0,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", ray_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Ray {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Origin", |_, this| Ok(Vector3(this.origin)));
|
|
||||||
fields.add_field_method_get("Direction", |_, this| Ok(Vector3(this.direction)));
|
|
||||||
fields.add_field_method_get("Unit", |_, this| {
|
|
||||||
Ok(Ray {
|
|
||||||
origin: this.origin,
|
|
||||||
direction: this.direction.normalize(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
// Methods
|
|
||||||
methods.add_method("ClosestPoint", |_, this, to: LuaUserDataRef<Vector3>| {
|
|
||||||
Ok(Vector3(this.closest_point(to.0)))
|
|
||||||
});
|
|
||||||
methods.add_method("Distance", |_, this, to: LuaUserDataRef<Vector3>| {
|
|
||||||
let closest = this.closest_point(to.0);
|
|
||||||
Ok((closest - to.0).length())
|
|
||||||
});
|
|
||||||
// Metamethods
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Ray {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", Vector3(self.origin), Vector3(self.direction))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomRay> for Ray {
|
|
||||||
fn from(v: DomRay) -> Self {
|
|
||||||
Ray {
|
|
||||||
origin: Vector3::from(v.origin).0,
|
|
||||||
direction: Vector3::from(v.direction).0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Ray> for DomRay {
|
|
||||||
fn from(v: Ray) -> Self {
|
|
||||||
DomRay {
|
|
||||||
origin: Vector3(v.origin).into(),
|
|
||||||
direction: Vector3(v.direction).into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use glam::Vec2;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Rect as DomRect;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, Vector2};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Rect](https://create.roblox.com/docs/reference/engine/datatypes/Rect)
|
|
||||||
Roblox datatype, backed by [`glam::Vec2`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the Rect class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Rect {
|
|
||||||
pub(crate) min: Vec2,
|
|
||||||
pub(crate) max: Vec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rect {
|
|
||||||
fn new(lhs: Vec2, rhs: Vec2) -> Self {
|
|
||||||
Self {
|
|
||||||
min: lhs.min(rhs),
|
|
||||||
max: lhs.max(rhs),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Rect {
|
|
||||||
const EXPORT_NAME: &'static str = "Rect";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
type ArgsVector2s<'lua> = (
|
|
||||||
Option<LuaUserDataRef<'lua, Vector2>>,
|
|
||||||
Option<LuaUserDataRef<'lua, Vector2>>,
|
|
||||||
);
|
|
||||||
type ArgsNums = (Option<f32>, Option<f32>, Option<f32>, Option<f32>);
|
|
||||||
|
|
||||||
let rect_new = |lua, args: LuaMultiValue| {
|
|
||||||
if let Ok((min, max)) = ArgsVector2s::from_lua_multi(args.clone(), lua) {
|
|
||||||
Ok(Rect::new(
|
|
||||||
min.map(|m| *m).unwrap_or_default().0,
|
|
||||||
max.map(|m| *m).unwrap_or_default().0,
|
|
||||||
))
|
|
||||||
} else if let Ok((x0, y0, x1, y1)) = ArgsNums::from_lua_multi(args, lua) {
|
|
||||||
let min = Vec2::new(x0.unwrap_or_default(), y0.unwrap_or_default());
|
|
||||||
let max = Vec2::new(x1.unwrap_or_default(), y1.unwrap_or_default());
|
|
||||||
Ok(Rect::new(min, max))
|
|
||||||
} else {
|
|
||||||
// FUTURE: Better error message here using given arg types
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Invalid arguments to constructor".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", rect_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Rect {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Min", |_, this| Ok(Vector2(this.min)));
|
|
||||||
fields.add_field_method_get("Max", |_, this| Ok(Vector2(this.max)));
|
|
||||||
fields.add_field_method_get("Width", |_, this| Ok(this.max.x - this.min.x));
|
|
||||||
fields.add_field_method_get("Height", |_, this| Ok(this.max.y - this.min.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
|
||||||
methods.add_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Rect {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", self.min, self.max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Neg for Rect {
|
|
||||||
type Output = Self;
|
|
||||||
fn neg(self) -> Self::Output {
|
|
||||||
Rect::new(-self.min, -self.max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add for Rect {
|
|
||||||
type Output = Self;
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Rect::new(self.min + rhs.min, self.max + rhs.max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub for Rect {
|
|
||||||
type Output = Self;
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
Rect::new(self.min - rhs.min, self.max - rhs.max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomRect> for Rect {
|
|
||||||
fn from(v: DomRect) -> Self {
|
|
||||||
Rect {
|
|
||||||
min: Vec2::new(v.min.x, v.min.y),
|
|
||||||
max: Vec2::new(v.max.x, v.max.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Rect> for DomRect {
|
|
||||||
fn from(v: Rect) -> Self {
|
|
||||||
DomRect {
|
|
||||||
min: Vector2(v.min).into(),
|
|
||||||
max: Vector2(v.max).into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use glam::{Mat4, Vec3};
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Region3 as DomRegion3;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, CFrame, Vector3};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Region3](https://create.roblox.com/docs/reference/engine/datatypes/Region3)
|
|
||||||
Roblox datatype, backed by [`glam::Vec3`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the Region3 class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Region3 {
|
|
||||||
pub(crate) min: Vec3,
|
|
||||||
pub(crate) max: Vec3,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Region3 {
|
|
||||||
const EXPORT_NAME: &'static str = "Region3";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let region3_new = |_, (min, max): (LuaUserDataRef<Vector3>, LuaUserDataRef<Vector3>)| {
|
|
||||||
Ok(Region3 {
|
|
||||||
min: min.0,
|
|
||||||
max: max.0,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", region3_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Region3 {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("CFrame", |_, this| {
|
|
||||||
Ok(CFrame(Mat4::from_translation(this.min.lerp(this.max, 0.5))))
|
|
||||||
});
|
|
||||||
fields.add_field_method_get("Size", |_, this| Ok(Vector3(this.max - this.min)));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
// Methods
|
|
||||||
methods.add_method("ExpandToGrid", |_, this, resolution: f32| {
|
|
||||||
Ok(Region3 {
|
|
||||||
min: (this.min / resolution).floor() * resolution,
|
|
||||||
max: (this.max / resolution).ceil() * resolution,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
// Metamethods
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Region3 {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", Vector3(self.min), Vector3(self.max))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomRegion3> for Region3 {
|
|
||||||
fn from(v: DomRegion3) -> Self {
|
|
||||||
Region3 {
|
|
||||||
min: Vector3::from(v.min).0,
|
|
||||||
max: Vector3::from(v.max).0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Region3> for DomRegion3 {
|
|
||||||
fn from(v: Region3) -> Self {
|
|
||||||
DomRegion3 {
|
|
||||||
min: Vector3(v.min).into(),
|
|
||||||
max: Vector3(v.max).into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use glam::IVec3;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Region3int16 as DomRegion3int16;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, Vector3int16};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Region3int16](https://create.roblox.com/docs/reference/engine/datatypes/Region3int16)
|
|
||||||
Roblox datatype, backed by [`glam::IVec3`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the Region3int16 class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Region3int16 {
|
|
||||||
pub(crate) min: IVec3,
|
|
||||||
pub(crate) max: IVec3,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Region3int16 {
|
|
||||||
const EXPORT_NAME: &'static str = "Region3int16";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let region3int16_new =
|
|
||||||
|_, (min, max): (LuaUserDataRef<Vector3int16>, LuaUserDataRef<Vector3int16>)| {
|
|
||||||
Ok(Region3int16 {
|
|
||||||
min: min.0,
|
|
||||||
max: max.0,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", region3int16_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Region3int16 {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Min", |_, this| Ok(Vector3int16(this.min)));
|
|
||||||
fields.add_field_method_get("Max", |_, this| Ok(Vector3int16(this.max)));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Region3int16 {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", Vector3int16(self.min), Vector3int16(self.max))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomRegion3int16> for Region3int16 {
|
|
||||||
fn from(v: DomRegion3int16) -> Self {
|
|
||||||
Region3int16 {
|
|
||||||
min: Vector3int16::from(v.min).0,
|
|
||||||
max: Vector3int16::from(v.max).0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Region3int16> for DomRegion3int16 {
|
|
||||||
fn from(v: Region3int16) -> Self {
|
|
||||||
DomRegion3int16 {
|
|
||||||
min: Vector3int16(v.min).into(),
|
|
||||||
max: Vector3int16(v.max).into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::UDim as DomUDim;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::super::*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [UDim](https://create.roblox.com/docs/reference/engine/datatypes/UDim) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the UDim class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct UDim {
|
|
||||||
pub(crate) scale: f32,
|
|
||||||
pub(crate) offset: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UDim {
|
|
||||||
pub(super) fn new(scale: f32, offset: i32) -> Self {
|
|
||||||
Self { scale, offset }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for UDim {
|
|
||||||
const EXPORT_NAME: &'static str = "UDim";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let udim_new = |_, (scale, offset): (Option<f32>, Option<i32>)| {
|
|
||||||
Ok(UDim {
|
|
||||||
scale: scale.unwrap_or_default(),
|
|
||||||
offset: offset.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", udim_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for UDim {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Scale", |_, this| Ok(this.scale));
|
|
||||||
fields.add_field_method_get("Offset", |_, this| Ok(this.offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
|
||||||
methods.add_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UDim {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
scale: 0f32,
|
|
||||||
offset: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for UDim {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", self.scale, self.offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Neg for UDim {
|
|
||||||
type Output = Self;
|
|
||||||
fn neg(self) -> Self::Output {
|
|
||||||
UDim {
|
|
||||||
scale: -self.scale,
|
|
||||||
offset: -self.offset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add for UDim {
|
|
||||||
type Output = Self;
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
UDim {
|
|
||||||
scale: self.scale + rhs.scale,
|
|
||||||
offset: self.offset + rhs.offset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub for UDim {
|
|
||||||
type Output = Self;
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
UDim {
|
|
||||||
scale: self.scale - rhs.scale,
|
|
||||||
offset: self.offset - rhs.offset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomUDim> for UDim {
|
|
||||||
fn from(v: DomUDim) -> Self {
|
|
||||||
UDim {
|
|
||||||
scale: v.scale,
|
|
||||||
offset: v.offset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UDim> for DomUDim {
|
|
||||||
fn from(v: UDim) -> Self {
|
|
||||||
DomUDim {
|
|
||||||
scale: v.scale,
|
|
||||||
offset: v.offset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,168 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use glam::Vec2;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::UDim2 as DomUDim2;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::{super::*, UDim};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [UDim2](https://create.roblox.com/docs/reference/engine/datatypes/UDim2) Roblox datatype.
|
|
||||||
|
|
||||||
This implements all documented properties, methods & constructors of the UDim2 class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct UDim2 {
|
|
||||||
pub(crate) x: UDim,
|
|
||||||
pub(crate) y: UDim,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for UDim2 {
|
|
||||||
const EXPORT_NAME: &'static str = "UDim2";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let udim2_from_offset = |_, (x, y): (Option<i32>, Option<i32>)| {
|
|
||||||
Ok(UDim2 {
|
|
||||||
x: UDim::new(0f32, x.unwrap_or_default()),
|
|
||||||
y: UDim::new(0f32, y.unwrap_or_default()),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let udim2_from_scale = |_, (x, y): (Option<f32>, Option<f32>)| {
|
|
||||||
Ok(UDim2 {
|
|
||||||
x: UDim::new(x.unwrap_or_default(), 0),
|
|
||||||
y: UDim::new(y.unwrap_or_default(), 0),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
type ArgsUDims<'lua> = (
|
|
||||||
Option<LuaUserDataRef<'lua, UDim>>,
|
|
||||||
Option<LuaUserDataRef<'lua, UDim>>,
|
|
||||||
);
|
|
||||||
type ArgsNums = (Option<f32>, Option<i32>, Option<f32>, Option<i32>);
|
|
||||||
let udim2_new = |lua, args: LuaMultiValue| {
|
|
||||||
if let Ok((x, y)) = ArgsUDims::from_lua_multi(args.clone(), lua) {
|
|
||||||
Ok(UDim2 {
|
|
||||||
x: x.map(|x| *x).unwrap_or_default(),
|
|
||||||
y: y.map(|y| *y).unwrap_or_default(),
|
|
||||||
})
|
|
||||||
} else if let Ok((sx, ox, sy, oy)) = ArgsNums::from_lua_multi(args, lua) {
|
|
||||||
Ok(UDim2 {
|
|
||||||
x: UDim::new(sx.unwrap_or_default(), ox.unwrap_or_default()),
|
|
||||||
y: UDim::new(sy.unwrap_or_default(), oy.unwrap_or_default()),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// FUTURE: Better error message here using given arg types
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Invalid arguments to constructor".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("fromOffset", udim2_from_offset)?
|
|
||||||
.with_function("fromScale", udim2_from_scale)?
|
|
||||||
.with_function("new", udim2_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for UDim2 {
|
|
||||||
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("Width", |_, this| Ok(this.x));
|
|
||||||
fields.add_field_method_get("Height", |_, this| Ok(this.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
// Methods
|
|
||||||
methods.add_method(
|
|
||||||
"Lerp",
|
|
||||||
|_, this, (goal, alpha): (LuaUserDataRef<UDim2>, f32)| {
|
|
||||||
let this_x = Vec2::new(this.x.scale, this.x.offset as f32);
|
|
||||||
let goal_x = Vec2::new(goal.x.scale, goal.x.offset as f32);
|
|
||||||
|
|
||||||
let this_y = Vec2::new(this.y.scale, this.y.offset as f32);
|
|
||||||
let goal_y = Vec2::new(goal.y.scale, goal.y.offset as f32);
|
|
||||||
|
|
||||||
let x = this_x.lerp(goal_x, alpha);
|
|
||||||
let y = this_y.lerp(goal_y, alpha);
|
|
||||||
|
|
||||||
Ok(UDim2 {
|
|
||||||
x: UDim {
|
|
||||||
scale: x.x,
|
|
||||||
offset: x.y.clamp(i32::MIN as f32, i32::MAX as f32).round() as i32,
|
|
||||||
},
|
|
||||||
y: UDim {
|
|
||||||
scale: y.x,
|
|
||||||
offset: y.y.clamp(i32::MIN as f32, i32::MAX as f32).round() as i32,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for UDim2 {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", self.x, self.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Neg for UDim2 {
|
|
||||||
type Output = Self;
|
|
||||||
fn neg(self) -> Self::Output {
|
|
||||||
UDim2 {
|
|
||||||
x: -self.x,
|
|
||||||
y: -self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add for UDim2 {
|
|
||||||
type Output = Self;
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
UDim2 {
|
|
||||||
x: self.x + rhs.x,
|
|
||||||
y: self.y + rhs.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub for UDim2 {
|
|
||||||
type Output = Self;
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
UDim2 {
|
|
||||||
x: self.x - rhs.x,
|
|
||||||
y: self.y - rhs.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomUDim2> for UDim2 {
|
|
||||||
fn from(v: DomUDim2) -> Self {
|
|
||||||
UDim2 {
|
|
||||||
x: v.x.into(),
|
|
||||||
y: v.y.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UDim2> for DomUDim2 {
|
|
||||||
fn from(v: UDim2) -> Self {
|
|
||||||
DomUDim2 {
|
|
||||||
x: v.x.into(),
|
|
||||||
y: v.y.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,149 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use glam::{Vec2, Vec3};
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Vector2 as DomVector2;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::super::*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Vector2](https://create.roblox.com/docs/reference/engine/datatypes/Vector2)
|
|
||||||
Roblox datatype, backed by [`glam::Vec2`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods &
|
|
||||||
constructors of the Vector2 class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
|
||||||
pub struct Vector2(pub Vec2);
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Vector2 {
|
|
||||||
const EXPORT_NAME: &'static str = "Vector2";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let vector2_new = |_, (x, y): (Option<f32>, Option<f32>)| {
|
|
||||||
Ok(Vector2(Vec2 {
|
|
||||||
x: x.unwrap_or_default(),
|
|
||||||
y: y.unwrap_or_default(),
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_value("xAxis", Vector2(Vec2::X))?
|
|
||||||
.with_value("yAxis", Vector2(Vec2::Y))?
|
|
||||||
.with_value("zero", Vector2(Vec2::ZERO))?
|
|
||||||
.with_value("one", Vector2(Vec2::ONE))?
|
|
||||||
.with_function("new", vector2_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Vector2 {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Magnitude", |_, this| Ok(this.0.length()));
|
|
||||||
fields.add_field_method_get("Unit", |_, this| Ok(Vector2(this.0.normalize())));
|
|
||||||
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
|
|
||||||
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
// Methods
|
|
||||||
methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
|
||||||
let this_v3 = Vec3::new(this.0.x, this.0.y, 0f32);
|
|
||||||
let rhs_v3 = Vec3::new(rhs.0.x, rhs.0.y, 0f32);
|
|
||||||
Ok(this_v3.cross(rhs_v3).z)
|
|
||||||
});
|
|
||||||
methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
|
||||||
Ok(this.0.dot(rhs.0))
|
|
||||||
});
|
|
||||||
methods.add_method(
|
|
||||||
"Lerp",
|
|
||||||
|_, this, (rhs, alpha): (LuaUserDataRef<Vector2>, f32)| {
|
|
||||||
Ok(Vector2(this.0.lerp(rhs.0, alpha)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method("Max", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
|
||||||
Ok(Vector2(this.0.max(rhs.0)))
|
|
||||||
});
|
|
||||||
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
|
||||||
Ok(Vector2(this.0.min(rhs.0)))
|
|
||||||
});
|
|
||||||
// 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 fmt::Display for Vector2 {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", self.0.x, self.0.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Neg for Vector2 {
|
|
||||||
type Output = Self;
|
|
||||||
fn neg(self) -> Self::Output {
|
|
||||||
Vector2(-self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add for Vector2 {
|
|
||||||
type Output = Self;
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Vector2(self.0 + rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub for Vector2 {
|
|
||||||
type Output = Self;
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
Vector2(self.0 - rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul for Vector2 {
|
|
||||||
type Output = Vector2;
|
|
||||||
fn mul(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 * rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul<f32> for Vector2 {
|
|
||||||
type Output = Vector2;
|
|
||||||
fn mul(self, rhs: f32) -> Self::Output {
|
|
||||||
Self(self.0 * rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Div for Vector2 {
|
|
||||||
type Output = Vector2;
|
|
||||||
fn div(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 / rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Div<f32> for Vector2 {
|
|
||||||
type Output = Vector2;
|
|
||||||
fn div(self, rhs: f32) -> Self::Output {
|
|
||||||
Self(self.0 / rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomVector2> for Vector2 {
|
|
||||||
fn from(v: DomVector2) -> Self {
|
|
||||||
Vector2(Vec2 { x: v.x, y: v.y })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vector2> for DomVector2 {
|
|
||||||
fn from(v: Vector2) -> Self {
|
|
||||||
DomVector2 { x: v.0.x, y: v.0.y }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use glam::IVec2;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Vector2int16 as DomVector2int16;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::super::*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Vector2int16](https://create.roblox.com/docs/reference/engine/datatypes/Vector2int16)
|
|
||||||
Roblox datatype, backed by [`glam::IVec2`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods &
|
|
||||||
constructors of the Vector2int16 class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Vector2int16(pub IVec2);
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Vector2int16 {
|
|
||||||
const EXPORT_NAME: &'static str = "Vector2int16";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let vector2int16_new = |_, (x, y): (Option<i16>, Option<i16>)| {
|
|
||||||
Ok(Vector2int16(IVec2 {
|
|
||||||
x: x.unwrap_or_default() as i32,
|
|
||||||
y: y.unwrap_or_default() as i32,
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", vector2int16_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Vector2int16 {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
|
|
||||||
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
|
||||||
methods.add_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_i32);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_i32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Vector2int16 {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", self.0.x, self.0.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Neg for Vector2int16 {
|
|
||||||
type Output = Self;
|
|
||||||
fn neg(self) -> Self::Output {
|
|
||||||
Vector2int16(-self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add for Vector2int16 {
|
|
||||||
type Output = Self;
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Vector2int16(self.0 + rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub for Vector2int16 {
|
|
||||||
type Output = Self;
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
Vector2int16(self.0 - rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul for Vector2int16 {
|
|
||||||
type Output = Vector2int16;
|
|
||||||
fn mul(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 * rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul<i32> for Vector2int16 {
|
|
||||||
type Output = Vector2int16;
|
|
||||||
fn mul(self, rhs: i32) -> Self::Output {
|
|
||||||
Self(self.0 * rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Div for Vector2int16 {
|
|
||||||
type Output = Vector2int16;
|
|
||||||
fn div(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 / rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Div<i32> for Vector2int16 {
|
|
||||||
type Output = Vector2int16;
|
|
||||||
fn div(self, rhs: i32) -> Self::Output {
|
|
||||||
Self(self.0 / rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomVector2int16> for Vector2int16 {
|
|
||||||
fn from(v: DomVector2int16) -> Self {
|
|
||||||
Vector2int16(IVec2 {
|
|
||||||
x: v.x.clamp(i16::MIN, i16::MAX) as i32,
|
|
||||||
y: v.y.clamp(i16::MIN, i16::MAX) as i32,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vector2int16> for DomVector2int16 {
|
|
||||||
fn from(v: Vector2int16) -> Self {
|
|
||||||
DomVector2int16 {
|
|
||||||
x: v.0.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
|
||||||
y: v.0.y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,223 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use glam::Vec3;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Vector3 as DomVector3;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
lune::util::TableBuilder,
|
|
||||||
roblox::{datatypes::util::round_float_decimal, exports::LuaExportsTable},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{super::*, EnumItem};
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Vector3](https://create.roblox.com/docs/reference/engine/datatypes/Vector3)
|
|
||||||
Roblox datatype, backed by [`glam::Vec3`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods &
|
|
||||||
constructors of the Vector3 class as of March 2023.
|
|
||||||
|
|
||||||
Note that this does not use native Luau vectors to simplify implementation
|
|
||||||
and instead allow us to implement all abovementioned APIs accurately.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Vector3(pub Vec3);
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Vector3 {
|
|
||||||
const EXPORT_NAME: &'static str = "Vector3";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let vector3_from_axis = |_, normal_id: LuaUserDataRef<EnumItem>| {
|
|
||||||
if normal_id.parent.desc.name == "Axis" {
|
|
||||||
Ok(match normal_id.name.as_str() {
|
|
||||||
"X" => Vector3(Vec3::X),
|
|
||||||
"Y" => Vector3(Vec3::Y),
|
|
||||||
"Z" => Vector3(Vec3::Z),
|
|
||||||
name => {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"Axis '{}' is not known",
|
|
||||||
name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"EnumItem must be a Axis, got {}",
|
|
||||||
normal_id.parent.desc.name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let vector3_from_normal_id = |_, normal_id: LuaUserDataRef<EnumItem>| {
|
|
||||||
if normal_id.parent.desc.name == "NormalId" {
|
|
||||||
Ok(match normal_id.name.as_str() {
|
|
||||||
"Left" => Vector3(Vec3::X),
|
|
||||||
"Top" => Vector3(Vec3::Y),
|
|
||||||
"Front" => Vector3(-Vec3::Z),
|
|
||||||
"Right" => Vector3(-Vec3::X),
|
|
||||||
"Bottom" => Vector3(-Vec3::Y),
|
|
||||||
"Back" => Vector3(Vec3::Z),
|
|
||||||
name => {
|
|
||||||
return Err(LuaError::RuntimeError(format!(
|
|
||||||
"NormalId '{}' is not known",
|
|
||||||
name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"EnumItem must be a NormalId, got {}",
|
|
||||||
normal_id.parent.desc.name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let vector3_new = |_, (x, y, z): (Option<f32>, Option<f32>, Option<f32>)| {
|
|
||||||
Ok(Vector3(Vec3 {
|
|
||||||
x: x.unwrap_or_default(),
|
|
||||||
y: y.unwrap_or_default(),
|
|
||||||
z: z.unwrap_or_default(),
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_value("xAxis", Vector3(Vec3::X))?
|
|
||||||
.with_value("yAxis", Vector3(Vec3::Y))?
|
|
||||||
.with_value("zAxis", Vector3(Vec3::Z))?
|
|
||||||
.with_value("zero", Vector3(Vec3::ZERO))?
|
|
||||||
.with_value("one", Vector3(Vec3::ONE))?
|
|
||||||
.with_function("fromAxis", vector3_from_axis)?
|
|
||||||
.with_function("fromNormalId", vector3_from_normal_id)?
|
|
||||||
.with_function("new", vector3_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Vector3 {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("Magnitude", |_, this| Ok(this.0.length()));
|
|
||||||
fields.add_field_method_get("Unit", |_, this| Ok(Vector3(this.0.normalize())));
|
|
||||||
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
|
|
||||||
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
|
|
||||||
fields.add_field_method_get("Z", |_, this| Ok(this.0.z));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
// Methods
|
|
||||||
methods.add_method("Angle", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
|
||||||
Ok(this.0.angle_between(rhs.0))
|
|
||||||
});
|
|
||||||
methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
|
||||||
Ok(Vector3(this.0.cross(rhs.0)))
|
|
||||||
});
|
|
||||||
methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
|
||||||
Ok(this.0.dot(rhs.0))
|
|
||||||
});
|
|
||||||
methods.add_method(
|
|
||||||
"FuzzyEq",
|
|
||||||
|_, this, (rhs, epsilon): (LuaUserDataRef<Vector3>, f32)| {
|
|
||||||
let eq_x = (rhs.0.x - this.0.x).abs() <= epsilon;
|
|
||||||
let eq_y = (rhs.0.y - this.0.y).abs() <= epsilon;
|
|
||||||
let eq_z = (rhs.0.z - this.0.z).abs() <= epsilon;
|
|
||||||
Ok(eq_x && eq_y && eq_z)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method(
|
|
||||||
"Lerp",
|
|
||||||
|_, this, (rhs, alpha): (LuaUserDataRef<Vector3>, f32)| {
|
|
||||||
Ok(Vector3(this.0.lerp(rhs.0, alpha)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
methods.add_method("Max", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
|
||||||
Ok(Vector3(this.0.max(rhs.0)))
|
|
||||||
});
|
|
||||||
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
|
||||||
Ok(Vector3(this.0.min(rhs.0)))
|
|
||||||
});
|
|
||||||
// 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 fmt::Display for Vector3 {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}, {}", self.0.x, self.0.y, self.0.z)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Neg for Vector3 {
|
|
||||||
type Output = Self;
|
|
||||||
fn neg(self) -> Self::Output {
|
|
||||||
Vector3(-self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add for Vector3 {
|
|
||||||
type Output = Self;
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Vector3(self.0 + rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub for Vector3 {
|
|
||||||
type Output = Self;
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
Vector3(self.0 - rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul for Vector3 {
|
|
||||||
type Output = Vector3;
|
|
||||||
fn mul(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 * rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul<f32> for Vector3 {
|
|
||||||
type Output = Vector3;
|
|
||||||
fn mul(self, rhs: f32) -> Self::Output {
|
|
||||||
Self(self.0 * rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Div for Vector3 {
|
|
||||||
type Output = Vector3;
|
|
||||||
fn div(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 / rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Div<f32> for Vector3 {
|
|
||||||
type Output = Vector3;
|
|
||||||
fn div(self, rhs: f32) -> Self::Output {
|
|
||||||
Self(self.0 / rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomVector3> for Vector3 {
|
|
||||||
fn from(v: DomVector3) -> Self {
|
|
||||||
Vector3(Vec3 {
|
|
||||||
x: v.x,
|
|
||||||
y: v.y,
|
|
||||||
z: v.z,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vector3> for DomVector3 {
|
|
||||||
fn from(v: Vector3) -> Self {
|
|
||||||
DomVector3 {
|
|
||||||
x: round_float_decimal(v.0.x),
|
|
||||||
y: round_float_decimal(v.0.y),
|
|
||||||
z: round_float_decimal(v.0.z),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
use core::fmt;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use glam::IVec3;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use rbx_dom_weak::types::Vector3int16 as DomVector3int16;
|
|
||||||
|
|
||||||
use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
|
|
||||||
|
|
||||||
use super::super::*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
An implementation of the [Vector3int16](https://create.roblox.com/docs/reference/engine/datatypes/Vector3int16)
|
|
||||||
Roblox datatype, backed by [`glam::IVec3`].
|
|
||||||
|
|
||||||
This implements all documented properties, methods &
|
|
||||||
constructors of the Vector3int16 class as of March 2023.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct Vector3int16(pub IVec3);
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Vector3int16 {
|
|
||||||
const EXPORT_NAME: &'static str = "Vector3int16";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let vector3int16_new = |_, (x, y, z): (Option<i16>, Option<i16>, Option<i16>)| {
|
|
||||||
Ok(Vector3int16(IVec3 {
|
|
||||||
x: x.unwrap_or_default() as i32,
|
|
||||||
y: y.unwrap_or_default() as i32,
|
|
||||||
z: z.unwrap_or_default() as i32,
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", vector3int16_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for Vector3int16 {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
|
|
||||||
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
|
|
||||||
fields.add_field_method_get("Z", |_, this| Ok(this.0.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);
|
|
||||||
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_i32);
|
|
||||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_i32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Vector3int16 {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}, {}", self.0.x, self.0.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Neg for Vector3int16 {
|
|
||||||
type Output = Self;
|
|
||||||
fn neg(self) -> Self::Output {
|
|
||||||
Vector3int16(-self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add for Vector3int16 {
|
|
||||||
type Output = Self;
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Vector3int16(self.0 + rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub for Vector3int16 {
|
|
||||||
type Output = Self;
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
Vector3int16(self.0 - rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul for Vector3int16 {
|
|
||||||
type Output = Vector3int16;
|
|
||||||
fn mul(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 * rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Mul<i32> for Vector3int16 {
|
|
||||||
type Output = Vector3int16;
|
|
||||||
fn mul(self, rhs: i32) -> Self::Output {
|
|
||||||
Self(self.0 * rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Div for Vector3int16 {
|
|
||||||
type Output = Vector3int16;
|
|
||||||
fn div(self, rhs: Self) -> Self::Output {
|
|
||||||
Self(self.0 / rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Div<i32> for Vector3int16 {
|
|
||||||
type Output = Vector3int16;
|
|
||||||
fn div(self, rhs: i32) -> Self::Output {
|
|
||||||
Self(self.0 / rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DomVector3int16> for Vector3int16 {
|
|
||||||
fn from(v: DomVector3int16) -> Self {
|
|
||||||
Vector3int16(IVec3 {
|
|
||||||
x: v.x.clamp(i16::MIN, i16::MAX) as i32,
|
|
||||||
y: v.y.clamp(i16::MIN, i16::MAX) as i32,
|
|
||||||
z: v.z.clamp(i16::MIN, i16::MAX) as i32,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vector3int16> for DomVector3int16 {
|
|
||||||
fn from(v: Vector3int16) -> Self {
|
|
||||||
DomVector3int16 {
|
|
||||||
x: v.0.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
|
||||||
y: v.0.y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
|
||||||
z: v.0.z.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
// HACK: We round to the nearest Very Small Decimal
|
|
||||||
// to reduce writing out floating point accumulation
|
|
||||||
// errors to files (mostly relevant for xml formats)
|
|
||||||
const ROUNDING: usize = 65_536; // 2 ^ 16
|
|
||||||
|
|
||||||
pub fn round_float_decimal(value: f32) -> f32 {
|
|
||||||
let place = ROUNDING as f32;
|
|
||||||
|
|
||||||
// Round only the fractional part, we do not want to
|
|
||||||
// lose any float precision in case a user for some
|
|
||||||
// reason has very very large float numbers in files
|
|
||||||
let whole = value.trunc();
|
|
||||||
let fract = (value.fract() * place).round() / place;
|
|
||||||
|
|
||||||
whole + fract
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)]
|
|
||||||
pub enum DocumentError {
|
|
||||||
#[error("Unknown document kind")]
|
|
||||||
UnknownKind,
|
|
||||||
#[error("Unknown document format")]
|
|
||||||
UnknownFormat,
|
|
||||||
#[error("Failed to read document from buffer - {0}")]
|
|
||||||
ReadError(String),
|
|
||||||
#[error("Failed to write document to buffer - {0}")]
|
|
||||||
WriteError(String),
|
|
||||||
#[error("Failed to convert into a DataModel - the given document is not a place")]
|
|
||||||
IntoDataModelInvalidArgs,
|
|
||||||
#[error("Failed to convert into array of Instances - the given document is a model")]
|
|
||||||
IntoInstanceArrayInvalidArgs,
|
|
||||||
#[error("Failed to convert into a place - the given instance is not a DataModel")]
|
|
||||||
FromDataModelInvalidArgs,
|
|
||||||
#[error("Failed to convert into a model - a given instance is a DataModel")]
|
|
||||||
FromInstanceArrayInvalidArgs,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DocumentError> for LuaError {
|
|
||||||
fn from(value: DocumentError) -> Self {
|
|
||||||
Self::RuntimeError(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,202 +0,0 @@
|
||||||
// Original implementation from Remodel:
|
|
||||||
// https://github.com/rojo-rbx/remodel/blob/master/src/sniff_type.rs
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A document format specifier.
|
|
||||||
|
|
||||||
Valid variants are the following:
|
|
||||||
|
|
||||||
- `Binary`
|
|
||||||
- `Xml`
|
|
||||||
|
|
||||||
Other variants are only to be used for logic internal to this crate.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
|
||||||
pub enum DocumentFormat {
|
|
||||||
Binary,
|
|
||||||
Xml,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DocumentFormat {
|
|
||||||
/**
|
|
||||||
Try to convert a file extension into a valid document format specifier.
|
|
||||||
|
|
||||||
Returns `None` if the file extension is not a canonical roblox file format extension.
|
|
||||||
*/
|
|
||||||
pub fn from_extension(extension: impl AsRef<str>) -> Option<Self> {
|
|
||||||
match extension.as_ref() {
|
|
||||||
"rbxl" | "rbxm" => Some(Self::Binary),
|
|
||||||
"rbxlx" | "rbxmx" => Some(Self::Xml),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Try to convert a file path into a valid document format specifier.
|
|
||||||
|
|
||||||
Returns `None` if the file extension of the path
|
|
||||||
is not a canonical roblox file format extension.
|
|
||||||
*/
|
|
||||||
pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
|
|
||||||
match path
|
|
||||||
.as_ref()
|
|
||||||
.extension()
|
|
||||||
.map(|ext| ext.to_string_lossy())
|
|
||||||
.as_deref()
|
|
||||||
{
|
|
||||||
Some("rbxl") | Some("rbxm") => Some(Self::Binary),
|
|
||||||
Some("rbxlx") | Some("rbxmx") => Some(Self::Xml),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Try to detect a document format specifier from file contents.
|
|
||||||
|
|
||||||
Returns `None` if the file contents do not seem to be from a valid roblox file.
|
|
||||||
*/
|
|
||||||
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Option<Self> {
|
|
||||||
let header = bytes.as_ref().get(0..8)?;
|
|
||||||
|
|
||||||
if header.starts_with(b"<roblox") {
|
|
||||||
match header[7] {
|
|
||||||
b'!' => Some(Self::Binary),
|
|
||||||
b' ' | b'>' => Some(Self::Xml),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DocumentFormat {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Binary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_extension_binary() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_extension("rbxl"),
|
|
||||||
Some(DocumentFormat::Binary)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_extension("rbxm"),
|
|
||||||
Some(DocumentFormat::Binary)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_extension_xml() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_extension("rbxlx"),
|
|
||||||
Some(DocumentFormat::Xml)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_extension("rbxmx"),
|
|
||||||
Some(DocumentFormat::Xml)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_extension_invalid() {
|
|
||||||
assert_eq!(DocumentFormat::from_extension("csv"), None);
|
|
||||||
assert_eq!(DocumentFormat::from_extension("json"), None);
|
|
||||||
assert_eq!(DocumentFormat::from_extension("rbx"), None);
|
|
||||||
assert_eq!(DocumentFormat::from_extension("rbxn"), None);
|
|
||||||
assert_eq!(DocumentFormat::from_extension("xlx"), None);
|
|
||||||
assert_eq!(DocumentFormat::from_extension("xmx"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_path_binary() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_path(PathBuf::from("model.rbxl")),
|
|
||||||
Some(DocumentFormat::Binary)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_path(PathBuf::from("model.rbxm")),
|
|
||||||
Some(DocumentFormat::Binary)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_path_xml() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_path(PathBuf::from("place.rbxlx")),
|
|
||||||
Some(DocumentFormat::Xml)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_path(PathBuf::from("place.rbxmx")),
|
|
||||||
Some(DocumentFormat::Xml)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_path_invalid() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_path(PathBuf::from("data-file.csv")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_path(PathBuf::from("nested/path/file.json")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_path(PathBuf::from(".no-name-strange-rbx")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_path(PathBuf::from("file_without_extension")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_bytes_binary() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_bytes(b"<roblox!hello"),
|
|
||||||
Some(DocumentFormat::Binary)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_bytes(b"<roblox!"),
|
|
||||||
Some(DocumentFormat::Binary)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_bytes_xml() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_bytes(b"<roblox xml:someschemajunk>"),
|
|
||||||
Some(DocumentFormat::Xml)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentFormat::from_bytes(b"<roblox>"),
|
|
||||||
Some(DocumentFormat::Xml)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_bytes_invalid() {
|
|
||||||
assert_eq!(DocumentFormat::from_bytes(b""), None);
|
|
||||||
assert_eq!(DocumentFormat::from_bytes(b" roblox"), None);
|
|
||||||
assert_eq!(DocumentFormat::from_bytes(b"<roblox"), None);
|
|
||||||
assert_eq!(DocumentFormat::from_bytes(b"<roblox-"), None);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,208 +0,0 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use rbx_dom_weak::WeakDom;
|
|
||||||
|
|
||||||
use crate::roblox::shared::instance::class_is_a_service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A document kind specifier.
|
|
||||||
|
|
||||||
Valid variants are the following:
|
|
||||||
|
|
||||||
- `Model`
|
|
||||||
- `Place`
|
|
||||||
|
|
||||||
Other variants are only to be used for logic internal to this crate.
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
|
||||||
pub enum DocumentKind {
|
|
||||||
Place,
|
|
||||||
Model,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DocumentKind {
|
|
||||||
/**
|
|
||||||
Try to convert a file extension into a valid document kind specifier.
|
|
||||||
|
|
||||||
Returns `None` if the file extension is not a canonical roblox file format extension.
|
|
||||||
*/
|
|
||||||
pub fn from_extension(extension: impl AsRef<str>) -> Option<Self> {
|
|
||||||
match extension.as_ref() {
|
|
||||||
"rbxl" | "rbxlx" => Some(Self::Place),
|
|
||||||
"rbxm" | "rbxmx" => Some(Self::Model),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Try to convert a file path into a valid document kind specifier.
|
|
||||||
|
|
||||||
Returns `None` if the file extension of the path
|
|
||||||
is not a canonical roblox file format extension.
|
|
||||||
*/
|
|
||||||
pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
|
|
||||||
match path
|
|
||||||
.as_ref()
|
|
||||||
.extension()
|
|
||||||
.map(|ext| ext.to_string_lossy())
|
|
||||||
.as_deref()
|
|
||||||
{
|
|
||||||
Some("rbxl") | Some("rbxlx") => Some(Self::Place),
|
|
||||||
Some("rbxm") | Some("rbxmx") => Some(Self::Model),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Try to detect a document kind specifier from a weak dom.
|
|
||||||
|
|
||||||
Returns `None` if the given dom is empty and as such can not have its kind inferred.
|
|
||||||
*/
|
|
||||||
pub fn from_weak_dom(dom: &WeakDom) -> Option<Self> {
|
|
||||||
let mut has_top_level_child = false;
|
|
||||||
let mut has_top_level_service = false;
|
|
||||||
for child_ref in dom.root().children() {
|
|
||||||
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
|
|
||||||
has_top_level_child = true;
|
|
||||||
if class_is_a_service(&child_inst.class).unwrap_or(false) {
|
|
||||||
has_top_level_service = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if has_top_level_service {
|
|
||||||
Some(Self::Place)
|
|
||||||
} else if has_top_level_child {
|
|
||||||
Some(Self::Model)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use rbx_dom_weak::InstanceBuilder;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_extension_place() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_extension("rbxl"),
|
|
||||||
Some(DocumentKind::Place)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_extension("rbxlx"),
|
|
||||||
Some(DocumentKind::Place)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_extension_model() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_extension("rbxm"),
|
|
||||||
Some(DocumentKind::Model)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_extension("rbxmx"),
|
|
||||||
Some(DocumentKind::Model)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_extension_invalid() {
|
|
||||||
assert_eq!(DocumentKind::from_extension("csv"), None);
|
|
||||||
assert_eq!(DocumentKind::from_extension("json"), None);
|
|
||||||
assert_eq!(DocumentKind::from_extension("rbx"), None);
|
|
||||||
assert_eq!(DocumentKind::from_extension("rbxn"), None);
|
|
||||||
assert_eq!(DocumentKind::from_extension("xlx"), None);
|
|
||||||
assert_eq!(DocumentKind::from_extension("xmx"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_path_place() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_path(PathBuf::from("place.rbxl")),
|
|
||||||
Some(DocumentKind::Place)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_path(PathBuf::from("place.rbxlx")),
|
|
||||||
Some(DocumentKind::Place)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_path_model() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_path(PathBuf::from("model.rbxm")),
|
|
||||||
Some(DocumentKind::Model)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_path(PathBuf::from("model.rbxmx")),
|
|
||||||
Some(DocumentKind::Model)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_path_invalid() {
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_path(PathBuf::from("data-file.csv")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_path(PathBuf::from("nested/path/file.json")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_path(PathBuf::from(".no-name-strange-rbx")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_path(PathBuf::from("file_without_extension")),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_weak_dom() {
|
|
||||||
let empty = WeakDom::new(InstanceBuilder::new("Instance"));
|
|
||||||
assert_eq!(DocumentKind::from_weak_dom(&empty), None);
|
|
||||||
|
|
||||||
let with_services = WeakDom::new(
|
|
||||||
InstanceBuilder::new("Instance")
|
|
||||||
.with_child(InstanceBuilder::new("Workspace"))
|
|
||||||
.with_child(InstanceBuilder::new("ReplicatedStorage")),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_weak_dom(&with_services),
|
|
||||||
Some(DocumentKind::Place)
|
|
||||||
);
|
|
||||||
|
|
||||||
let with_children = WeakDom::new(
|
|
||||||
InstanceBuilder::new("Instance")
|
|
||||||
.with_child(InstanceBuilder::new("Model"))
|
|
||||||
.with_child(InstanceBuilder::new("Part")),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_weak_dom(&with_children),
|
|
||||||
Some(DocumentKind::Model)
|
|
||||||
);
|
|
||||||
|
|
||||||
let with_mixed = WeakDom::new(
|
|
||||||
InstanceBuilder::new("Instance")
|
|
||||||
.with_child(InstanceBuilder::new("Workspace"))
|
|
||||||
.with_child(InstanceBuilder::new("Part")),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
DocumentKind::from_weak_dom(&with_mixed),
|
|
||||||
Some(DocumentKind::Place)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,290 +0,0 @@
|
||||||
use rbx_dom_weak::{types::Ref as DomRef, InstanceBuilder as DomInstanceBuilder, WeakDom};
|
|
||||||
use rbx_xml::{
|
|
||||||
DecodeOptions as XmlDecodeOptions, DecodePropertyBehavior as XmlDecodePropertyBehavior,
|
|
||||||
EncodeOptions as XmlEncodeOptions, EncodePropertyBehavior as XmlEncodePropertyBehavior,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod error;
|
|
||||||
mod format;
|
|
||||||
mod kind;
|
|
||||||
mod postprocessing;
|
|
||||||
|
|
||||||
pub use error::*;
|
|
||||||
pub use format::*;
|
|
||||||
pub use kind::*;
|
|
||||||
|
|
||||||
use postprocessing::*;
|
|
||||||
|
|
||||||
use crate::roblox::instance::{data_model, Instance};
|
|
||||||
|
|
||||||
pub type DocumentResult<T> = Result<T, DocumentError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
A container for [`rbx_dom_weak::WeakDom`] that also takes care of
|
|
||||||
reading and writing different kinds and formats of roblox files.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Code Sample #1
|
|
||||||
|
|
||||||
```rust ignore
|
|
||||||
// Reading a document from a file
|
|
||||||
|
|
||||||
let file_path = PathBuf::from("place-file.rbxl");
|
|
||||||
let file_contents = std::fs::read(&file_path)?;
|
|
||||||
|
|
||||||
let document = Document::from_bytes_auto(file_contents)?;
|
|
||||||
|
|
||||||
// Writing a document to a file
|
|
||||||
|
|
||||||
let file_path = PathBuf::from("place-file")
|
|
||||||
.with_extension(document.extension()?);
|
|
||||||
|
|
||||||
std::fs::write(&file_path, document.to_bytes()?)?;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Code Sample #2
|
|
||||||
|
|
||||||
```rust ignore
|
|
||||||
// Converting a Document to a DataModel or model child instances
|
|
||||||
let data_model = document.into_data_model_instance()?;
|
|
||||||
|
|
||||||
let model_children = document.into_instance_array()?;
|
|
||||||
|
|
||||||
// Converting a DataModel or model child instances into a Document
|
|
||||||
let place_doc = Document::from_data_model_instance(data_model)?;
|
|
||||||
|
|
||||||
let model_doc = Document::from_instance_array(model_children)?;
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Document {
|
|
||||||
kind: DocumentKind,
|
|
||||||
format: DocumentFormat,
|
|
||||||
dom: WeakDom,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Document {
|
|
||||||
/**
|
|
||||||
Gets the canonical file extension for a given kind and
|
|
||||||
format of document, which will follow this chart:
|
|
||||||
|
|
||||||
| Kind | Format | Extension |
|
|
||||||
|:------|:-------|:----------|
|
|
||||||
| Place | Binary | `rbxl` |
|
|
||||||
| Place | Xml | `rbxlx` |
|
|
||||||
| Model | Binary | `rbxm` |
|
|
||||||
| Model | Xml | `rbxmx` |
|
|
||||||
*/
|
|
||||||
#[rustfmt::skip]
|
|
||||||
pub fn canonical_extension(kind: DocumentKind, format: DocumentFormat) -> &'static str {
|
|
||||||
match (kind, format) {
|
|
||||||
(DocumentKind::Place, DocumentFormat::Binary) => "rbxl",
|
|
||||||
(DocumentKind::Place, DocumentFormat::Xml) => "rbxlx",
|
|
||||||
(DocumentKind::Model, DocumentFormat::Binary) => "rbxm",
|
|
||||||
(DocumentKind::Model, DocumentFormat::Xml) => "rbxmx",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_bytes_inner(bytes: impl AsRef<[u8]>) -> DocumentResult<(DocumentFormat, WeakDom)> {
|
|
||||||
let bytes = bytes.as_ref();
|
|
||||||
let format = DocumentFormat::from_bytes(bytes).ok_or(DocumentError::UnknownFormat)?;
|
|
||||||
let dom = match format {
|
|
||||||
DocumentFormat::Binary => rbx_binary::from_reader(bytes)
|
|
||||||
.map_err(|err| DocumentError::ReadError(err.to_string())),
|
|
||||||
DocumentFormat::Xml => {
|
|
||||||
let xml_options = XmlDecodeOptions::new()
|
|
||||||
.property_behavior(XmlDecodePropertyBehavior::ReadUnknown);
|
|
||||||
rbx_xml::from_reader(bytes, xml_options)
|
|
||||||
.map_err(|err| DocumentError::ReadError(err.to_string()))
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
Ok((format, dom))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Decodes and creates a new document from a byte buffer.
|
|
||||||
|
|
||||||
This will automatically handle and detect if the document should be decoded
|
|
||||||
using a roblox binary or roblox xml format, and if it is a model or place file.
|
|
||||||
|
|
||||||
Note that detection of model vs place file is heavily dependent on the structure
|
|
||||||
of the file, and a model file with services in it will detect as a place file, so
|
|
||||||
if possible using [`Document::from_bytes`] with an explicit kind should be preferred.
|
|
||||||
*/
|
|
||||||
pub fn from_bytes_auto(bytes: impl AsRef<[u8]>) -> DocumentResult<Self> {
|
|
||||||
let (format, dom) = Self::from_bytes_inner(bytes)?;
|
|
||||||
let kind = DocumentKind::from_weak_dom(&dom).ok_or(DocumentError::UnknownKind)?;
|
|
||||||
Ok(Self { kind, format, dom })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Decodes and creates a new document from a byte buffer.
|
|
||||||
|
|
||||||
This will automatically handle and detect if the document
|
|
||||||
should be decoded using a roblox binary or roblox xml format.
|
|
||||||
*/
|
|
||||||
pub fn from_bytes(bytes: impl AsRef<[u8]>, kind: DocumentKind) -> DocumentResult<Self> {
|
|
||||||
let (format, dom) = Self::from_bytes_inner(bytes)?;
|
|
||||||
Ok(Self { kind, format, dom })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Encodes the document as a vector of bytes, to
|
|
||||||
be written to a file or sent over the network.
|
|
||||||
|
|
||||||
This will use the same format that the document was created
|
|
||||||
with, meaning if the document is a binary document the output
|
|
||||||
will be binary, and vice versa for xml and other future formats.
|
|
||||||
*/
|
|
||||||
pub fn to_bytes(&self) -> DocumentResult<Vec<u8>> {
|
|
||||||
self.to_bytes_with_format(self.format)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Encodes the document as a vector of bytes, to
|
|
||||||
be written to a file or sent over the network.
|
|
||||||
*/
|
|
||||||
pub fn to_bytes_with_format(&self, format: DocumentFormat) -> DocumentResult<Vec<u8>> {
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
match format {
|
|
||||||
DocumentFormat::Binary => {
|
|
||||||
rbx_binary::to_writer(&mut bytes, &self.dom, self.dom.root().children())
|
|
||||||
.map_err(|err| DocumentError::WriteError(err.to_string()))
|
|
||||||
}
|
|
||||||
DocumentFormat::Xml => {
|
|
||||||
let xml_options = XmlEncodeOptions::new()
|
|
||||||
.property_behavior(XmlEncodePropertyBehavior::WriteUnknown);
|
|
||||||
rbx_xml::to_writer(
|
|
||||||
&mut bytes,
|
|
||||||
&self.dom,
|
|
||||||
self.dom.root().children(),
|
|
||||||
xml_options,
|
|
||||||
)
|
|
||||||
.map_err(|err| DocumentError::WriteError(err.to_string()))
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
Ok(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets the kind this document was created with.
|
|
||||||
*/
|
|
||||||
pub fn kind(&self) -> DocumentKind {
|
|
||||||
self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets the format this document was created with.
|
|
||||||
*/
|
|
||||||
pub fn format(&self) -> DocumentFormat {
|
|
||||||
self.format
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets the file extension for this document.
|
|
||||||
*/
|
|
||||||
pub fn extension(&self) -> &'static str {
|
|
||||||
Self::canonical_extension(self.kind, self.format)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates a DataModel instance out of this place document.
|
|
||||||
|
|
||||||
Will error if the document is not a place.
|
|
||||||
*/
|
|
||||||
pub fn into_data_model_instance(mut self) -> DocumentResult<Instance> {
|
|
||||||
if self.kind != DocumentKind::Place {
|
|
||||||
return Err(DocumentError::IntoDataModelInvalidArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dom_root = self.dom.root_ref();
|
|
||||||
|
|
||||||
let data_model_ref = self
|
|
||||||
.dom
|
|
||||||
.insert(dom_root, DomInstanceBuilder::new(data_model::CLASS_NAME));
|
|
||||||
let data_model_child_refs = self.dom.root().children().to_vec();
|
|
||||||
|
|
||||||
for child_ref in data_model_child_refs {
|
|
||||||
if child_ref != data_model_ref {
|
|
||||||
self.dom.transfer_within(child_ref, data_model_ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Instance::from_external_dom(&mut self.dom, data_model_ref))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates an array of instances out of this model document.
|
|
||||||
|
|
||||||
Will error if the document is not a model.
|
|
||||||
*/
|
|
||||||
pub fn into_instance_array(mut self) -> DocumentResult<Vec<Instance>> {
|
|
||||||
if self.kind != DocumentKind::Model {
|
|
||||||
return Err(DocumentError::IntoInstanceArrayInvalidArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dom_child_refs = self.dom.root().children().to_vec();
|
|
||||||
|
|
||||||
let root_child_instances = dom_child_refs
|
|
||||||
.into_iter()
|
|
||||||
.map(|child_ref| Instance::from_external_dom(&mut self.dom, child_ref))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(root_child_instances)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates a place document out of a DataModel instance.
|
|
||||||
|
|
||||||
Will error if the instance is not a DataModel.
|
|
||||||
*/
|
|
||||||
pub fn from_data_model_instance(i: Instance) -> DocumentResult<Self> {
|
|
||||||
if i.get_class_name() != data_model::CLASS_NAME {
|
|
||||||
return Err(DocumentError::FromDataModelInvalidArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = WeakDom::new(DomInstanceBuilder::new("ROOT"));
|
|
||||||
let children: Vec<DomRef> = i
|
|
||||||
.get_children()
|
|
||||||
.iter()
|
|
||||||
.map(|instance| instance.dom_ref)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Instance::clone_multiple_into_external_dom(&children, &mut dom);
|
|
||||||
postprocess_dom_for_place(&mut dom);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
kind: DocumentKind::Place,
|
|
||||||
format: DocumentFormat::default(),
|
|
||||||
dom,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates a model document out of an array of instances.
|
|
||||||
|
|
||||||
Will error if any of the instances is a DataModel.
|
|
||||||
*/
|
|
||||||
pub fn from_instance_array(v: Vec<Instance>) -> DocumentResult<Self> {
|
|
||||||
for i in &v {
|
|
||||||
if i.get_class_name() == data_model::CLASS_NAME {
|
|
||||||
return Err(DocumentError::FromInstanceArrayInvalidArgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = WeakDom::new(DomInstanceBuilder::new("ROOT"));
|
|
||||||
let instances: Vec<DomRef> = v.iter().map(|instance| instance.dom_ref).collect();
|
|
||||||
|
|
||||||
Instance::clone_multiple_into_external_dom(&instances, &mut dom);
|
|
||||||
postprocess_dom_for_model(&mut dom);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
kind: DocumentKind::Model,
|
|
||||||
format: DocumentFormat::default(),
|
|
||||||
dom,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
use rbx_dom_weak::{
|
|
||||||
types::{Ref as DomRef, VariantType as DomType},
|
|
||||||
Instance as DomInstance, WeakDom,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::roblox::shared::instance::class_is_a;
|
|
||||||
|
|
||||||
pub fn postprocess_dom_for_place(_dom: &mut WeakDom) {
|
|
||||||
// Nothing here yet
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
|
|
||||||
let root_ref = dom.root_ref();
|
|
||||||
recurse_instances(dom, root_ref, &|inst| {
|
|
||||||
// Get rid of some unique ids - roblox does not
|
|
||||||
// save these in model files, and we shouldn't either
|
|
||||||
remove_matching_prop(inst, DomType::UniqueId, "UniqueId");
|
|
||||||
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
|
|
||||||
// Similar story with ScriptGuid - this is used
|
|
||||||
// in the studio-only cloud script drafts feature
|
|
||||||
if class_is_a(&inst.class, "LuaSourceContainer").unwrap_or(false) {
|
|
||||||
inst.properties.remove("ScriptGuid");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recurse_instances<F>(dom: &mut WeakDom, dom_ref: DomRef, f: &F)
|
|
||||||
where
|
|
||||||
F: Fn(&mut DomInstance) + 'static,
|
|
||||||
{
|
|
||||||
let child_refs = match dom.get_by_ref_mut(dom_ref) {
|
|
||||||
Some(inst) => {
|
|
||||||
f(inst);
|
|
||||||
inst.children().to_vec()
|
|
||||||
}
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
for child_ref in child_refs {
|
|
||||||
recurse_instances(dom, child_ref, f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
|
|
||||||
if inst.properties.get(name).map_or(false, |u| u.ty() == ty) {
|
|
||||||
inst.properties.remove(name);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Trait for any item that should be exported as part of the `roblox` built-in library.
|
|
||||||
|
|
||||||
This may be an enum or a struct that should export constants and/or constructs.
|
|
||||||
|
|
||||||
### Example usage
|
|
||||||
|
|
||||||
```rs
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
struct MyType(usize);
|
|
||||||
|
|
||||||
impl MyType {
|
|
||||||
pub fn new(n: usize) -> Self {
|
|
||||||
Self(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for MyType {
|
|
||||||
const EXPORT_NAME: &'static str = "MyType";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let my_type_new = |lua, n: Option<usize>| {
|
|
||||||
Self::new(n.unwrap_or_default())
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", my_type_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaUserData for MyType {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
pub trait LuaExportsTable<'lua> {
|
|
||||||
const EXPORT_NAME: &'static str;
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &'lua Lua) -> LuaResult<LuaTable<'lua>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Exports a single item that implements the [`LuaExportsTable`] trait.
|
|
||||||
|
|
||||||
Returns the name of the export, as well as the export table.
|
|
||||||
|
|
||||||
### Example usage
|
|
||||||
|
|
||||||
```rs
|
|
||||||
let lua: mlua::Lua::new();
|
|
||||||
|
|
||||||
let (name1, table1) = export::<Type1>(lua)?;
|
|
||||||
let (name2, table2) = export::<Type2>(lua)?;
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
pub fn export<'lua, T>(lua: &'lua Lua) -> LuaResult<(&'static str, LuaValue<'lua>)>
|
|
||||||
where
|
|
||||||
T: LuaExportsTable<'lua>,
|
|
||||||
{
|
|
||||||
Ok((
|
|
||||||
T::EXPORT_NAME,
|
|
||||||
<T as LuaExportsTable>::create_exports_table(lua)?.into_lua(lua)?,
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -1,362 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use rbx_dom_weak::{
|
|
||||||
types::{Variant as DomValue, VariantType as DomType},
|
|
||||||
Instance as DomInstance,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::roblox::{
|
|
||||||
datatypes::{
|
|
||||||
attributes::{ensure_valid_attribute_name, ensure_valid_attribute_value},
|
|
||||||
conversion::{DomValueToLua, LuaToDomValue},
|
|
||||||
types::EnumItem,
|
|
||||||
userdata_impl_eq, userdata_impl_to_string,
|
|
||||||
},
|
|
||||||
shared::instance::{class_is_a, find_property_info},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{data_model, registry::InstanceRegistry, Instance};
|
|
||||||
|
|
||||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|
||||||
m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
userdata_impl_to_string(lua, this, ())
|
|
||||||
});
|
|
||||||
m.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
|
||||||
m.add_meta_method(LuaMetaMethod::Index, instance_property_get);
|
|
||||||
m.add_meta_method_mut(LuaMetaMethod::NewIndex, instance_property_set);
|
|
||||||
m.add_method("Clone", |lua, this, ()| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.clone_instance().into_lua(lua)
|
|
||||||
});
|
|
||||||
m.add_method_mut("Destroy", |_, this, ()| {
|
|
||||||
this.destroy();
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
m.add_method_mut("ClearAllChildren", |_, this, ()| {
|
|
||||||
this.clear_all_children();
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
m.add_method("GetChildren", |lua, this, ()| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.get_children().into_lua(lua)
|
|
||||||
});
|
|
||||||
m.add_method("GetDescendants", |lua, this, ()| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.get_descendants().into_lua(lua)
|
|
||||||
});
|
|
||||||
m.add_method("GetFullName", |lua, this, ()| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.get_full_name().into_lua(lua)
|
|
||||||
});
|
|
||||||
m.add_method("GetDebugId", |lua, this, ()| {
|
|
||||||
this.dom_ref.to_string().into_lua(lua)
|
|
||||||
});
|
|
||||||
m.add_method("FindFirstAncestor", |lua, this, name: String| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.find_ancestor(|child| child.name == name).into_lua(lua)
|
|
||||||
});
|
|
||||||
m.add_method(
|
|
||||||
"FindFirstAncestorOfClass",
|
|
||||||
|lua, this, class_name: String| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.find_ancestor(|child| child.class == class_name)
|
|
||||||
.into_lua(lua)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.add_method(
|
|
||||||
"FindFirstAncestorWhichIsA",
|
|
||||||
|lua, this, class_name: String| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.find_ancestor(|child| class_is_a(&child.class, &class_name).unwrap_or(false))
|
|
||||||
.into_lua(lua)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.add_method(
|
|
||||||
"FindFirstChild",
|
|
||||||
|lua, this, (name, recursive): (String, Option<bool>)| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
let predicate = |child: &DomInstance| child.name == name;
|
|
||||||
if matches!(recursive, Some(true)) {
|
|
||||||
this.find_descendant(predicate).into_lua(lua)
|
|
||||||
} else {
|
|
||||||
this.find_child(predicate).into_lua(lua)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.add_method(
|
|
||||||
"FindFirstChildOfClass",
|
|
||||||
|lua, this, (class_name, recursive): (String, Option<bool>)| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
let predicate = |child: &DomInstance| child.class == class_name;
|
|
||||||
if matches!(recursive, Some(true)) {
|
|
||||||
this.find_descendant(predicate).into_lua(lua)
|
|
||||||
} else {
|
|
||||||
this.find_child(predicate).into_lua(lua)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.add_method(
|
|
||||||
"FindFirstChildWhichIsA",
|
|
||||||
|lua, this, (class_name, recursive): (String, Option<bool>)| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
let predicate =
|
|
||||||
|child: &DomInstance| class_is_a(&child.class, &class_name).unwrap_or(false);
|
|
||||||
if matches!(recursive, Some(true)) {
|
|
||||||
this.find_descendant(predicate).into_lua(lua)
|
|
||||||
} else {
|
|
||||||
this.find_child(predicate).into_lua(lua)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.add_method("IsA", |_, this, class_name: String| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
Ok(class_is_a(&this.class_name, class_name).unwrap_or(false))
|
|
||||||
});
|
|
||||||
m.add_method(
|
|
||||||
"IsAncestorOf",
|
|
||||||
|_, this, instance: LuaUserDataRef<Instance>| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
Ok(instance
|
|
||||||
.find_ancestor(|ancestor| ancestor.referent() == this.dom_ref)
|
|
||||||
.is_some())
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.add_method(
|
|
||||||
"IsDescendantOf",
|
|
||||||
|_, this, instance: LuaUserDataRef<Instance>| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
Ok(this
|
|
||||||
.find_ancestor(|ancestor| ancestor.referent() == instance.dom_ref)
|
|
||||||
.is_some())
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.add_method("GetAttribute", |lua, this, name: String| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
match this.get_attribute(name) {
|
|
||||||
Some(attribute) => Ok(LuaValue::dom_value_to_lua(lua, &attribute)?),
|
|
||||||
None => Ok(LuaValue::Nil),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
m.add_method("GetAttributes", |lua, this, ()| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
let attributes = this.get_attributes();
|
|
||||||
let tab = lua.create_table_with_capacity(0, attributes.len())?;
|
|
||||||
for (key, value) in attributes.into_iter() {
|
|
||||||
tab.set(key, LuaValue::dom_value_to_lua(lua, &value)?)?;
|
|
||||||
}
|
|
||||||
Ok(tab)
|
|
||||||
});
|
|
||||||
m.add_method(
|
|
||||||
"SetAttribute",
|
|
||||||
|lua, this, (attribute_name, lua_value): (String, LuaValue)| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
ensure_valid_attribute_name(&attribute_name)?;
|
|
||||||
match lua_value.lua_to_dom_value(lua, None) {
|
|
||||||
Ok(dom_value) => {
|
|
||||||
ensure_valid_attribute_value(&dom_value)?;
|
|
||||||
this.set_attribute(attribute_name, dom_value);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.add_method("GetTags", |_, this, ()| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
Ok(this.get_tags())
|
|
||||||
});
|
|
||||||
m.add_method("HasTag", |_, this, tag: String| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
Ok(this.has_tag(tag))
|
|
||||||
});
|
|
||||||
m.add_method("AddTag", |_, this, tag: String| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.add_tag(tag);
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
m.add_method("RemoveTag", |_, this, tag: String| {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
this.remove_tag(tag);
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_not_destroyed(inst: &Instance) -> LuaResult<()> {
|
|
||||||
if inst.is_destroyed() {
|
|
||||||
Err(LuaError::RuntimeError(
|
|
||||||
"Instance has been destroyed".to_string(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Gets a property value for an instance.
|
|
||||||
|
|
||||||
Getting a value does the following:
|
|
||||||
|
|
||||||
1. Check if it is a special property like "ClassName", "Name" or "Parent"
|
|
||||||
2. Check if a property exists for the wanted name
|
|
||||||
2a. Get an existing instance property OR
|
|
||||||
2b. Get a property from a known default value
|
|
||||||
3. Get a current child of the instance
|
|
||||||
4. No valid property or instance found, throw error
|
|
||||||
*/
|
|
||||||
fn instance_property_get<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
this: &Instance,
|
|
||||||
prop_name: String,
|
|
||||||
) -> LuaResult<LuaValue<'lua>> {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
|
|
||||||
match prop_name.as_str() {
|
|
||||||
"ClassName" => return this.get_class_name().into_lua(lua),
|
|
||||||
"Name" => {
|
|
||||||
return this.get_name().into_lua(lua);
|
|
||||||
}
|
|
||||||
"Parent" => {
|
|
||||||
return this.get_parent().into_lua(lua);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
|
|
||||||
if let Some(prop) = this.get_property(&prop_name) {
|
|
||||||
if let DomValue::Enum(enum_value) = prop {
|
|
||||||
let enum_name = info.enum_name.ok_or_else(|| {
|
|
||||||
LuaError::RuntimeError(format!(
|
|
||||||
"Failed to get property '{}' - encountered unknown enum",
|
|
||||||
prop_name
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
EnumItem::from_enum_name_and_value(&enum_name, enum_value.to_u32())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
LuaError::RuntimeError(format!(
|
|
||||||
"Failed to get property '{}' - Enum.{} does not contain numeric value {}",
|
|
||||||
prop_name, enum_name, enum_value.to_u32()
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.into_lua(lua)
|
|
||||||
} else {
|
|
||||||
Ok(LuaValue::dom_value_to_lua(lua, &prop)?)
|
|
||||||
}
|
|
||||||
} else if let (Some(enum_name), Some(enum_value)) = (info.enum_name, info.enum_default) {
|
|
||||||
EnumItem::from_enum_name_and_value(&enum_name, enum_value)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
LuaError::RuntimeError(format!(
|
|
||||||
"Failed to get property '{}' - Enum.{} does not contain numeric value {}",
|
|
||||||
prop_name, enum_name, enum_value
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.into_lua(lua)
|
|
||||||
} else if let Some(prop_default) = info.value_default {
|
|
||||||
Ok(LuaValue::dom_value_to_lua(lua, prop_default)?)
|
|
||||||
} else if info.value_type.is_some() {
|
|
||||||
if info.value_type == Some(DomType::Ref) {
|
|
||||||
Ok(LuaValue::Nil)
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"Failed to get property '{}' - missing default value",
|
|
||||||
prop_name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"Failed to get property '{}' - malformed property info",
|
|
||||||
prop_name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
|
|
||||||
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
|
|
||||||
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
|
|
||||||
getter.call(this.clone())
|
|
||||||
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
|
|
||||||
Ok(LuaValue::Function(method))
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"{} is not a valid member of {}",
|
|
||||||
prop_name, this
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Sets a property value for an instance.
|
|
||||||
|
|
||||||
Setting a value does the following:
|
|
||||||
|
|
||||||
1. Check if it is a special property like "ClassName", "Name" or "Parent"
|
|
||||||
2. Check if a property exists for the wanted name
|
|
||||||
2a. Set a strict enum from a given EnumItem OR
|
|
||||||
2b. Set a normal property from a given value
|
|
||||||
*/
|
|
||||||
fn instance_property_set<'lua>(
|
|
||||||
lua: &'lua Lua,
|
|
||||||
this: &mut Instance,
|
|
||||||
(prop_name, prop_value): (String, LuaValue<'lua>),
|
|
||||||
) -> LuaResult<()> {
|
|
||||||
ensure_not_destroyed(this)?;
|
|
||||||
|
|
||||||
match prop_name.as_str() {
|
|
||||||
"ClassName" => {
|
|
||||||
return Err(LuaError::RuntimeError(
|
|
||||||
"Failed to set ClassName - property is read-only".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
"Name" => {
|
|
||||||
let name = String::from_lua(prop_value, lua)?;
|
|
||||||
this.set_name(name);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
"Parent" => {
|
|
||||||
if this.get_class_name() == data_model::CLASS_NAME {
|
|
||||||
return Err(LuaError::RuntimeError(
|
|
||||||
"Failed to set Parent - DataModel can not be reparented".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
|
|
||||||
let parent = Parent::from_lua(prop_value, lua)?;
|
|
||||||
this.set_parent(parent.map(|p| p.clone()));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
|
|
||||||
if let Some(enum_name) = info.enum_name {
|
|
||||||
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
|
|
||||||
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
|
|
||||||
this.set_property(prop_name, DomValue::Enum((*given_enum).clone().into()));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
|
|
||||||
"Failed to set property '{}' - expected Enum.{}, got Enum.{}",
|
|
||||||
prop_name, enum_name, given_enum.parent.desc.name
|
|
||||||
))),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else if let Some(dom_type) = info.value_type {
|
|
||||||
match prop_value.lua_to_dom_value(lua, Some(dom_type)) {
|
|
||||||
Ok(dom_value) => {
|
|
||||||
this.set_property(prop_name, dom_value);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"Failed to set property '{}' - malformed property info",
|
|
||||||
prop_name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
|
|
||||||
setter.call((this.clone(), prop_value))
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"{} is not a valid member of {}",
|
|
||||||
prop_name, this
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::roblox::shared::{
|
|
||||||
classes::{
|
|
||||||
add_class_restricted_getter, add_class_restricted_method,
|
|
||||||
get_or_create_property_ref_instance,
|
|
||||||
},
|
|
||||||
instance::class_is_a_service,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Instance;
|
|
||||||
|
|
||||||
pub const CLASS_NAME: &str = "DataModel";
|
|
||||||
|
|
||||||
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
|
|
||||||
add_class_restricted_getter(f, CLASS_NAME, "Workspace", data_model_get_workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
|
||||||
add_class_restricted_method(m, CLASS_NAME, "GetService", data_model_get_service);
|
|
||||||
add_class_restricted_method(m, CLASS_NAME, "FindService", data_model_find_service);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Get the workspace parented under this datamodel, or create it if it doesn't exist.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
|
|
||||||
get_or_create_property_ref_instance(this, "Workspace", "Workspace")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets or creates a service for this DataModel.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> LuaResult<Instance> {
|
|
||||||
if matches!(class_is_a_service(&service_name), None | Some(false)) {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"'{}' is not a valid service name",
|
|
||||||
service_name
|
|
||||||
)))
|
|
||||||
} else if let Some(service) = this.find_child(|child| child.class == service_name) {
|
|
||||||
Ok(service)
|
|
||||||
} else {
|
|
||||||
let service = Instance::new_orphaned(service_name);
|
|
||||||
service.set_parent(Some(this.clone()));
|
|
||||||
Ok(service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets a service for this DataModel, if it exists.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
fn data_model_find_service(
|
|
||||||
_: &Lua,
|
|
||||||
this: &Instance,
|
|
||||||
service_name: String,
|
|
||||||
) -> LuaResult<Option<Instance>> {
|
|
||||||
if matches!(class_is_a_service(&service_name), None | Some(false)) {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"'{}' is not a valid service name",
|
|
||||||
service_name
|
|
||||||
)))
|
|
||||||
} else if let Some(service) = this.find_child(|child| child.class == service_name) {
|
|
||||||
Ok(Some(service))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,787 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::{BTreeMap, VecDeque},
|
|
||||||
fmt,
|
|
||||||
hash::{Hash, Hasher},
|
|
||||||
sync::Mutex,
|
|
||||||
};
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use rbx_dom_weak::{
|
|
||||||
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
|
|
||||||
Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
lune::util::TableBuilder,
|
|
||||||
roblox::exports::LuaExportsTable,
|
|
||||||
roblox::shared::instance::{class_exists, class_is_a},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) mod base;
|
|
||||||
pub(crate) mod data_model;
|
|
||||||
pub(crate) mod terrain;
|
|
||||||
pub(crate) mod workspace;
|
|
||||||
|
|
||||||
pub mod registry;
|
|
||||||
|
|
||||||
const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes";
|
|
||||||
const PROPERTY_NAME_TAGS: &str = "Tags";
|
|
||||||
|
|
||||||
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
|
|
||||||
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Instance {
|
|
||||||
pub(crate) dom_ref: DomRef,
|
|
||||||
pub(crate) class_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Instance {
|
|
||||||
/**
|
|
||||||
Creates a new `Instance` from an existing dom object ref.
|
|
||||||
|
|
||||||
Panics if the instance does not exist in the internal dom,
|
|
||||||
or if the given dom object ref points to the dom root.
|
|
||||||
|
|
||||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
|
||||||
any existing lock must first be released to prevent any deadlocking.
|
|
||||||
*/
|
|
||||||
pub(crate) fn new(dom_ref: DomRef) -> Self {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let instance = dom
|
|
||||||
.get_by_ref(dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
|
|
||||||
if instance.referent() == dom.root_ref() {
|
|
||||||
panic!("Instances can not be created from dom roots")
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
dom_ref,
|
|
||||||
class_name: instance.class.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates a new `Instance` from a dom object ref, if the instance exists.
|
|
||||||
|
|
||||||
Panics if the given dom object ref points to the dom root.
|
|
||||||
|
|
||||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
|
||||||
any existing lock must first be released to prevent any deadlocking.
|
|
||||||
*/
|
|
||||||
pub(crate) fn new_opt(dom_ref: DomRef) -> Option<Self> {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
if let Some(instance) = dom.get_by_ref(dom_ref) {
|
|
||||||
if instance.referent() == dom.root_ref() {
|
|
||||||
panic!("Instances can not be created from dom roots")
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Self {
|
|
||||||
dom_ref,
|
|
||||||
class_name: instance.class.clone(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates a new orphaned `Instance` with a given class name.
|
|
||||||
|
|
||||||
An orphaned instance is an instance at the root of a weak dom.
|
|
||||||
|
|
||||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
|
||||||
any existing lock must first be released to prevent any deadlocking.
|
|
||||||
*/
|
|
||||||
pub(crate) fn new_orphaned(class_name: impl AsRef<str>) -> Self {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let class_name = class_name.as_ref();
|
|
||||||
|
|
||||||
let instance = DomInstanceBuilder::new(class_name.to_string());
|
|
||||||
|
|
||||||
let dom_root = dom.root_ref();
|
|
||||||
let dom_ref = dom.insert(dom_root, instance);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
dom_ref,
|
|
||||||
class_name: class_name.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Creates a new orphaned `Instance` by transferring
|
|
||||||
it from an external weak dom to the internal one.
|
|
||||||
|
|
||||||
An orphaned instance is an instance at the root of a weak dom.
|
|
||||||
|
|
||||||
Panics if the given dom ref is the root dom ref of the external weak dom.
|
|
||||||
*/
|
|
||||||
pub fn from_external_dom(external_dom: &mut WeakDom, external_dom_ref: DomRef) -> Self {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let dom_root = dom.root_ref();
|
|
||||||
|
|
||||||
external_dom.transfer(external_dom_ref, &mut dom, dom_root);
|
|
||||||
|
|
||||||
drop(dom); // Self::new needs mutex handle, drop it first
|
|
||||||
Self::new(external_dom_ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Clones an instance to an external weak dom.
|
|
||||||
|
|
||||||
This will place the instance as a child of the
|
|
||||||
root of the weak dom, and return its referent.
|
|
||||||
*/
|
|
||||||
pub fn clone_into_external_dom(self, external_dom: &mut WeakDom) -> DomRef {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let cloned = dom.clone_into_external(self.dom_ref, external_dom);
|
|
||||||
external_dom.transfer_within(cloned, external_dom.root_ref());
|
|
||||||
|
|
||||||
cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clone_multiple_into_external_dom(
|
|
||||||
referents: &[DomRef],
|
|
||||||
external_dom: &mut WeakDom,
|
|
||||||
) -> Vec<DomRef> {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let cloned = dom.clone_multiple_into_external(referents, external_dom);
|
|
||||||
|
|
||||||
for referent in cloned.iter() {
|
|
||||||
external_dom.transfer_within(*referent, external_dom.root_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Clones the instance and all of its descendants, and orphans it.
|
|
||||||
|
|
||||||
To then save the new instance it must be re-parented,
|
|
||||||
which matches the exact behavior of Roblox's instances.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn clone_instance(&self) -> Instance {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let new_ref = dom.clone_within(self.dom_ref);
|
|
||||||
drop(dom); // Self::new needs mutex handle, drop it first
|
|
||||||
|
|
||||||
let new_inst = Self::new(new_ref);
|
|
||||||
new_inst.set_parent(None);
|
|
||||||
new_inst
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Destroys the instance, removing it completely
|
|
||||||
from the weak dom with no way of recovering it.
|
|
||||||
|
|
||||||
All member methods will throw errors when called from lua and panic
|
|
||||||
when called from rust after the instance has been destroyed.
|
|
||||||
|
|
||||||
Returns `true` if destroyed successfully, `false` if already destroyed.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`Destroy`](https://create.roblox.com/docs/reference/engine/classes/Instance#Destroy)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn destroy(&mut self) -> bool {
|
|
||||||
if self.is_destroyed() {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
dom.destroy(self.dom_ref);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_destroyed(&self) -> bool {
|
|
||||||
// NOTE: This property can not be cached since instance references
|
|
||||||
// other than this one may have destroyed this one, and we don't
|
|
||||||
// keep track of all current instance reference structs
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
dom.get_by_ref(self.dom_ref).is_none()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Destroys all child instances.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`Instance::Destroy`] for more info about what happens when an instance gets destroyed
|
|
||||||
* [`ClearAllChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClearAllChildren)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn clear_all_children(&mut self) {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let instance = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
|
|
||||||
let child_refs = instance.children().to_vec();
|
|
||||||
for child_ref in child_refs {
|
|
||||||
dom.destroy(child_ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Checks if the instance matches or inherits a given class name.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
|
|
||||||
class_is_a(&self.class_name, class_name).unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets the class name of the instance.
|
|
||||||
|
|
||||||
This will return the correct class name even if the instance has been destroyed.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_class_name(&self) -> &str {
|
|
||||||
self.class_name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets the name of the instance, if it exists.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_name(&self) -> String {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
dom.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.name
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Sets the name of the instance, if it exists.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn set_name(&self, name: impl Into<String>) {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
dom.get_by_ref_mut(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.name = name.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets the parent of the instance, if it exists.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_parent(&self) -> Option<Instance> {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let parent_ref = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.parent();
|
|
||||||
|
|
||||||
if parent_ref == dom.root_ref() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
drop(dom); // Self::new needs mutex handle, drop it first
|
|
||||||
Some(Self::new(parent_ref))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Sets the parent of the instance, if it exists.
|
|
||||||
|
|
||||||
If the provided parent is [`None`] the instance will become orphaned.
|
|
||||||
|
|
||||||
An orphaned instance is an instance at the root of a weak dom.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn set_parent(&self, parent: Option<Instance>) {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let parent_ref = parent
|
|
||||||
.map(|parent| parent.dom_ref)
|
|
||||||
.unwrap_or_else(|| dom.root_ref());
|
|
||||||
|
|
||||||
dom.transfer_within(self.dom_ref, parent_ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets a property for the instance, if it exists.
|
|
||||||
*/
|
|
||||||
pub fn get_property(&self, name: impl AsRef<str>) -> Option<DomValue> {
|
|
||||||
INTERNAL_DOM
|
|
||||||
.lock()
|
|
||||||
.expect("Failed to lock document")
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.properties
|
|
||||||
.get(name.as_ref())
|
|
||||||
.cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Sets a property for the instance.
|
|
||||||
|
|
||||||
Note that setting a property here will not fail even if the
|
|
||||||
property does not actually exist for the instance class.
|
|
||||||
*/
|
|
||||||
pub fn set_property(&self, name: impl AsRef<str>, value: DomValue) {
|
|
||||||
INTERNAL_DOM
|
|
||||||
.lock()
|
|
||||||
.expect("Failed to lock document")
|
|
||||||
.get_by_ref_mut(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.properties
|
|
||||||
.insert(name.as_ref().to_string(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets an attribute for the instance, if it exists.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_attribute(&self, name: impl AsRef<str>) -> Option<DomValue> {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let inst = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
if let Some(DomValue::Attributes(attributes)) =
|
|
||||||
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
|
|
||||||
{
|
|
||||||
attributes.get(name.as_ref()).cloned()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets all known attributes for the instance.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_attributes(&self) -> BTreeMap<String, DomValue> {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let inst = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
if let Some(DomValue::Attributes(attributes)) =
|
|
||||||
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
|
|
||||||
{
|
|
||||||
attributes.clone().into_iter().collect()
|
|
||||||
} else {
|
|
||||||
BTreeMap::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Sets an attribute for the instance.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn set_attribute(&self, name: impl AsRef<str>, value: DomValue) {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let inst = dom
|
|
||||||
.get_by_ref_mut(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
// NOTE: Attributes do not support integers, only floats
|
|
||||||
let value = match value {
|
|
||||||
DomValue::Int32(i) => DomValue::Float32(i as f32),
|
|
||||||
DomValue::Int64(i) => DomValue::Float64(i as f64),
|
|
||||||
value => value,
|
|
||||||
};
|
|
||||||
if let Some(DomValue::Attributes(attributes)) =
|
|
||||||
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
|
|
||||||
{
|
|
||||||
attributes.insert(name.as_ref().to_string(), value);
|
|
||||||
} else {
|
|
||||||
let mut attributes = DomAttributes::new();
|
|
||||||
attributes.insert(name.as_ref().to_string(), value);
|
|
||||||
inst.properties.insert(
|
|
||||||
PROPERTY_NAME_ATTRIBUTES.to_string(),
|
|
||||||
DomValue::Attributes(attributes),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Adds a tag to the instance.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn add_tag(&self, name: impl AsRef<str>) {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let inst = dom
|
|
||||||
.get_by_ref_mut(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
|
|
||||||
tags.push(name.as_ref());
|
|
||||||
} else {
|
|
||||||
inst.properties.insert(
|
|
||||||
PROPERTY_NAME_TAGS.to_string(),
|
|
||||||
DomValue::Tags(vec![name.as_ref().to_string()].into()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets all current tags for the instance.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_tags(&self) -> Vec<String> {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let inst = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
|
|
||||||
tags.iter().map(ToString::to_string).collect()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Checks if the instance has a specific tag.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn has_tag(&self, name: impl AsRef<str>) -> bool {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let inst = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
|
|
||||||
let name = name.as_ref();
|
|
||||||
tags.iter().any(|tag| tag == name)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Removes a tag from the instance.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn remove_tag(&self, name: impl AsRef<str>) {
|
|
||||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let inst = dom
|
|
||||||
.get_by_ref_mut(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document");
|
|
||||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
|
|
||||||
let name = name.as_ref();
|
|
||||||
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
|
|
||||||
new_tags.retain(|tag| tag != name);
|
|
||||||
inst.properties.insert(
|
|
||||||
PROPERTY_NAME_TAGS.to_string(),
|
|
||||||
DomValue::Tags(new_tags.into()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets all of the current children of this `Instance`.
|
|
||||||
|
|
||||||
Note that this is a somewhat expensive operation and that other
|
|
||||||
operations using weak dom referents should be preferred if possible.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_children(&self) -> Vec<Instance> {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let children = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.children()
|
|
||||||
.to_vec();
|
|
||||||
|
|
||||||
drop(dom); // Self::new needs mutex handle, drop it first
|
|
||||||
children.into_iter().map(Self::new).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets all of the current descendants of this `Instance` using a breadth-first search.
|
|
||||||
|
|
||||||
Note that this is a somewhat expensive operation and that other
|
|
||||||
operations using weak dom referents should be preferred if possible.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_descendants(&self) -> Vec<Instance> {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let mut descendants = Vec::new();
|
|
||||||
let mut queue = VecDeque::from_iter(
|
|
||||||
dom.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.children(),
|
|
||||||
);
|
|
||||||
|
|
||||||
while let Some(queue_ref) = queue.pop_front() {
|
|
||||||
descendants.push(*queue_ref);
|
|
||||||
let queue_inst = dom.get_by_ref(*queue_ref).unwrap();
|
|
||||||
for queue_ref_inner in queue_inst.children().iter().rev() {
|
|
||||||
queue.push_back(queue_ref_inner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(dom); // Self::new needs mutex handle, drop it first
|
|
||||||
descendants.into_iter().map(Self::new).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Gets the "full name" of this instance.
|
|
||||||
|
|
||||||
This will be a path composed of instance names from the top-level
|
|
||||||
ancestor of this instance down to itself, in the following format:
|
|
||||||
|
|
||||||
`Ancestor.Child.Descendant.Instance`
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn get_full_name(&self) -> String {
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
let dom_root = dom.root_ref();
|
|
||||||
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
let mut instance_ref = self.dom_ref;
|
|
||||||
|
|
||||||
while let Some(instance) = dom.get_by_ref(instance_ref) {
|
|
||||||
if instance_ref != dom_root && instance.class != data_model::CLASS_NAME {
|
|
||||||
instance_ref = instance.parent();
|
|
||||||
parts.push(instance.name.clone());
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.reverse();
|
|
||||||
parts.join(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Finds a child of the instance using the given predicate callback.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`FindFirstChild`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChild) on the Roblox Developer Hub
|
|
||||||
* [`FindFirstChildOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildOfClass) on the Roblox Developer Hub
|
|
||||||
* [`FindFirstChildWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildWhichIsA) on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn find_child<F>(&self, predicate: F) -> Option<Instance>
|
|
||||||
where
|
|
||||||
F: Fn(&DomInstance) -> bool,
|
|
||||||
{
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let children = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.children()
|
|
||||||
.to_vec();
|
|
||||||
|
|
||||||
let found_ref = children.into_iter().find(|child_ref| {
|
|
||||||
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
|
|
||||||
predicate(child_inst)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
drop(dom); // Self::new needs mutex handle, drop it first
|
|
||||||
found_ref.map(Self::new)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Finds an ancestor of the instance using the given predicate callback.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`FindFirstAncestor`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestor) on the Roblox Developer Hub
|
|
||||||
* [`FindFirstAncestorOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorOfClass) on the Roblox Developer Hub
|
|
||||||
* [`FindFirstAncestorWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorWhichIsA) on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn find_ancestor<F>(&self, predicate: F) -> Option<Instance>
|
|
||||||
where
|
|
||||||
F: Fn(&DomInstance) -> bool,
|
|
||||||
{
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let mut ancestor_ref = dom
|
|
||||||
.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.parent();
|
|
||||||
|
|
||||||
while let Some(ancestor) = dom.get_by_ref(ancestor_ref) {
|
|
||||||
if predicate(ancestor) {
|
|
||||||
drop(dom); // Self::new needs mutex handle, drop it first
|
|
||||||
return Some(Self::new(ancestor_ref));
|
|
||||||
} else {
|
|
||||||
ancestor_ref = ancestor.parent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Finds a descendant of the instance using the given
|
|
||||||
predicate callback and a breadth-first search.
|
|
||||||
|
|
||||||
### See Also
|
|
||||||
* [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant)
|
|
||||||
on the Roblox Developer Hub
|
|
||||||
*/
|
|
||||||
pub fn find_descendant<F>(&self, predicate: F) -> Option<Instance>
|
|
||||||
where
|
|
||||||
F: Fn(&DomInstance) -> bool,
|
|
||||||
{
|
|
||||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
|
||||||
|
|
||||||
let mut queue = VecDeque::from_iter(
|
|
||||||
dom.get_by_ref(self.dom_ref)
|
|
||||||
.expect("Failed to find instance in document")
|
|
||||||
.children(),
|
|
||||||
);
|
|
||||||
|
|
||||||
while let Some(queue_item) = queue
|
|
||||||
.pop_front()
|
|
||||||
.and_then(|queue_ref| dom.get_by_ref(*queue_ref))
|
|
||||||
{
|
|
||||||
if predicate(queue_item) {
|
|
||||||
let queue_ref = queue_item.referent();
|
|
||||||
drop(dom); // Self::new needs mutex handle, drop it first
|
|
||||||
return Some(Self::new(queue_ref));
|
|
||||||
} else {
|
|
||||||
queue.extend(queue_item.children())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LuaExportsTable<'_> for Instance {
|
|
||||||
const EXPORT_NAME: &'static str = "Instance";
|
|
||||||
|
|
||||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
|
||||||
let instance_new = |lua, class_name: String| {
|
|
||||||
if class_exists(&class_name) {
|
|
||||||
Instance::new_orphaned(class_name).into_lua(lua)
|
|
||||||
} else {
|
|
||||||
Err(LuaError::RuntimeError(format!(
|
|
||||||
"Failed to create Instance - '{}' is not a valid class name",
|
|
||||||
class_name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
TableBuilder::new(lua)?
|
|
||||||
.with_function("new", instance_new)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Here we add inheritance-like behavior for instances by creating
|
|
||||||
fields that are restricted to specific classnames / base classes
|
|
||||||
|
|
||||||
Note that we should try to be conservative with how many classes
|
|
||||||
and methods we support here - we should only implement methods that
|
|
||||||
are necessary for modifying the dom and / or having ergonomic access
|
|
||||||
to the dom, not try to replicate Roblox engine behavior of instances
|
|
||||||
|
|
||||||
If a user wants to replicate Roblox engine behavior, they can use the
|
|
||||||
instance registry, and register properties + methods from the lua side
|
|
||||||
*/
|
|
||||||
impl LuaUserData for Instance {
|
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
data_model::add_fields(fields);
|
|
||||||
workspace::add_fields(fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
base::add_methods(methods);
|
|
||||||
data_model::add_methods(methods);
|
|
||||||
terrain::add_methods(methods);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for Instance {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
self.dom_ref.hash(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Instance {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
if self.is_destroyed() {
|
|
||||||
"<<DESTROYED>>".to_string()
|
|
||||||
} else {
|
|
||||||
self.get_name()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for Instance {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.dom_ref == other.dom_ref
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Instance> for DomRef {
|
|
||||||
fn from(value: Instance) -> Self {
|
|
||||||
value.dom_ref
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue