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"
|
||||
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]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
|
@ -759,12 +748,6 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
|
@ -1367,15 +1350,6 @@ version = "2.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
|
@ -1484,56 +1458,27 @@ name = "lune"
|
|||
version = "0.8.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"bstr",
|
||||
"chrono",
|
||||
"chrono_lc",
|
||||
"clap",
|
||||
"console",
|
||||
"dialoguer",
|
||||
"directories",
|
||||
"dunce",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"glam",
|
||||
"http 1.1.0",
|
||||
"http-body-util",
|
||||
"hyper 1.3.1",
|
||||
"hyper-tungstenite",
|
||||
"hyper-util",
|
||||
"include_dir",
|
||||
"itertools",
|
||||
"lz4_flex",
|
||||
"lune-roblox",
|
||||
"lune-std",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler 0.0.2",
|
||||
"mlua-luau-scheduler",
|
||||
"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",
|
||||
"rustyline",
|
||||
"self_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"zip_next",
|
||||
]
|
||||
|
||||
|
@ -1570,7 +1515,7 @@ dependencies = [
|
|||
"lune-std-task",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler 0.0.1",
|
||||
"mlua-luau-scheduler",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
@ -1620,7 +1565,7 @@ dependencies = [
|
|||
"lune-std-serde",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler 0.0.1",
|
||||
"mlua-luau-scheduler",
|
||||
"reqwest",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
|
@ -1634,7 +1579,7 @@ dependencies = [
|
|||
"directories",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler 0.0.1",
|
||||
"mlua-luau-scheduler",
|
||||
"os_str_bytes",
|
||||
"pin-project",
|
||||
"tokio",
|
||||
|
@ -1657,7 +1602,7 @@ dependencies = [
|
|||
"lune-roblox",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler 0.0.1",
|
||||
"mlua-luau-scheduler",
|
||||
"once_cell",
|
||||
"rbx_cookie",
|
||||
]
|
||||
|
@ -1685,7 +1630,7 @@ dependencies = [
|
|||
"dialoguer",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler 0.0.1",
|
||||
"mlua-luau-scheduler",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
@ -1695,7 +1640,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler 0.0.1",
|
||||
"mlua-luau-scheduler",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
@ -1827,23 +1772,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "mlua-sys"
|
||||
version = "0.5.2"
|
||||
|
@ -3074,7 +3002,6 @@ dependencies = [
|
|||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"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"] }
|
||||
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-std-serde = { version = "0.1.0", path = "../lune-std-serde" }
|
||||
|
|
|
@ -18,7 +18,31 @@ name = "lune"
|
|||
path = "src/lib.rs"
|
||||
|
||||
[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 = [
|
||||
"dep:anyhow",
|
||||
"dep:env_logger",
|
||||
|
@ -27,113 +51,39 @@ cli = [
|
|||
"dep:rustyline",
|
||||
"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]
|
||||
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]
|
||||
mlua = { version = "0.9.7", features = ["luau"] }
|
||||
mlua-luau-scheduler = "0.0.1"
|
||||
|
||||
console = "0.15"
|
||||
dialoguer = "0.11"
|
||||
directories = "5.0"
|
||||
futures-util = "0.3"
|
||||
once_cell = "1.17"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "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-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tokio = { version = "1.24", features = ["full", "tracing"] }
|
||||
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" }
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
reqwest = { version = "0.11", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
] }
|
||||
|
||||
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
|
||||
|
||||
### DATETIME
|
||||
chrono = "=0.4.34" # NOTE: 0.4.35 does not compile with chrono_lc
|
||||
chrono_lc = "0.1"
|
||||
lune-std = { version = "0.1.0", path = "../lune-std" }
|
||||
lune-utils = { version = "0.1.0", path = "../lune-utils" }
|
||||
lune-roblox = { optional = true, version = "0.1.0", path = "../lune-roblox" }
|
||||
|
||||
### CLI
|
||||
|
||||
anyhow = { optional = true, version = "1.0" }
|
||||
env_logger = { optional = true, version = "0.11" }
|
||||
itertools = "0.12"
|
||||
clap = { optional = true, version = "4.1", features = ["derive"] }
|
||||
include_dir = { optional = true, version = "0.7", features = ["glob"] }
|
||||
rustyline = { optional = true, version = "14.0" }
|
||||
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 console::style;
|
||||
use directories::UserDirs;
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
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
|
||||
.iter()
|
||||
.map(|line| &line[shortest_indent..])
|
||||
// Replace newlines with a single space inbetween instead
|
||||
.interleave(std::iter::repeat(" ").take(comment_lines.len() - 1))
|
||||
.collect();
|
||||
.map(|line| line[shortest_indent..].to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
Some(unindented_lines)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
mod lune;
|
||||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
mod rt;
|
||||
|
||||
#[cfg(feature = "roblox")]
|
||||
pub mod roblox;
|
||||
pub use lune_roblox as roblox;
|
||||
|
||||
#[cfg(test)]
|
||||
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;
|
||||
|
||||
use cli::Cli;
|
||||
use console::style;
|
||||
|
||||
use lune_utils::fmt::Label;
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() -> ExitCode {
|
||||
|
@ -35,12 +36,7 @@ async fn main() -> ExitCode {
|
|||
match Cli::new().run().await {
|
||||
Ok(code) => code,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}{}{}\n{err:?}",
|
||||
style("[").dim(),
|
||||
style("ERROR").red(),
|
||||
style("]").dim(),
|
||||
);
|
||||
eprintln!("{}\n{err:?}", Label::Error);
|
||||
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