Remove old builtins from lune crate and use lune-std instead

This commit is contained in:
Filip Tibell 2024-04-23 15:33:44 +02:00
parent 2261733516
commit c8dcea0a21
No known key found for this signature in database
115 changed files with 77 additions and 14005 deletions

93
Cargo.lock generated
View file

@ -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",
]

View file

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

View file

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

View file

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

View file

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

View file

@ -1,32 +0,0 @@
use mlua::prelude::*;
use thiserror::Error;
pub type DateTimeResult<T, E = DateTimeError> = Result<T, E>;
#[derive(Debug, Clone, Error)]
pub enum DateTimeError {
#[error("invalid date")]
InvalidDate,
#[error("invalid time")]
InvalidTime,
#[error("ambiguous date or time")]
Ambiguous,
#[error("date or time is outside allowed range")]
OutOfRangeUnspecified,
#[error("{name} must be within range {min} -> {max}, got {value}")]
OutOfRange {
name: &'static str,
value: String,
min: String,
max: String,
},
#[error(transparent)]
ParseError(#[from] chrono::ParseError),
}
impl From<DateTimeError> for LuaError {
fn from(value: DateTimeError) -> Self {
LuaError::runtime(value.to_string())
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}'")),
}
}
}

View file

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

View file

@ -1,231 +0,0 @@
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr},
};
use bstr::{BString, ByteSlice};
use mlua::prelude::*;
use reqwest::Method;
use super::util::table_to_hash_map;
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
return {
status = 426,
body = "Upgrade Required",
headers = {
Upgrade = "websocket",
},
}
"#;
// Net request config
#[derive(Debug, Clone)]
pub struct RequestConfigOptions {
pub decompress: bool,
}
impl Default for RequestConfigOptions {
fn default() -> Self {
Self { decompress: true }
}
}
impl<'lua> FromLua<'lua> for RequestConfigOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::Nil = value {
// Nil means default options
Ok(Self::default())
} else if let LuaValue::Table(tab) = value {
// Table means custom options
let decompress = match tab.get::<_, Option<bool>>("decompress") {
Ok(decomp) => Ok(decomp.unwrap_or(true)),
Err(_) => Err(LuaError::RuntimeError(
"Invalid option value for 'decompress' in request config options".to_string(),
)),
}?;
Ok(Self { decompress })
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestConfigOptions",
message: Some(format!(
"Invalid request config options - expected table or nil, got {}",
value.type_name()
)),
})
}
}
}
#[derive(Debug, Clone)]
pub struct RequestConfig {
pub url: String,
pub method: Method,
pub query: HashMap<String, Vec<String>>,
pub headers: HashMap<String, Vec<String>>,
pub body: Option<Vec<u8>>,
pub options: RequestConfigOptions,
}
impl FromLua<'_> for RequestConfig {
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
// If we just got a string we assume its a GET request to a given url
if let LuaValue::String(s) = value {
Ok(Self {
url: s.to_string_lossy().to_string(),
method: Method::GET,
query: HashMap::new(),
headers: HashMap::new(),
body: None,
options: 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,
})
}
}
}

View file

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

View file

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

View file

@ -1,105 +0,0 @@
use std::{
net::SocketAddr,
rc::{Rc, Weak},
};
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use tokio::{net::TcpListener, pin};
use mlua::prelude::*;
use mlua_luau_scheduler::LuaSpawnExt;
use 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()
}

View file

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

View file

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

View file

@ -1,82 +0,0 @@
use std::{future::Future, net::SocketAddr, pin::Pin, rc::Rc};
use http_body_util::{BodyExt, Full};
use hyper::{
body::{Bytes, Incoming},
service::Service,
Request, Response,
};
use hyper_tungstenite::{is_upgrade_request, upgrade};
use mlua::prelude::*;
use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt};
use super::{
super::websocket::NetWebSocket, keys::SvcKeys, request::LuaRequest, response::LuaResponse,
};
#[derive(Debug, Clone)]
pub(super) struct Svc {
pub(super) lua: Rc<Lua>,
pub(super) addr: SocketAddr,
pub(super) keys: SvcKeys,
}
impl Service<Request<Incoming>> for Svc {
type Response = Response<Full<Bytes>>;
type Error = LuaError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn call(&self, req: Request<Incoming>) -> Self::Future {
let lua = self.lua.clone();
let addr = self.addr;
let keys = self.keys;
if keys.has_websocket_handler() && is_upgrade_request(&req) {
Box::pin(async move {
let (res, sock) = upgrade(req, None).into_lua_err()?;
let lua_inner = lua.clone();
lua.spawn_local(async move {
let sock = sock.await.unwrap();
let lua_sock = NetWebSocket::new(sock);
let lua_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()
})
}
}
}

View file

@ -1,94 +0,0 @@
use std::collections::HashMap;
use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH};
use reqwest::header::HeaderMap;
use mlua::prelude::*;
use 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)
}

View file

@ -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),
})
});
}
}

View file

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

View file

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

View file

@ -1,177 +0,0 @@
use std::{
collections::HashMap,
env::{self},
path::PathBuf,
};
use directories::UserDirs;
use mlua::prelude::*;
use tokio::process::Command;
mod kind;
mod stdio;
pub(super) use kind::*;
pub(super) use stdio::*;
#[derive(Debug, Clone, Default)]
pub(super) struct ProcessSpawnOptions {
pub cwd: Option<PathBuf>,
pub envs: HashMap<String, String>,
pub shell: Option<String>,
pub stdio: ProcessSpawnOptionsStdio,
}
impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
let mut this = Self::default();
let value = match value {
LuaValue::Nil => return Ok(this),
LuaValue::Table(t) => t,
_ => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ProcessSpawnOptions",
message: Some(format!(
"Invalid spawn options - expected table, got {}",
value.type_name()
)),
})
}
};
/*
If we got a working directory to use:
1. Substitute leading tilde (~) for the users home dir
2. Make sure it exists
*/
match value.get("cwd")? {
LuaValue::Nil => {}
LuaValue::String(s) => {
let mut cwd = PathBuf::from(s.to_str()?);
if let Ok(stripped) = cwd.strip_prefix("~") {
let user_dirs = UserDirs::new().ok_or_else(|| {
LuaError::runtime(
"Invalid value for option 'cwd' - failed to get home directory",
)
})?;
cwd = user_dirs.home_dir().join(stripped)
}
if !cwd.exists() {
return Err(LuaError::runtime(
"Invalid value for option 'cwd' - path does not exist",
));
};
this.cwd = Some(cwd);
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'cwd' - expected string, got '{}'",
value.type_name()
)))
}
}
/*
If we got environment variables, make sure they are strings
*/
match value.get("env")? {
LuaValue::Nil => {}
LuaValue::Table(e) => {
for pair in e.pairs::<String, String>() {
let (k, v) = pair.context("Environment variables must be strings")?;
this.envs.insert(k, v);
}
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'env' - expected table, got '{}'",
value.type_name()
)))
}
}
/*
If we got a shell to use:
1. When given as a string, use that literally
2. When set to true, use a default shell for the platform
*/
match value.get("shell")? {
LuaValue::Nil => {}
LuaValue::String(s) => this.shell = Some(s.to_string_lossy().to_string()),
LuaValue::Boolean(true) => {
this.shell = match env::consts::FAMILY {
"unix" => Some("/bin/sh".to_string()),
"windows" => Some("powershell".to_string()),
_ => None,
};
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'shell' - expected 'true' or 'string', got '{}'",
value.type_name()
)))
}
}
/*
If we got options for stdio handling, parse those as well - note that
we accept a separate "stdin" value here for compatibility with older
scripts, but the user should preferrably pass it in the stdio table
*/
this.stdio = value.get("stdio")?;
match value.get("stdin")? {
LuaValue::Nil => {}
LuaValue::String(s) => this.stdio.stdin = Some(s.as_bytes().to_vec()),
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'stdin' - expected 'string', got '{}'",
value.type_name()
)))
}
}
Ok(this)
}
}
impl ProcessSpawnOptions {
pub fn into_command(self, program: impl Into<String>, args: Option<Vec<String>>) -> Command {
let mut program = program.into();
// Run a shell using the command param if wanted
let pargs = match self.shell {
None => args,
Some(shell) => {
let shell_args = match args {
Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))],
None => vec!["-c".to_string(), program.to_string()],
};
program = shell.to_string();
Some(shell_args)
}
};
// Create command with the wanted options
let mut cmd = match pargs {
None => Command::new(program),
Some(args) => {
let mut cmd = Command::new(program);
cmd.args(args);
cmd
}
};
// Set dir to run in and env variables
if let Some(cwd) = self.cwd {
cmd.current_dir(cwd);
}
if !self.envs.is_empty() {
cmd.envs(self.envs);
}
cmd
}
}

View file

@ -1,56 +0,0 @@
use mlua::prelude::*;
use super::kind::ProcessSpawnOptionsStdioKind;
#[derive(Debug, Clone, Default)]
pub struct ProcessSpawnOptionsStdio {
pub stdout: ProcessSpawnOptionsStdioKind,
pub stderr: ProcessSpawnOptionsStdioKind,
pub stdin: Option<Vec<u8>>,
}
impl From<ProcessSpawnOptionsStdioKind> for ProcessSpawnOptionsStdio {
fn from(value: ProcessSpawnOptionsStdioKind) -> Self {
Self {
stdout: value,
stderr: value,
..Default::default()
}
}
}
impl<'lua> FromLua<'lua> for ProcessSpawnOptionsStdio {
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
match value {
LuaValue::Nil => Ok(Self::default()),
LuaValue::String(s) => {
Ok(ProcessSpawnOptionsStdioKind::from_lua(LuaValue::String(s), lua)?.into())
}
LuaValue::Table(t) => {
let mut this = Self::default();
if let Some(stdin) = t.get("stdin")? {
this.stdin = stdin;
}
if let Some(stdout) = t.get("stdout")? {
this.stdout = stdout;
}
if let Some(stderr) = t.get("stderr")? {
this.stderr = stderr;
}
Ok(this)
}
_ => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ProcessSpawnOptionsStdio",
message: Some(format!(
"Invalid spawn options stdio - expected string or table, got {}",
value.type_name()
)),
}),
}
}
}

View file

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

View file

@ -1,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,
})
}

View file

@ -1,91 +0,0 @@
use std::sync::Arc;
use mlua::prelude::*;
use regex::{Captures, Regex};
use self_cell::self_cell;
use super::matches::LuaMatch;
type OptionalCaptures<'a> = Option<Captures<'a>>;
self_cell! {
struct LuaCapturesInner {
owner: Arc<String>,
#[covariant]
dependent: OptionalCaptures,
}
}
/**
A wrapper over the `regex::Captures` struct that can be used from Lua.
*/
pub struct LuaCaptures {
inner: LuaCapturesInner,
}
impl LuaCaptures {
/**
Create a new `LuaCaptures` instance from a `Regex` pattern and a `String` text.
Returns `Some(_)` if captures were found, `None` if no captures were found.
*/
pub fn new(pattern: &Regex, text: String) -> Option<Self> {
let inner =
LuaCapturesInner::new(Arc::from(text), |owned| pattern.captures(owned.as_str()));
if inner.borrow_dependent().is_some() {
Some(Self { inner })
} else {
None
}
}
fn captures(&self) -> &Captures {
self.inner
.borrow_dependent()
.as_ref()
.expect("None captures should not be used")
}
fn num_captures(&self) -> usize {
// NOTE: Here we exclude the match for the entire regex
// pattern, only counting the named and numbered captures
self.captures().len() - 1
}
fn text(&self) -> Arc<String> {
Arc::clone(self.inner.borrow_owner())
}
}
impl LuaUserData for LuaCaptures {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("get", |_, this, index: usize| {
Ok(this
.captures()
.get(index)
.map(|m| LuaMatch::new(this.text(), m)))
});
methods.add_method("group", |_, this, group: String| {
Ok(this
.captures()
.name(&group)
.map(|m| LuaMatch::new(this.text(), m)))
});
methods.add_method("format", |_, this, format: String| {
let mut new = String::new();
this.captures().expand(&format, &mut new);
Ok(new)
});
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures()));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("RegexCaptures({})", this.num_captures()))
});
}
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_meta_field(LuaMetaMethod::Type, "RegexCaptures");
}
}

View file

@ -1,53 +0,0 @@
use std::{ops::Range, sync::Arc};
use mlua::prelude::*;
use regex::Match;
/**
A wrapper over the `regex::Match` struct that can be used from Lua.
*/
pub struct LuaMatch {
text: Arc<String>,
start: usize,
end: usize,
}
impl LuaMatch {
/**
Create a new `LuaMatch` instance from a `String` text and a `regex::Match`.
*/
pub fn new(text: Arc<String>, matched: Match) -> Self {
Self {
text,
start: matched.start(),
end: matched.end(),
}
}
fn range(&self) -> Range<usize> {
self.start..self.end
}
fn slice(&self) -> &str {
&self.text[self.range()]
}
}
impl LuaUserData for LuaMatch {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
// NOTE: Strings are 0 based in Rust but 1 based in Luau, and end of range in Rust is exclusive
fields.add_field_method_get("start", |_, this| Ok(this.start.saturating_add(1)));
fields.add_field_method_get("finish", |_, this| Ok(this.end));
fields.add_field_method_get("len", |_, this| Ok(this.range().len()));
fields.add_field_method_get("text", |_, this| Ok(this.slice().to_string()));
fields.add_meta_field(LuaMetaMethod::Type, "RegexMatch");
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len()));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("RegexMatch({})", this.slice()))
});
}
}

View file

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

View file

@ -1,76 +0,0 @@
use std::sync::Arc;
use mlua::prelude::*;
use regex::Regex;
use super::{captures::LuaCaptures, matches::LuaMatch};
/**
A wrapper over the `regex::Regex` struct that can be used from Lua.
*/
#[derive(Debug, Clone)]
pub struct LuaRegex {
inner: Regex,
}
impl LuaRegex {
/**
Create a new `LuaRegex` instance from a `String` pattern.
*/
pub fn new(pattern: String) -> LuaResult<Self> {
Regex::new(&pattern)
.map(|inner| Self { inner })
.map_err(LuaError::external)
}
}
impl LuaUserData for LuaRegex {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("isMatch", |_, this, text: String| {
Ok(this.inner.is_match(&text))
});
methods.add_method("find", |_, this, text: String| {
let arc = Arc::new(text);
Ok(this
.inner
.find(&arc)
.map(|m| LuaMatch::new(Arc::clone(&arc), m)))
});
methods.add_method("captures", |_, this, text: String| {
Ok(LuaCaptures::new(&this.inner, text))
});
methods.add_method("split", |_, this, text: String| {
Ok(this
.inner
.split(&text)
.map(|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");
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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' | '.')
}

View file

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

View file

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

View file

@ -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(&current).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 == '.'
}

View file

@ -1,8 +0,0 @@
mod table_builder;
pub mod formatting;
pub mod luaurc;
pub mod paths;
pub mod traits;
pub use table_builder::TableBuilder;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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::*;

View file

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

View file

@ -1,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")
}
}

View file

@ -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),
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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),
];

View file

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

View file

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

View file

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

View file

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

View file

@ -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),
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +0,0 @@
// HACK: We round to the nearest Very Small Decimal
// to reduce writing out floating point accumulation
// errors to files (mostly relevant for xml formats)
const ROUNDING: usize = 65_536; // 2 ^ 16
pub fn round_float_decimal(value: f32) -> f32 {
let place = ROUNDING as f32;
// Round only the fractional part, we do not want to
// lose any float precision in case a user for some
// reason has very very large float numbers in files
let whole = value.trunc();
let fract = (value.fract() * place).round() / place;
whole + fract
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,68 +0,0 @@
use mlua::prelude::*;
/**
Trait for any item that should be exported as part of the `roblox` built-in library.
This may be an enum or a struct that should export constants and/or constructs.
### Example usage
```rs
use mlua::prelude::*;
struct MyType(usize);
impl MyType {
pub fn new(n: usize) -> Self {
Self(n)
}
}
impl LuaExportsTable<'_> for MyType {
const EXPORT_NAME: &'static str = "MyType";
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
let my_type_new = |lua, n: Option<usize>| {
Self::new(n.unwrap_or_default())
};
TableBuilder::new(lua)?
.with_function("new", my_type_new)?
.build_readonly()
}
}
impl LuaUserData for MyType {
// ...
}
```
*/
pub trait LuaExportsTable<'lua> {
const EXPORT_NAME: &'static str;
fn create_exports_table(lua: &'lua Lua) -> LuaResult<LuaTable<'lua>>;
}
/**
Exports a single item that implements the [`LuaExportsTable`] trait.
Returns the name of the export, as well as the export table.
### Example usage
```rs
let lua: mlua::Lua::new();
let (name1, table1) = export::<Type1>(lua)?;
let (name2, table2) = export::<Type2>(lua)?;
```
*/
pub fn export<'lua, T>(lua: &'lua Lua) -> LuaResult<(&'static str, LuaValue<'lua>)>
where
T: LuaExportsTable<'lua>,
{
Ok((
T::EXPORT_NAME,
<T as LuaExportsTable>::create_exports_table(lua)?.into_lua(lua)?,
))
}

View file

@ -1,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
)))
}
}

View file

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

View file

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