Compare commits

..

45 commits
v0.8.6 ... main

Author SHA1 Message Date
Micah
27e3efca97
Implement Enum attributes (#306) 2025-04-03 22:14:24 +02:00
Micah
8bd1a9b77d
Implement support for Roblox's Content type (#305) 2025-04-03 20:40:08 +02:00
Micah
bb8c4bce82
Update rbx-dom dependencies (#304) 2025-04-02 23:10:56 +02:00
Filip Tibell
6902ecaa7c
Fix various new clippy lints 2025-03-24 19:44:14 +01:00
dai
dc08b91314
Fix deadlock in stdio.format calls in tostring metamethod (#288) 2025-03-24 19:34:51 +01:00
Micah
822dd19393
Add functions for getting Roblox Studio locations to roblox library (#284) 2025-03-24 19:29:22 +01:00
6cd0234a5f
Allow toggling JIT in the CLI (#265) 2025-03-24 19:26:02 +01:00
Micah
19e7f57284
Loosen Lune version string requirements (#294) 2025-03-24 19:24:36 +01:00
Qwreey
5d1401cdf6
Add process.endianness constant (#267) 2024-11-05 13:10:05 +01:00
Sasial
91af86cca2
IsA, ClassName & Parent should work if an instance is already destroyed (#271) 2024-11-05 13:02:15 +01:00
Filip Tibell
c935149c1e
Update dependencies 2024-10-17 11:43:51 +02:00
Filip Tibell
e5bda57665
Document new breaking changes in changelog 2024-10-17 11:43:13 +02:00
Filip Tibell
ef294f207c
Fix websocket example files 2024-10-17 11:27:32 +02:00
Filip Tibell
f89d02a60d
Use 4 spaces for error formatting indentation 2024-10-17 11:26:01 +02:00
Filip Tibell
d090cd2420
Remove redundant stack trace information in error formatter 2024-10-17 11:23:20 +02:00
Filip Tibell
99c17795c1
Update rokit action version and tool versions 2024-10-17 09:26:13 +02:00
Filip Tibell
138221b93e
Update websocket tests and types to use new calling convention 2024-10-16 22:00:33 +02:00
Filip Tibell
8abfc21181
Use standard method calling conventions for websockets 2024-10-16 21:55:53 +02:00
309c461e11
Implement a non-blocking child process interface (#211) 2024-10-16 21:48:12 +02:00
Filip Tibell
93fa14d832
Revert some unnecessary stylistic changes 2024-10-16 21:41:16 +02:00
df4fb9be91
Make Runtime::run Return Lua Values (#178) 2024-10-16 21:35:23 +02:00
eaac9ff53a
Migrate to Rokit as toolchain manager (#238) 2024-10-16 21:06:14 +02:00
Eli
0d2f5539b6
Add Moonwave comments for DateTime properties. (#248) 2024-10-16 21:03:58 +02:00
howmanysmall
0f4cac29aa
Fix Regex types (#250) 2024-10-16 21:03:00 +02:00
Filip Tibell
010cd36375
Version 0.8.9 2024-10-07 19:34:55 +02:00
Filip Tibell
c17da72815
Update dependencies 2024-10-07 19:33:59 +02:00
Filip Tibell
ff83c401b8
Version 0.8.8 2024-08-22 21:30:36 +02:00
Kenneth Loeffler
a007fa94a6
Update all rbx-dom dependencies to their latest versions (#245) 2024-08-22 21:24:32 +02:00
Filip Tibell
1d4d1635eb
Temporarily disable publish to crates.io in workflow 2024-08-10 13:36:19 +02:00
Filip Tibell
56f08a88aa
Fix new clippy lints 2024-08-10 13:34:13 +02:00
Filip Tibell
833d0e244b
Add version and date to changelog 2024-08-10 13:25:55 +02:00
Filip Tibell
3e09807638
Bump all crate versions 2024-08-10 13:07:56 +02:00
Filip Tibell
ea7013322f
Fix type and tostring metamethods not always being respected during table formatting 2024-08-10 13:01:13 +02:00
Filip Tibell
98b31b9f67
Update tooling 2024-08-10 12:48:51 +02:00
Filip Tibell
8364a8e4de
We no longer use selene 2024-08-10 12:48:27 +02:00
Filip Tibell
473ad80e8f
Update dependencies 2024-08-10 12:47:36 +02:00
Filip Tibell
180d20ce4a
Update changelog with missing PRs 2024-08-10 12:28:26 +02:00
Filip Tibell
b585234b08
Clarify some comments and expose more instance functions in lune-roblox 2024-07-21 23:50:55 +02:00
ZachCurtis
5379c79488
Add missing vector methods (#228) 2024-07-21 23:35:58 +02:00
Nick Winans
8aefe88104
Add compression level option to serde.compress (#224) 2024-07-06 22:38:35 +02:00
Maxwell Ruben
cb552af660
Fix readDir with trailing forward-slash on Windows (#220) 2024-07-06 22:34:12 +02:00
Filip Tibell
95c2ca0965
Fix mixed indentation in regex documentation comment 2024-06-23 14:56:31 +02:00
Filip Tibell
5167a71e6f
Improve documentation comments for serde library 2024-06-23 14:53:32 +02:00
Filip Tibell
eac34d2e7e
Re-enable crates.io publish step in release workflow 2024-06-23 14:40:33 +02:00
Filip Tibell
ff80981282
Add missing changelog entry 2024-06-23 14:39:58 +02:00
108 changed files with 2219 additions and 1036 deletions

View file

@ -23,11 +23,8 @@ jobs:
with: with:
components: rustfmt components: rustfmt
- name: Install Just
uses: extractions/setup-just@v2
- name: Install Tooling - name: Install Tooling
uses: ok-nick/setup-aftman@v0.4.2 uses: CompeyDev/setup-rokit@v0.1.2
- name: Check Formatting - name: Check Formatting
run: just fmt-check run: just fmt-check
@ -40,11 +37,8 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Just
uses: extractions/setup-just@v2
- name: Install Tooling - name: Install Tooling
uses: ok-nick/setup-aftman@v0.4.2 uses: CompeyDev/setup-rokit@v0.1.2
- name: Analyze - name: Analyze
run: just analyze run: just analyze

View file

@ -129,7 +129,7 @@ end
]] ]]
print("Sending 4 pings to google 🌏") print("Sending 4 pings to google 🌏")
local result = process.spawn("ping", { local result = process.exec("ping", {
"google.com", "google.com",
"-c 4", "-c 4",
}) })

View file

@ -28,8 +28,8 @@ end)
for _ = 1, 5 do for _ = 1, 5 do
local start = os.clock() local start = os.clock()
socket.send(tostring(1)) socket:send(tostring(1))
local response = socket.next() local response = socket:next()
local elapsed = os.clock() - start local elapsed = os.clock() - start
print(`Got response '{response}' in {elapsed * 1_000} milliseconds`) print(`Got response '{response}' in {elapsed * 1_000} milliseconds`)
task.wait(1 - elapsed) task.wait(1 - elapsed)
@ -38,7 +38,7 @@ end
-- Everything went well, and we are done with the socket, so we can close it -- Everything went well, and we are done with the socket, so we can close it
print("Closing web socket...") print("Closing web socket...")
socket.close() socket:close()
task.cancel(forceExit) task.cancel(forceExit)
print("Done! 🌙") print("Done! 🌙")

View file

@ -15,9 +15,9 @@ local handle = net.serve(PORT, {
handleWebSocket = function(socket) handleWebSocket = function(socket)
print("Got new web socket connection!") print("Got new web socket connection!")
repeat repeat
local message = socket.next() local message = socket:next()
if message ~= nil then if message ~= nil then
socket.send("Echo - " .. message) socket:send("Echo - " .. message)
end end
until message == nil until message == nil
print("Web socket disconnected.") print("Web socket disconnected.")

View file

@ -8,6 +8,81 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## `0.9.0`
### Breaking changes
- Added two new process spawning functions - `process.create` and `process.exec`, removing the previous `process.spawn` API completely. ([#211])
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function.
The new `process.create` function is a non-blocking process creation API and can be used to interactively
read and write stdio of the process.
```lua
local child = process.create("program", {
"cli-argument",
"other-cli-argument"
})
-- Writing to stdin
child.stdin:write("Hello from Lune!")
-- Reading from stdout
local data = child.stdout:read()
print(buffer.tostring(data))
```
- WebSocket methods in `net.socket` and `net.serve` now use standard Lua method calling convention and colon syntax.
This means `socket.send(...)` is now `socket:send(...)`, `socket.close(...)` is now `socket:close(...)`, and so on.
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
### Changed
- Documentation comments for several standard library properties have been improved ([#248], [#250])
- Error messages no longer contain redundant or duplicate stack trace information
[#178]: https://github.com/lune-org/lune/pull/178
[#211]: https://github.com/lune-org/lune/pull/211
[#248]: https://github.com/lune-org/lune/pull/248
[#250]: https://github.com/lune-org/lune/pull/250
## `0.8.9` - October 7th, 2024
### Changed
- Updated to Luau version `0.640`
## `0.8.8` - August 22nd, 2024
### Fixed
- Fixed errors when deserializing `Lighting.AttributesSerialize` by updating `rbx-dom` dependencies ([#245])
[#245]: https://github.com/lune-org/lune/pull/245
## `0.8.7` - August 10th, 2024
### Added
- Added a compression level option to `serde.compress` ([#224])
- Added missing vector methods to the `roblox` library ([#228])
### Changed
- Updated to Luau version `0.635`
- Updated to rbx-dom database version `0.634`
### Fixed
- Fixed `fs.readDir` with trailing forward-slash on Windows ([#220])
- Fixed `__type` and `__tostring` metamethods not always being respected when formatting tables
[#220]: https://github.com/lune-org/lune/pull/220
[#224]: https://github.com/lune-org/lune/pull/224
[#228]: https://github.com/lune-org/lune/pull/228
## `0.8.6` - June 23rd, 2024 ## `0.8.6` - June 23rd, 2024
### Added ### Added
@ -37,6 +112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Check the documentation for the `luau` standard library for more information. Check the documentation for the `luau` standard library for more information.
- Implemented support for floor division operator / `__idiv` for the `Vector2` and `Vector3` types in the `roblox` standard library ([#196]) - Implemented support for floor division operator / `__idiv` for the `Vector2` and `Vector3` types in the `roblox` standard library ([#196])
- Fixed the `_VERSION` global containing an incorrect Lune version string.
### Changed ### Changed

1129
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,4 +0,0 @@
[tools]
luau-lsp = "JohnnyMorganz/luau-lsp@1.29.1"
selene = "Kampfkarren/selene@0.27.1"
stylua = "JohnnyMorganz/StyLua@0.20.0"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-roblox" name = "lune-roblox"
version = "0.1.2" version = "0.1.4"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,17 +13,17 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
glam = "0.27" glam = "0.27"
rand = "0.8" rand = "0.8"
thiserror = "1.0" thiserror = "1.0"
once_cell = "1.17" once_cell = "1.17"
rbx_binary = "0.7.3" rbx_binary = "1.0.0"
rbx_dom_weak = "2.6.0" rbx_dom_weak = "3.0.0"
rbx_reflection = "4.4.0" rbx_reflection = "5.0.0"
rbx_reflection_database = "0.2.9" rbx_reflection_database = "1.0.0"
rbx_xml = "0.13.2" rbx_xml = "1.0.0"
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -47,6 +47,7 @@ pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
| DomType::CFrame | DomType::CFrame
| DomType::Color3 | DomType::Color3
| DomType::ColorSequence | DomType::ColorSequence
| DomType::EnumItem
| DomType::Float32 | DomType::Float32
| DomType::Float64 | DomType::Float64
| DomType::Font | DomType::Font

View file

@ -51,7 +51,7 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)), DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)), DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)), DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::Content(s) => Ok(LuaValue::String( DomValue::ContentId(s) => Ok(LuaValue::String(
lua.create_string(AsRef::<str>::as_ref(s))?, lua.create_string(AsRef::<str>::as_ref(s))?,
)), )),
@ -104,8 +104,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
(LuaValue::String(s), DomType::BinaryString) => { (LuaValue::String(s), DomType::BinaryString) => {
Ok(DomValue::BinaryString(s.as_ref().into())) Ok(DomValue::BinaryString(s.as_ref().into()))
} }
(LuaValue::String(s), DomType::Content) => { (LuaValue::String(s), DomType::ContentId) => {
Ok(DomValue::Content(s.to_str()?.to_string().into())) Ok(DomValue::ContentId(s.to_str()?.to_string().into()))
} }
// NOTE: Some values are either optional or default and we // NOTE: Some values are either optional or default and we
@ -200,6 +200,8 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3), DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
DomValue::Color3uint8(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::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
DomValue::Content(value) => dom_to_userdata!(lua, value => Content),
DomValue::EnumItem(value) => dom_to_userdata!(lua, value => EnumItem),
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces), DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
DomValue::Font(value) => dom_to_userdata!(lua, value => Font), DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange), DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
@ -256,7 +258,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3), DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8), DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence), DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum), DomType::Content => userdata_to_dom!(self as Content => dom::Content),
DomType::EnumItem => userdata_to_dom!(self as EnumItem => dom::EnumItem),
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces), DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
DomType::Font => userdata_to_dom!(self as Font => dom::Font), DomType::Font => userdata_to_dom!(self as Font => dom::Font),
DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange), DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange),
@ -314,7 +317,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame), 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::<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::<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::<EnumItem>() => userdata_to_dom!(value as EnumItem => dom::EnumItem),
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces), 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::<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::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),

View file

@ -19,7 +19,9 @@ impl DomValueExt for DomType {
Color3uint8 => "Color3uint8", Color3uint8 => "Color3uint8",
ColorSequence => "ColorSequence", ColorSequence => "ColorSequence",
Content => "Content", Content => "Content",
ContentId => "ContentId",
Enum => "Enum", Enum => "Enum",
EnumItem => "EnumItem",
Faces => "Faces", Faces => "Faces",
Float32 => "Float32", Float32 => "Float32",
Float64 => "Float64", Float64 => "Float64",

View file

@ -0,0 +1,120 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::{Content as DomContent, ContentType};
use lune_utils::TableBuilder;
use crate::{exports::LuaExportsTable, instance::Instance};
use super::{super::*, EnumItem};
/**
An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype.
This implements all documented properties, methods & constructors of the Content type as of April 2025.
*/
#[derive(Debug, Clone, PartialEq)]
pub struct Content(ContentType);
impl LuaExportsTable<'_> for Content {
const EXPORT_NAME: &'static str = "Content";
fn create_exports_table(lua: &'_ Lua) -> LuaResult<LuaTable<'_>> {
let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri)));
let from_object = |_, obj: LuaUserDataRef<Instance>| {
let database = rbx_reflection_database::get();
let instance_descriptor = database
.classes
.get("Instance")
.expect("the reflection database should always have Instance in it");
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
"you should not be able to construct an Instance that is not known to Lune",
);
if database.has_superclass(param_descriptor, instance_descriptor) {
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
} else {
Ok(Content(ContentType::Object(obj.dom_ref)))
}
};
TableBuilder::new(lua)?
.with_value("none", Content(ContentType::None))?
.with_function("fromUri", from_uri)?
.with_function("fromObject", from_object)?
.build_readonly()
}
}
impl LuaUserData for Content {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("SourceType", |_, this| {
let variant_name = match &this.0 {
ContentType::None => "None",
ContentType::Uri(_) => "Uri",
ContentType::Object(_) => "Object",
other => {
return Err(LuaError::runtime(format!(
"cannot get SourceType: unknown ContentType variant '{other:?}'"
)))
}
};
Ok(EnumItem::from_enum_name_and_name(
"ContentSourceType",
variant_name,
))
});
fields.add_field_method_get("Uri", |_, this| {
if let ContentType::Uri(uri) = &this.0 {
Ok(Some(uri.to_owned()))
} else {
Ok(None)
}
});
fields.add_field_method_get("Object", |_, this| {
if let ContentType::Object(referent) = &this.0 {
Ok(Instance::new_opt(*referent))
} else {
Ok(None)
}
});
}
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 Content {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Regardless of the actual content of the Content, Roblox just emits
// `Content` when casting it to a string. We do not do that.
write!(f, "Content(")?;
match &self.0 {
ContentType::None => write!(f, "None")?,
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
ContentType::Object(_) => write!(f, "Object")?,
other => write!(f, "UnknownType({other:?})")?,
}
write!(f, ")")
}
}
impl From<DomContent> for Content {
fn from(value: DomContent) -> Self {
Self(value.value().clone())
}
}
impl From<Content> for DomContent {
fn from(value: Content) -> Self {
match value.0 {
ContentType::None => Self::none(),
ContentType::Uri(uri) => Self::from_uri(uri),
ContentType::Object(referent) => Self::from_referent(referent),
other => unimplemented!("unknown variant of ContentType: {other:?}"),
}
}
}

View file

@ -1,7 +1,7 @@
use core::fmt; use core::fmt;
use mlua::prelude::*; use mlua::prelude::*;
use rbx_dom_weak::types::Enum as DomEnum; use rbx_dom_weak::types::EnumItem as DomEnumItem;
use super::{super::*, Enum}; use super::{super::*, Enum};
@ -100,8 +100,18 @@ impl PartialEq for EnumItem {
} }
} }
impl From<EnumItem> for DomEnum { impl From<EnumItem> for DomEnumItem {
fn from(v: EnumItem) -> Self { fn from(v: EnumItem) -> Self {
DomEnum::from_u32(v.value) DomEnumItem {
ty: v.parent.desc.name.to_string(),
value: v.value,
}
}
}
impl From<DomEnumItem> for EnumItem {
fn from(value: DomEnumItem) -> Self {
EnumItem::from_enum_name_and_value(value.ty, value.value)
.expect("cannot convert rbx_type::EnumItem with unknown type into EnumItem")
} }
} }

View file

@ -4,6 +4,7 @@ mod cframe;
mod color3; mod color3;
mod color_sequence; mod color_sequence;
mod color_sequence_keypoint; mod color_sequence_keypoint;
mod content;
mod r#enum; mod r#enum;
mod r#enum_item; mod r#enum_item;
mod r#enums; mod r#enums;
@ -30,6 +31,7 @@ pub use cframe::CFrame;
pub use color3::Color3; pub use color3::Color3;
pub use color_sequence::ColorSequence; pub use color_sequence::ColorSequence;
pub use color_sequence_keypoint::ColorSequenceKeypoint; pub use color_sequence_keypoint::ColorSequenceKeypoint;
pub use content::Content;
pub use faces::Faces; pub use faces::Faces;
pub use font::Font; pub use font::Font;
pub use number_range::NumberRange; pub use number_range::NumberRange;

View file

@ -52,6 +52,9 @@ impl LuaUserData for Vector2 {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
// Methods // Methods
methods.add_method("Angle", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(this.0.angle_between(rhs.0))
});
methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector2>| { methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector2>| {
let this_v3 = Vec3::new(this.0.x, this.0.y, 0f32); let this_v3 = Vec3::new(this.0.x, this.0.y, 0f32);
let rhs_v3 = Vec3::new(rhs.0.x, rhs.0.y, 0f32); let rhs_v3 = Vec3::new(rhs.0.x, rhs.0.y, 0f32);
@ -60,6 +63,14 @@ impl LuaUserData for Vector2 {
methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector2>| { methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(this.0.dot(rhs.0)) Ok(this.0.dot(rhs.0))
}); });
methods.add_method(
"FuzzyEq",
|_, this, (rhs, epsilon): (LuaUserDataRef<Vector2>, f32)| {
let eq_x = (rhs.0.x - this.0.x).abs() <= epsilon;
let eq_y = (rhs.0.y - this.0.y).abs() <= epsilon;
Ok(eq_x && eq_y)
},
);
methods.add_method( methods.add_method(
"Lerp", "Lerp",
|_, this, (rhs, alpha): (LuaUserDataRef<Vector2>, f32)| { |_, this, (rhs, alpha): (LuaUserDataRef<Vector2>, f32)| {
@ -72,6 +83,10 @@ impl LuaUserData for Vector2 {
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector2>| { methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(Vector2(this.0.min(rhs.0))) Ok(Vector2(this.0.min(rhs.0)))
}); });
methods.add_method("Abs", |_, this, ()| Ok(Vector2(this.0.abs())));
methods.add_method("Ceil", |_, this, ()| Ok(Vector2(this.0.ceil())));
methods.add_method("Floor", |_, this, ()| Ok(Vector2(this.0.floor())));
methods.add_method("Sign", |_, this, ()| Ok(Vector2(this.0.signum())));
// Metamethods // Metamethods
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);

View file

@ -133,6 +133,10 @@ impl LuaUserData for Vector3 {
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector3>| { methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector3>| {
Ok(Vector3(this.0.min(rhs.0))) Ok(Vector3(this.0.min(rhs.0)))
}); });
methods.add_method("Abs", |_, this, ()| Ok(Vector3(this.0.abs())));
methods.add_method("Ceil", |_, this, ()| Ok(Vector3(this.0.ceil())));
methods.add_method("Floor", |_, this, ()| Ok(Vector3(this.0.floor())));
methods.add_method("Sign", |_, this, ()| Ok(Vector3(this.0.signum())));
// Metamethods // Metamethods
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);

View file

@ -65,7 +65,7 @@ impl DocumentKind {
for child_ref in dom.root().children() { for child_ref in dom.root().children() {
if let Some(child_inst) = dom.get_by_ref(*child_ref) { if let Some(child_inst) = dom.get_by_ref(*child_ref) {
has_top_level_child = true; has_top_level_child = true;
if class_is_a_service(&child_inst.class).unwrap_or(false) { if class_is_a_service(child_inst.class).unwrap_or(false) {
has_top_level_service = true; has_top_level_service = true;
break; break;
} }

View file

@ -1,6 +1,6 @@
use rbx_dom_weak::{ use rbx_dom_weak::{
types::{Ref as DomRef, VariantType as DomType}, types::{Ref as DomRef, VariantType as DomType},
Instance as DomInstance, WeakDom, ustr, Instance as DomInstance, WeakDom,
}; };
use crate::shared::instance::class_is_a; use crate::shared::instance::class_is_a;
@ -18,8 +18,8 @@ pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
remove_matching_prop(inst, DomType::UniqueId, "HistoryId"); remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
// Similar story with ScriptGuid - this is used // Similar story with ScriptGuid - this is used
// in the studio-only cloud script drafts feature // in the studio-only cloud script drafts feature
if class_is_a(&inst.class, "LuaSourceContainer").unwrap_or(false) { if class_is_a(inst.class, "LuaSourceContainer").unwrap_or(false) {
inst.properties.remove("ScriptGuid"); inst.properties.remove(&ustr("ScriptGuid"));
} }
}); });
} }
@ -41,7 +41,8 @@ where
} }
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) { fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
if inst.properties.get(name).map_or(false, |u| u.ty() == ty) { let name = &ustr(name);
if inst.properties.get(name).is_some_and(|u| u.ty() == ty) {
inst.properties.remove(name); inst.properties.remove(name);
} }
} }

View file

@ -71,7 +71,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
"FindFirstAncestorWhichIsA", "FindFirstAncestorWhichIsA",
|lua, this, class_name: String| { |lua, this, class_name: String| {
ensure_not_destroyed(this)?; ensure_not_destroyed(this)?;
this.find_ancestor(|child| class_is_a(&child.class, &class_name).unwrap_or(false)) this.find_ancestor(|child| class_is_a(child.class, &class_name).unwrap_or(false))
.into_lua(lua) .into_lua(lua)
}, },
); );
@ -104,7 +104,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|lua, this, (class_name, recursive): (String, Option<bool>)| { |lua, this, (class_name, recursive): (String, Option<bool>)| {
ensure_not_destroyed(this)?; ensure_not_destroyed(this)?;
let predicate = let predicate =
|child: &DomInstance| class_is_a(&child.class, &class_name).unwrap_or(false); |child: &DomInstance| class_is_a(child.class, &class_name).unwrap_or(false);
if matches!(recursive, Some(true)) { if matches!(recursive, Some(true)) {
this.find_descendant(predicate).into_lua(lua) this.find_descendant(predicate).into_lua(lua)
} else { } else {
@ -113,8 +113,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
}, },
); );
m.add_method("IsA", |_, this, class_name: String| { m.add_method("IsA", |_, this, class_name: String| {
ensure_not_destroyed(this)?; Ok(class_is_a(this.class_name, class_name).unwrap_or(false))
Ok(class_is_a(&this.class_name, class_name).unwrap_or(false))
}); });
m.add_method( m.add_method(
"IsAncestorOf", "IsAncestorOf",
@ -217,20 +216,21 @@ fn instance_property_get<'lua>(
this: &Instance, this: &Instance,
prop_name: String, prop_name: String,
) -> LuaResult<LuaValue<'lua>> { ) -> LuaResult<LuaValue<'lua>> {
ensure_not_destroyed(this)?;
match prop_name.as_str() { match prop_name.as_str() {
"ClassName" => return this.get_class_name().into_lua(lua), "ClassName" => return this.get_class_name().into_lua(lua),
"Name" => {
return this.get_name().into_lua(lua);
}
"Parent" => { "Parent" => {
return this.get_parent().into_lua(lua); return this.get_parent().into_lua(lua);
} }
_ => {} _ => {}
} }
if let Some(info) = find_property_info(&this.class_name, &prop_name) { ensure_not_destroyed(this)?;
if prop_name.as_str() == "Name" {
return this.get_name().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 Some(prop) = this.get_property(&prop_name) {
if let DomValue::Enum(enum_value) = prop { if let DomValue::Enum(enum_value) = prop {
let enum_name = info.enum_name.ok_or_else(|| { let enum_name = info.enum_name.ok_or_else(|| {
@ -275,7 +275,7 @@ fn instance_property_get<'lua>(
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) { } else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
Ok(LuaValue::UserData(lua.create_userdata(inst)?)) Ok(LuaValue::UserData(lua.create_userdata(inst)?))
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) { } else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
getter.call(this.clone()) getter.call(*this)
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) { } else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
Ok(LuaValue::Function(method)) Ok(LuaValue::Function(method))
} else { } else {
@ -321,17 +321,17 @@ fn instance_property_set<'lua>(
} }
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>; type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
let parent = Parent::from_lua(prop_value, lua)?; let parent = Parent::from_lua(prop_value, lua)?;
this.set_parent(parent.map(|p| p.clone())); this.set_parent(parent.map(|p| *p));
return Ok(()); return Ok(());
} }
_ => {} _ => {}
} }
if let Some(info) = find_property_info(&this.class_name, &prop_name) { if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(enum_name) = info.enum_name { if let Some(enum_name) = info.enum_name {
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) { match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
Ok(given_enum) if given_enum.parent.desc.name == enum_name => { Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
this.set_property(prop_name, DomValue::Enum((*given_enum).clone().into())); this.set_property(prop_name, DomValue::EnumItem((*given_enum).clone().into()));
Ok(()) Ok(())
} }
Ok(given_enum) => Err(LuaError::RuntimeError(format!( Ok(given_enum) => Err(LuaError::RuntimeError(format!(
@ -354,7 +354,7 @@ fn instance_property_set<'lua>(
))) )))
} }
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) { } else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
setter.call((this.clone(), prop_value)) setter.call((*this, prop_value))
} else { } else {
Err(LuaError::RuntimeError(format!( Err(LuaError::RuntimeError(format!(
"{prop_name} is not a valid member of {this}", "{prop_name} is not a valid member of {this}",

View file

@ -48,7 +48,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua
Ok(service) Ok(service)
} else { } else {
let service = Instance::new_orphaned(service_name); let service = Instance::new_orphaned(service_name);
service.set_parent(Some(this.clone())); service.set_parent(Some(*this));
Ok(service) Ok(service)
} }
} }

View file

@ -11,7 +11,7 @@ use mlua::prelude::*;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rbx_dom_weak::{ use rbx_dom_weak::{
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue}, types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom, ustr, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, Ustr, WeakDom,
}; };
use lune_utils::TableBuilder; use lune_utils::TableBuilder;
@ -34,10 +34,10 @@ const PROPERTY_NAME_TAGS: &str = "Tags";
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> = static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT")))); Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
#[derive(Debug, Clone)] #[derive(Debug, Clone, Copy)]
pub struct Instance { pub struct Instance {
pub(crate) dom_ref: DomRef, pub(crate) dom_ref: DomRef,
pub(crate) class_name: String, pub(crate) class_name: Ustr,
} }
impl Instance { impl Instance {
@ -45,38 +45,26 @@ impl Instance {
Creates a new `Instance` from an existing dom object ref. Creates a new `Instance` from an existing dom object ref.
Panics if the instance does not exist in the internal dom, Panics if the instance does not exist in the internal dom,
or if the given dom object ref points to the dom root. or if the given dom object ref points to the internal dom root.
**WARNING:** Creating a new instance requires locking the internal dom, **WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking. any existing lock must first be released to prevent any deadlocking.
*/ */
pub(crate) fn new(dom_ref: DomRef) -> Self { #[must_use]
let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); pub fn new(dom_ref: DomRef) -> Self {
Self::new_opt(dom_ref).expect("Failed to find instance in document")
let instance = dom
.get_by_ref(dom_ref)
.expect("Failed to find instance in document");
assert!(
!(instance.referent() == dom.root_ref()),
"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. 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. Panics if the given dom object ref points to the internal dom root.
**WARNING:** Creating a new instance requires locking the internal dom, **WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking. any existing lock must first be released to prevent any deadlocking.
*/ */
pub(crate) fn new_opt(dom_ref: DomRef) -> Option<Self> { #[must_use]
pub fn new_opt(dom_ref: DomRef) -> Option<Self> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
if let Some(instance) = dom.get_by_ref(dom_ref) { if let Some(instance) = dom.get_by_ref(dom_ref) {
@ -87,7 +75,7 @@ impl Instance {
Some(Self { Some(Self {
dom_ref, dom_ref,
class_name: instance.class.clone(), class_name: instance.class,
}) })
} else { } else {
None None
@ -97,24 +85,25 @@ impl Instance {
/** /**
Creates a new orphaned `Instance` with a given class name. Creates a new orphaned `Instance` with a given class name.
An orphaned instance is an instance at the root of a weak dom. An orphaned instance is an instance at the root of Lune's internal weak dom.
**WARNING:** Creating a new instance requires locking the internal dom, **WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking. any existing lock must first be released to prevent any deadlocking.
*/ */
pub(crate) fn new_orphaned(class_name: impl AsRef<str>) -> Self { #[must_use]
pub fn new_orphaned(class_name: impl AsRef<str>) -> Self {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let class_name = class_name.as_ref(); let class_name = class_name.as_ref();
let instance = DomInstanceBuilder::new(class_name.to_string()); let instance = DomInstanceBuilder::new(class_name);
let dom_root = dom.root_ref(); let dom_root = dom.root_ref();
let dom_ref = dom.insert(dom_root, instance); let dom_ref = dom.insert(dom_root, instance);
Self { Self {
dom_ref, dom_ref,
class_name: class_name.to_string(), class_name: ustr(class_name),
} }
} }
@ -122,10 +111,11 @@ impl Instance {
Creates a new orphaned `Instance` by transferring Creates a new orphaned `Instance` by transferring
it from an external weak dom to the internal one. it from an external weak dom to the internal one.
An orphaned instance is an instance at the root of a weak dom. An orphaned instance is an instance at the root of Lune's internal weak dom.
Panics if the given dom ref is the root dom ref of the external weak dom. Panics if the given dom ref is the root dom ref of the external weak dom.
*/ */
#[must_use]
pub fn from_external_dom(external_dom: &mut WeakDom, external_dom_ref: DomRef) -> Self { 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 mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let dom_root = dom.root_ref(); let dom_root = dom.root_ref();
@ -151,6 +141,12 @@ impl Instance {
cloned cloned
} }
/**
Clones multiple instances to an external weak dom.
This will place the instances as children of the
root of the weak dom, and return their referents.
*/
pub fn clone_multiple_into_external_dom( pub fn clone_multiple_into_external_dom(
referents: &[DomRef], referents: &[DomRef],
external_dom: &mut WeakDom, external_dom: &mut WeakDom,
@ -248,7 +244,7 @@ impl Instance {
on the Roblox Developer Hub on the Roblox Developer Hub
*/ */
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool { pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
class_is_a(&self.class_name, class_name).unwrap_or(false) class_is_a(self.class_name, class_name).unwrap_or(false)
} }
/** /**
@ -306,10 +302,7 @@ impl Instance {
pub fn get_parent(&self) -> Option<Instance> { pub fn get_parent(&self) -> Option<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let parent_ref = dom let parent_ref = dom.get_by_ref(self.dom_ref)?.parent();
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.parent();
if parent_ref == dom.root_ref() { if parent_ref == dom.root_ref() {
None None
@ -324,7 +317,7 @@ impl Instance {
If the provided parent is [`None`] the instance will become orphaned. If the provided parent is [`None`] the instance will become orphaned.
An orphaned instance is an instance at the root of a weak dom. An orphaned instance is an instance at the root of Lune's internal weak dom.
### See Also ### See Also
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent) * [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
@ -348,7 +341,7 @@ impl Instance {
.get_by_ref(self.dom_ref) .get_by_ref(self.dom_ref)
.expect("Failed to find instance in document") .expect("Failed to find instance in document")
.properties .properties
.get(name.as_ref()) .get(&ustr(name.as_ref()))
.cloned() .cloned()
} }
@ -365,7 +358,7 @@ impl Instance {
.get_by_ref_mut(self.dom_ref) .get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document") .expect("Failed to find instance in document")
.properties .properties
.insert(name.as_ref().to_string(), value); .insert(ustr(name.as_ref()), value);
} }
/** /**
@ -381,7 +374,7 @@ impl Instance {
.get_by_ref(self.dom_ref) .get_by_ref(self.dom_ref)
.expect("Failed to find instance in document"); .expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) = if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(PROPERTY_NAME_ATTRIBUTES) inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
{ {
attributes.get(name.as_ref()).cloned() attributes.get(name.as_ref()).cloned()
} else { } else {
@ -402,7 +395,7 @@ impl Instance {
.get_by_ref(self.dom_ref) .get_by_ref(self.dom_ref)
.expect("Failed to find instance in document"); .expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) = if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(PROPERTY_NAME_ATTRIBUTES) inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
{ {
attributes.clone().into_iter().collect() attributes.clone().into_iter().collect()
} else { } else {
@ -429,14 +422,14 @@ impl Instance {
value => value, value => value,
}; };
if let Some(DomValue::Attributes(attributes)) = if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES) inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
{ {
attributes.insert(name.as_ref().to_string(), value); attributes.insert(name.as_ref().to_string(), value);
} else { } else {
let mut attributes = DomAttributes::new(); let mut attributes = DomAttributes::new();
attributes.insert(name.as_ref().to_string(), value); attributes.insert(name.as_ref().to_string(), value);
inst.properties.insert( inst.properties.insert(
PROPERTY_NAME_ATTRIBUTES.to_string(), ustr(PROPERTY_NAME_ATTRIBUTES),
DomValue::Attributes(attributes), DomValue::Attributes(attributes),
); );
} }
@ -456,11 +449,11 @@ impl Instance {
.get_by_ref_mut(self.dom_ref) .get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document"); .expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) = if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES) inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
{ {
attributes.remove(name.as_ref()); attributes.remove(name.as_ref());
if attributes.is_empty() { if attributes.is_empty() {
inst.properties.remove(PROPERTY_NAME_ATTRIBUTES); inst.properties.remove(&ustr(PROPERTY_NAME_ATTRIBUTES));
} }
} }
} }
@ -477,11 +470,11 @@ impl Instance {
let inst = dom let inst = dom
.get_by_ref_mut(self.dom_ref) .get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document"); .expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) { if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
tags.push(name.as_ref()); tags.push(name.as_ref());
} else { } else {
inst.properties.insert( inst.properties.insert(
PROPERTY_NAME_TAGS.to_string(), ustr(PROPERTY_NAME_TAGS),
DomValue::Tags(vec![name.as_ref().to_string()].into()), DomValue::Tags(vec![name.as_ref().to_string()].into()),
); );
} }
@ -499,7 +492,7 @@ impl Instance {
let inst = dom let inst = dom
.get_by_ref(self.dom_ref) .get_by_ref(self.dom_ref)
.expect("Failed to find instance in document"); .expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) { if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
tags.iter().map(ToString::to_string).collect() tags.iter().map(ToString::to_string).collect()
} else { } else {
Vec::new() Vec::new()
@ -518,7 +511,7 @@ impl Instance {
let inst = dom let inst = dom
.get_by_ref(self.dom_ref) .get_by_ref(self.dom_ref)
.expect("Failed to find instance in document"); .expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) { if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
let name = name.as_ref(); let name = name.as_ref();
tags.iter().any(|tag| tag == name) tags.iter().any(|tag| tag == name)
} else { } else {
@ -538,14 +531,12 @@ impl Instance {
let inst = dom let inst = dom
.get_by_ref_mut(self.dom_ref) .get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document"); .expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) { if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
let name = name.as_ref(); let name = name.as_ref();
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>(); let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
new_tags.retain(|tag| tag != name); new_tags.retain(|tag| tag != name);
inst.properties.insert( inst.properties
PROPERTY_NAME_TAGS.to_string(), .insert(ustr(PROPERTY_NAME_TAGS), DomValue::Tags(new_tags.into()));
DomValue::Tags(new_tags.into()),
);
} }
} }

View file

@ -27,9 +27,8 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M)
} }
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors { fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors") if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") {
{ inner
material_colors
} else { } else {
MaterialColors::default() MaterialColors::default()
} }

View file

@ -25,6 +25,7 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
export::<Color3>(lua)?, export::<Color3>(lua)?,
export::<ColorSequence>(lua)?, export::<ColorSequence>(lua)?,
export::<ColorSequenceKeypoint>(lua)?, export::<ColorSequenceKeypoint>(lua)?,
export::<Content>(lua)?,
export::<Faces>(lua)?, export::<Faces>(lua)?,
export::<Font>(lua)?, export::<Font>(lua)?,
export::<NumberRange>(lua)?, export::<NumberRange>(lua)?,

View file

@ -122,7 +122,7 @@ pub(crate) fn get_or_create_property_ref_instance(
Ok(inst) Ok(inst)
} else { } else {
let inst = Instance::new_orphaned(class_name); let inst = Instance::new_orphaned(class_name);
inst.set_parent(Some(this.clone())); inst.set_parent(Some(*this));
this.set_property(prop_name, DomValue::Ref(inst.dom_ref)); this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
Ok(inst) Ok(inst)
} }

View file

@ -23,7 +23,7 @@ pub fn make_list_writer() -> Box<ListWriter> {
}) })
} }
/** /*
Userdata metamethod implementations Userdata metamethod implementations
Note that many of these return [`LuaResult`] even though they don't Note that many of these return [`LuaResult`] even though they don't

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-datetime" name = "lune-std-datetime"
version = "0.1.2" version = "0.1.3"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,10 +13,10 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
thiserror = "1.0" thiserror = "1.0"
chrono = "0.4.38" chrono = "0.4.38"
chrono_lc = "0.1.6" chrono_lc = "0.1.6"
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -60,7 +60,7 @@ where
} }
} }
/** /*
Conversion methods between `DateTimeValues` and plain lua tables Conversion methods between `DateTimeValues` and plain lua tables
Note that the `IntoLua` implementation here uses a read-only table, Note that the `IntoLua` implementation here uses a read-only table,
@ -117,7 +117,7 @@ impl IntoLua<'_> for DateTimeValues {
} }
} }
/** /*
Conversion methods between chrono's timezone-aware `DateTime` to Conversion methods between chrono's timezone-aware `DateTime` to
and from our non-timezone-aware `DateTimeValues` values struct and from our non-timezone-aware `DateTimeValues` values struct
*/ */

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-fs" name = "lune-std-fs"
version = "0.1.1" version = "0.1.2"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,11 +13,11 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
bstr = "1.9" bstr = "1.9"
tokio = { version = "1", default-features = false, features = ["fs"] } tokio = { version = "1", default-features = false, features = ["fs"] }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-datetime = { version = "0.1.1", path = "../lune-std-datetime" } lune-std-datetime = { version = "0.1.2", path = "../lune-std-datetime" }

25
crates/lune-std-fs/src/lib.rs Normal file → Executable file
View file

@ -1,7 +1,7 @@
#![allow(clippy::cargo_common_metadata)] #![allow(clippy::cargo_common_metadata)]
use std::io::ErrorKind as IoErrorKind; use std::io::ErrorKind as IoErrorKind;
use std::path::{PathBuf, MAIN_SEPARATOR}; use std::path::PathBuf;
use bstr::{BString, ByteSlice}; use bstr::{BString, ByteSlice};
use mlua::prelude::*; use mlua::prelude::*;
@ -50,29 +50,16 @@ async fn fs_read_dir(_: &Lua, path: String) -> LuaResult<Vec<String>> {
let mut dir_strings = Vec::new(); let mut dir_strings = Vec::new();
let mut dir = fs::read_dir(&path).await.into_lua_err()?; let mut dir = fs::read_dir(&path).await.into_lua_err()?;
while let Some(dir_entry) = dir.next_entry().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() { if let Some(dir_name_str) = dir_entry.file_name().to_str() {
dir_strings.push(dir_path_str.to_owned()); dir_strings.push(dir_name_str.to_owned());
} else { } else {
return Err(LuaError::RuntimeError(format!( return Err(LuaError::RuntimeError(format!(
"File path could not be converted into a string: '{}'", "File name could not be converted into a string: '{}'",
dir_entry.path().display() dir_entry.file_name().to_string_lossy()
))); )));
} }
} }
let mut dir_string_prefix = path; Ok(dir_strings)
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<()> { async fn fs_write_file(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> {

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-luau" name = "lune-std-luau"
version = "0.1.1" version = "0.1.2"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,6 +13,6 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau", "luau-jit"] } mlua = { version = "0.9.9", features = ["luau", "luau-jit"] }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -2,7 +2,7 @@
use mlua::prelude::*; use mlua::prelude::*;
use lune_utils::TableBuilder; use lune_utils::{jit::JitStatus, TableBuilder};
mod options; mod options;
@ -78,7 +78,13 @@ fn load_source<'lua>(
// changed, otherwise disable JIT since it'll fall back anyways // changed, otherwise disable JIT since it'll fall back anyways
lua.enable_jit(options.codegen_enabled && !env_changed); lua.enable_jit(options.codegen_enabled && !env_changed);
let function = chunk.into_function()?; let function = chunk.into_function()?;
lua.enable_jit(true); lua.enable_jit(
lua.app_data_ref::<JitStatus>()
.ok_or(LuaError::runtime(
"Failed to get current JitStatus ref from AppData",
))?
.enabled(),
);
Ok(function) Ok(function)
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-net" name = "lune-std-net"
version = "0.1.1" version = "0.1.2"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,7 +13,7 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" } mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
bstr = "1.9" bstr = "1.9"
@ -35,5 +35,5 @@ tokio = { version = "1", default-features = false, features = [
"macros", "macros",
] } ] }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-serde = { version = "0.1.1", path = "../lune-std-serde" } lune-std-serde = { version = "0.1.2", path = "../lune-std-serde" }

View file

@ -65,9 +65,9 @@ async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
res.await?.into_lua_table(lua) res.await?.into_lua_table(lua)
} }
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaTable> { async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaValue> {
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?; let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
NetWebSocket::new(ws).into_lua_table(lua) NetWebSocket::new(ws).into_lua(lua)
} }
async fn net_serve<'lua>( async fn net_serve<'lua>(

View file

@ -18,6 +18,7 @@ impl LuaRequest {
let path = self.head.uri.path().to_string(); let path = self.head.uri.path().to_string();
let body = lua.create_string(&self.body)?; let body = lua.create_string(&self.body)?;
#[allow(clippy::mutable_key_type)]
let query: HashMap<LuaString, LuaString> = self let query: HashMap<LuaString, LuaString> = self
.head .head
.uri .uri
@ -32,6 +33,7 @@ impl LuaRequest {
}) })
.collect::<LuaResult<_>>()?; .collect::<LuaResult<_>>()?;
#[allow(clippy::mutable_key_type)]
let headers: HashMap<LuaString, LuaString> = self let headers: HashMap<LuaString, LuaString> = self
.head .head
.headers .headers

View file

@ -40,13 +40,13 @@ impl Service<Request<Incoming>> for Svc {
lua.spawn_local(async move { lua.spawn_local(async move {
let sock = sock.await.unwrap(); let sock = sock.await.unwrap();
let lua_sock = NetWebSocket::new(sock); let lua_sock = NetWebSocket::new(sock);
let lua_tab = lua_sock.into_lua_table(&lua_inner).unwrap(); let lua_val = lua_sock.into_lua(&lua_inner).unwrap();
let handler_websocket: LuaFunction = let handler_websocket: LuaFunction =
keys.websocket_handler(&lua_inner).unwrap().unwrap(); keys.websocket_handler(&lua_inner).unwrap().unwrap();
lua_inner lua_inner
.push_thread_back(handler_websocket, lua_tab) .push_thread_back(handler_websocket, lua_val)
.unwrap(); .unwrap();
}); });

View file

@ -23,29 +23,6 @@ use hyper_tungstenite::{
WebSocketStream, WebSocketStream,
}; };
use lune_utils::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)] #[derive(Debug)]
pub struct NetWebSocket<T> { pub struct NetWebSocket<T> {
close_code_exists: Arc<AtomicBool>, close_code_exists: Arc<AtomicBool>,
@ -125,25 +102,6 @@ where
let mut ws = self.write_stream.lock().await; let mut ws = self.write_stream.lock().await;
ws.close().await.into_lua_err() 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> impl<T> LuaUserData for NetWebSocket<T>

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-process" name = "lune-std-process"
version = "0.1.2" version = "0.1.3"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,13 +13,16 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" } mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
directories = "5.0" directories = "5.0"
pin-project = "1.0" pin-project = "1.0"
os_str_bytes = { version = "7.0", features = ["conversions"] } os_str_bytes = { version = "7.0", features = ["conversions"] }
bstr = "1.9"
bytes = "1.6.0"
tokio = { version = "1", default-features = false, features = [ tokio = { version = "1", default-features = false, features = [
"io-std", "io-std",
"io-util", "io-util",
@ -28,4 +31,4 @@ tokio = { version = "1", default-features = false, features = [
"sync", "sync",
] } ] }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,27 +1,33 @@
#![allow(clippy::cargo_common_metadata)] #![allow(clippy::cargo_common_metadata)]
use std::{ use std::{
cell::RefCell,
env::{ env::{
self, self,
consts::{ARCH, OS}, consts::{ARCH, OS},
}, },
path::MAIN_SEPARATOR, path::MAIN_SEPARATOR,
process::Stdio, process::Stdio,
rc::Rc,
sync::Arc,
}; };
use mlua::prelude::*; use mlua::prelude::*;
use lune_utils::TableBuilder; use lune_utils::TableBuilder;
use mlua_luau_scheduler::{Functions, LuaSpawnExt}; use mlua_luau_scheduler::{Functions, LuaSpawnExt};
use options::ProcessSpawnOptionsStdio;
use os_str_bytes::RawOsString; use os_str_bytes::RawOsString;
use tokio::io::AsyncWriteExt; use stream::{ChildProcessReader, ChildProcessWriter};
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
mod options; mod options;
mod stream;
mod tee_writer; mod tee_writer;
mod wait_for_child; mod wait_for_child;
use self::options::ProcessSpawnOptions; use self::options::ProcessSpawnOptions;
use self::wait_for_child::{wait_for_child, WaitForChildResult}; use self::wait_for_child::wait_for_child;
use lune_utils::path::get_current_dir; use lune_utils::path::get_current_dir;
@ -42,8 +48,13 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
cwd_str.push(MAIN_SEPARATOR); cwd_str.push(MAIN_SEPARATOR);
} }
// Create constants for OS & processor architecture // Create constants for OS & processor architecture
let os = lua.create_string(&OS.to_lowercase())?; let os = lua.create_string(OS.to_lowercase())?;
let arch = lua.create_string(&ARCH.to_lowercase())?; let arch = lua.create_string(ARCH.to_lowercase())?;
let endianness = lua.create_string(if cfg!(target_endian = "big") {
"big"
} else {
"little"
})?;
// Create readonly args array // Create readonly args array
let args_vec = lua let args_vec = lua
.app_data_ref::<Vec<String>>() .app_data_ref::<Vec<String>>()
@ -69,11 +80,13 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)? TableBuilder::new(lua)?
.with_value("os", os)? .with_value("os", os)?
.with_value("arch", arch)? .with_value("arch", arch)?
.with_value("endianness", endianness)?
.with_value("args", args_tab)? .with_value("args", args_tab)?
.with_value("cwd", cwd_str)? .with_value("cwd", cwd_str)?
.with_value("env", env_tab)? .with_value("env", env_tab)?
.with_value("exit", process_exit)? .with_value("exit", process_exit)?
.with_async_function("spawn", process_spawn)? .with_async_function("exec", process_exec)?
.with_function("create", process_create)?
.build_readonly() .build_readonly()
} }
@ -141,11 +154,16 @@ fn process_env_iter<'lua>(
}) })
} }
async fn process_spawn( async fn process_exec(
lua: &Lua, lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions), (program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> { ) -> LuaResult<LuaTable> {
let res = lua.spawn(spawn_command(program, args, options)).await?; let res = lua
.spawn(async move {
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
})
.await?;
/* /*
NOTE: If an exit code was not given by the child process, NOTE: If an exit code was not given by the child process,
@ -168,30 +186,104 @@ async fn process_spawn(
.build_readonly() .build_readonly()
} }
async fn spawn_command( #[allow(clippy::await_holding_refcell_ref)]
fn process_create(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
// We do not want the user to provide stdio options for process.create,
// so we reset the options, regardless of what the user provides us
let mut spawn_options = options.clone();
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
let code_rx_rc = Rc::new(RefCell::new(code_rx));
let child = spawn_command(program, args, spawn_options)?;
let child_arc = Arc::new(RwLock::new(child));
let child_arc_clone = Arc::clone(&child_arc);
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
let stdin = child_lock.stdin.take().unwrap();
let stdout = child_lock.stdout.take().unwrap();
let stderr = child_lock.stderr.take().unwrap();
let child_arc_inner = Arc::clone(&child_arc);
// Spawn a background task to wait for the child to exit and send the exit code
let status_handle = tokio::spawn(async move {
let res = child_arc_inner.write().await.wait().await;
if let Ok(output) = res {
let code = output.code().unwrap_or_default();
code_tx
.send(code)
.expect("ExitCode receiver was unexpectedly dropped");
}
});
TableBuilder::new(lua)?
.with_value("stdout", ChildProcessReader(stdout))?
.with_value("stderr", ChildProcessReader(stderr))?
.with_value("stdin", ChildProcessWriter(stdin))?
.with_async_function("kill", move |_, ()| {
// First, stop the status task so the RwLock is dropped
status_handle.abort();
let child_arc_clone = Arc::clone(&child_arc);
// Then get another RwLock to write to the child process and kill it
async move { Ok(child_arc_clone.write().await.kill().await?) }
})?
.with_async_function("status", move |lua, ()| {
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
async move {
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
// the receiver gets suddenly dropped
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
TableBuilder::new(lua)?
.with_value("code", code)?
.with_value("ok", code == 0)?
.build_readonly()
}
})?
.build_readonly()
}
async fn spawn_command_with_stdin(
program: String, program: String,
args: Option<Vec<String>>, args: Option<Vec<String>>,
mut options: ProcessSpawnOptions, mut options: ProcessSpawnOptions,
) -> LuaResult<WaitForChildResult> { ) -> LuaResult<Child> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let stdin = options.stdio.stdin.take(); let stdin = options.stdio.stdin.take();
let mut child = options let mut child = spawn_command(program, args, options)?;
.into_command(program, args)
.stdin(if stdin.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
if let Some(stdin) = stdin { if let Some(stdin) = stdin {
let mut child_stdin = child.stdin.take().unwrap(); let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(&stdin).await.into_lua_err()?; child_stdin.write_all(&stdin).await.into_lua_err()?;
} }
wait_for_child(child, stdout, stderr).await Ok(child)
}
fn spawn_command(
program: String,
args: Option<Vec<String>>,
options: ProcessSpawnOptions,
) -> LuaResult<Child> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let child = options
.into_command(program, args)
.stdin(Stdio::piped())
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
Ok(child)
} }

View file

@ -0,0 +1,58 @@
use bstr::BString;
use bytes::BytesMut;
use mlua::prelude::*;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
const CHUNK_SIZE: usize = 8;
#[derive(Debug, Clone)]
pub struct ChildProcessReader<R: AsyncRead>(pub R);
#[derive(Debug, Clone)]
pub struct ChildProcessWriter<W: AsyncWrite>(pub W);
impl<R: AsyncRead + Unpin> ChildProcessReader<R> {
pub async fn read(&mut self, chunk_size: Option<usize>) -> LuaResult<Vec<u8>> {
let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE));
self.0.read_buf(&mut buf).await?;
Ok(buf.to_vec())
}
pub async fn read_to_end(&mut self) -> LuaResult<Vec<u8>> {
let mut buf = vec![];
self.0.read_to_end(&mut buf).await?;
Ok(buf)
}
}
impl<R: AsyncRead + Unpin + 'static> LuaUserData for ChildProcessReader<R> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("read", |lua, this, chunk_size: Option<usize>| async move {
let buf = this.read(chunk_size).await?;
if buf.is_empty() {
return Ok(LuaValue::Nil);
}
Ok(LuaValue::String(lua.create_string(buf)?))
});
methods.add_async_method_mut("readToEnd", |lua, this, ()| async {
Ok(lua.create_string(this.read_to_end().await?))
});
}
}
impl<W: AsyncWrite + Unpin> ChildProcessWriter<W> {
pub async fn write(&mut self, data: BString) -> LuaResult<()> {
self.0.write_all(data.as_ref()).await?;
Ok(())
}
}
impl<W: AsyncWrite + Unpin + 'static> LuaUserData for ChildProcessWriter<W> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await });
}
}

View file

@ -33,7 +33,7 @@ where
} }
} }
impl<'a, W> AsyncWrite for AsyncTeeWriter<'a, W> impl<W> AsyncWrite for AsyncTeeWriter<'_, W>
where where
W: AsyncWrite + Unpin, W: AsyncWrite + Unpin,
{ {

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-regex" name = "lune-std-regex"
version = "0.1.1" version = "0.1.2"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,9 +13,9 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
regex = "1.10" regex = "1.10"
self_cell = "1.0" self_cell = "1.0"
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-roblox" name = "lune-std-roblox"
version = "0.1.2" version = "0.1.4"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,11 +13,12 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" } mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
once_cell = "1.17" once_cell = "1.17"
rbx_cookie = { version = "0.1.4", default-features = false } rbx_cookie = { version = "0.1.4", default-features = false }
roblox_install = "1.0.0"
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-roblox = { version = "0.1.2", path = "../lune-roblox" } lune-roblox = { version = "0.1.4", path = "../lune-roblox" }

View file

@ -13,6 +13,7 @@ use lune_roblox::{
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new(); static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
use lune_utils::TableBuilder; use lune_utils::TableBuilder;
use roblox_install::RobloxStudio;
/** /**
Creates the `roblox` standard library module. Creates the `roblox` standard library module.
@ -39,6 +40,10 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.with_function("getReflectionDatabase", get_reflection_database)? .with_function("getReflectionDatabase", get_reflection_database)?
.with_function("implementProperty", implement_property)? .with_function("implementProperty", implement_property)?
.with_function("implementMethod", implement_method)? .with_function("implementMethod", implement_method)?
.with_function("studioApplicationPath", studio_application_path)?
.with_function("studioContentPath", studio_content_path)?
.with_function("studioPluginPath", studio_plugin_path)?
.with_function("studioBuiltinPluginPath", studio_builtin_plugin_path)?
.build_readonly() .build_readonly()
} }
@ -72,7 +77,7 @@ async fn serialize_place<'lua>(
lua: &'lua Lua, lua: &'lua Lua,
(data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>), (data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>),
) -> LuaResult<LuaString<'lua>> { ) -> LuaResult<LuaString<'lua>> {
let data_model = (*data_model).clone(); let data_model = *data_model;
let fut = lua.spawn_blocking(move || { let fut = lua.spawn_blocking(move || {
let doc = Document::from_data_model_instance(data_model)?; let doc = Document::from_data_model_instance(data_model)?;
let bytes = doc.to_bytes_with_format(match as_xml { let bytes = doc.to_bytes_with_format(match as_xml {
@ -89,7 +94,7 @@ async fn serialize_model<'lua>(
lua: &'lua Lua, lua: &'lua Lua,
(instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>), (instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>),
) -> LuaResult<LuaString<'lua>> { ) -> LuaResult<LuaString<'lua>> {
let instances = instances.iter().map(|i| (*i).clone()).collect(); let instances = instances.iter().map(|i| **i).collect();
let fut = lua.spawn_blocking(move || { let fut = lua.spawn_blocking(move || {
let doc = Document::from_instance_array(instances)?; let doc = Document::from_instance_array(instances)?;
let bytes = doc.to_bytes_with_format(match as_xml { let bytes = doc.to_bytes_with_format(match as_xml {
@ -147,3 +152,27 @@ fn implement_method(
InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?; InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?;
Ok(()) Ok(())
} }
fn studio_application_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.application_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_content_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.content_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.plugins_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_builtin_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.built_in_plugins_path().display().to_string())
.map_err(LuaError::external)
}

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-serde" name = "lune-std-serde"
version = "0.1.1" version = "0.1.2"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,7 +13,7 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau", "serialize"] } mlua = { version = "0.9.9", features = ["luau", "serialize"] }
async-compression = { version = "0.4", features = [ async-compression = { version = "0.4", features = [
"tokio", "tokio",
@ -23,7 +23,7 @@ async-compression = { version = "0.4", features = [
"zlib", "zlib",
] } ] }
bstr = "1.9" bstr = "1.9"
lz4 = "1.24" lz4 = "1.26"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] } serde_json = { version = "1.0", features = ["preserve_order"] }
serde_yaml = "0.9" serde_yaml = "0.9"
@ -44,4 +44,4 @@ tokio = { version = "1", default-features = false, features = [
"io-util", "io-util",
] } ] }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -13,6 +13,7 @@ use async_compression::{
BrotliDecoder, BrotliEncoder, GzipDecoder, GzipEncoder, ZlibDecoder, ZlibEncoder, BrotliDecoder, BrotliEncoder, GzipDecoder, GzipEncoder, ZlibDecoder, ZlibEncoder,
}, },
Level::Best as CompressionQuality, Level::Best as CompressionQuality,
Level::Precise as PreciseCompressionQuality,
}; };
/** /**
@ -116,9 +117,10 @@ impl<'lua> FromLua<'lua> for CompressDecompressFormat {
Errors when the compression fails. Errors when the compression fails.
*/ */
pub async fn compress<'lua>( pub async fn compress(
source: impl AsRef<[u8]>, source: impl AsRef<[u8]>,
format: CompressDecompressFormat, format: CompressDecompressFormat,
level: Option<i32>,
) -> LuaResult<Vec<u8>> { ) -> LuaResult<Vec<u8>> {
if let CompressDecompressFormat::LZ4 = format { if let CompressDecompressFormat::LZ4 = format {
let source = source.as_ref().to_vec(); let source = source.as_ref().to_vec();
@ -130,18 +132,22 @@ pub async fn compress<'lua>(
let mut bytes = Vec::new(); let mut bytes = Vec::new();
let reader = BufReader::new(source.as_ref()); let reader = BufReader::new(source.as_ref());
let compression_quality = match level {
Some(l) => PreciseCompressionQuality(l),
None => CompressionQuality,
};
match format { match format {
CompressDecompressFormat::Brotli => { CompressDecompressFormat::Brotli => {
let mut encoder = BrotliEncoder::with_quality(reader, CompressionQuality); let mut encoder = BrotliEncoder::with_quality(reader, compression_quality);
copy(&mut encoder, &mut bytes).await?; copy(&mut encoder, &mut bytes).await?;
} }
CompressDecompressFormat::GZip => { CompressDecompressFormat::GZip => {
let mut encoder = GzipEncoder::with_quality(reader, CompressionQuality); let mut encoder = GzipEncoder::with_quality(reader, compression_quality);
copy(&mut encoder, &mut bytes).await?; copy(&mut encoder, &mut bytes).await?;
} }
CompressDecompressFormat::ZLib => { CompressDecompressFormat::ZLib => {
let mut encoder = ZlibEncoder::with_quality(reader, CompressionQuality); let mut encoder = ZlibEncoder::with_quality(reader, compression_quality);
copy(&mut encoder, &mut bytes).await?; copy(&mut encoder, &mut bytes).await?;
} }
CompressDecompressFormat::LZ4 => unreachable!(), CompressDecompressFormat::LZ4 => unreachable!(),
@ -157,7 +163,7 @@ pub async fn compress<'lua>(
Errors when the decompression fails. Errors when the decompression fails.
*/ */
pub async fn decompress<'lua>( pub async fn decompress(
source: impl AsRef<[u8]>, source: impl AsRef<[u8]>,
format: CompressDecompressFormat, format: CompressDecompressFormat,
) -> LuaResult<Vec<u8>> { ) -> LuaResult<Vec<u8>> {

View file

@ -46,9 +46,9 @@ fn serde_decode(lua: &Lua, (format, bs): (EncodeDecodeFormat, BString)) -> LuaRe
async fn serde_compress( async fn serde_compress(
lua: &Lua, lua: &Lua,
(format, bs): (CompressDecompressFormat, BString), (format, bs, level): (CompressDecompressFormat, BString, Option<i32>),
) -> LuaResult<LuaString> { ) -> LuaResult<LuaString> {
let bytes = compress(bs, format).await?; let bytes = compress(bs, format, level).await?;
lua.create_string(bytes) lua.create_string(bytes)
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-stdio" name = "lune-std-stdio"
version = "0.1.1" version = "0.1.2"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -14,7 +14,7 @@ workspace = true
[dependencies] [dependencies]
dialoguer = "0.11" dialoguer = "0.11"
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" } mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
tokio = { version = "1", default-features = false, features = [ tokio = { version = "1", default-features = false, features = [
@ -22,4 +22,4 @@ tokio = { version = "1", default-features = false, features = [
"io-util", "io-util",
] } ] }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -194,14 +194,14 @@ pub fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
prompt = prompt.default(b); prompt = prompt.default(b);
}; };
let result = prompt let result = prompt
.with_prompt(&options.text.expect("Missing text in prompt options")) .with_prompt(options.text.expect("Missing text in prompt options"))
.interact() .interact()
.into_lua_err()?; .into_lua_err()?;
Ok(PromptResult::Boolean(result)) Ok(PromptResult::Boolean(result))
} }
PromptKind::Select => { PromptKind::Select => {
let chosen = Select::with_theme(&theme) let chosen = Select::with_theme(&theme)
.with_prompt(&options.text.unwrap_or_default()) .with_prompt(options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options")) .items(&options.options.expect("Missing options in prompt options"))
.interact_opt() .interact_opt()
.into_lua_err()?; .into_lua_err()?;
@ -212,7 +212,7 @@ pub fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
} }
PromptKind::MultiSelect => { PromptKind::MultiSelect => {
let chosen = MultiSelect::with_theme(&theme) let chosen = MultiSelect::with_theme(&theme)
.with_prompt(&options.text.unwrap_or_default()) .with_prompt(options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options")) .items(&options.options.expect("Missing options in prompt options"))
.interact_opt() .interact_opt()
.into_lua_err()?; .into_lua_err()?;

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std-task" name = "lune-std-task"
version = "0.1.1" version = "0.1.2"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,9 +13,9 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" } mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
tokio = { version = "1", default-features = false, features = ["time"] } tokio = { version = "1", default-features = false, features = ["time"] }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-std" name = "lune-std"
version = "0.1.3" version = "0.1.5"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -38,22 +38,22 @@ stdio = ["dep:lune-std-stdio"]
task = ["dep:lune-std-task"] task = ["dep:lune-std-task"]
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" } mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tokio = { version = "1", default-features = false, features = ["fs", "sync"] } tokio = { version = "1", default-features = false, features = ["fs", "sync"] }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-datetime = { optional = true, version = "0.1.2", path = "../lune-std-datetime" } lune-std-datetime = { optional = true, version = "0.1.3", path = "../lune-std-datetime" }
lune-std-fs = { optional = true, version = "0.1.1", path = "../lune-std-fs" } lune-std-fs = { optional = true, version = "0.1.2", path = "../lune-std-fs" }
lune-std-luau = { optional = true, version = "0.1.1", path = "../lune-std-luau" } lune-std-luau = { optional = true, version = "0.1.2", path = "../lune-std-luau" }
lune-std-net = { optional = true, version = "0.1.1", path = "../lune-std-net" } lune-std-net = { optional = true, version = "0.1.2", path = "../lune-std-net" }
lune-std-process = { optional = true, version = "0.1.2", path = "../lune-std-process" } lune-std-process = { optional = true, version = "0.1.3", path = "../lune-std-process" }
lune-std-regex = { optional = true, version = "0.1.1", path = "../lune-std-regex" } lune-std-regex = { optional = true, version = "0.1.2", path = "../lune-std-regex" }
lune-std-roblox = { optional = true, version = "0.1.2", path = "../lune-std-roblox" } lune-std-roblox = { optional = true, version = "0.1.4", path = "../lune-std-roblox" }
lune-std-serde = { optional = true, version = "0.1.1", path = "../lune-std-serde" } lune-std-serde = { optional = true, version = "0.1.2", path = "../lune-std-serde" }
lune-std-stdio = { optional = true, version = "0.1.1", path = "../lune-std-stdio" } lune-std-stdio = { optional = true, version = "0.1.2", path = "../lune-std-stdio" }
lune-std-task = { optional = true, version = "0.1.1", path = "../lune-std-task" } lune-std-task = { optional = true, version = "0.1.2", path = "../lune-std-task" }

View file

@ -150,9 +150,9 @@ impl RequireContext {
self.get_from_cache(lua, abs_path.as_ref()) self.get_from_cache(lua, abs_path.as_ref())
} }
async fn load<'lua>( async fn load(
&self, &self,
lua: &'lua Lua, lua: &Lua,
abs_path: impl AsRef<Path>, abs_path: impl AsRef<Path>,
rel_path: impl AsRef<Path>, rel_path: impl AsRef<Path>,
) -> LuaResult<LuaRegistryKey> { ) -> LuaResult<LuaRegistryKey> {

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune-utils" name = "lune-utils"
version = "0.1.2" version = "0.1.3"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -13,7 +13,7 @@ path = "src/lib.rs"
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau", "async"] } mlua = { version = "0.9.9", features = ["luau", "async"] }
tokio = { version = "1", default-features = false, features = ["fs"] } tokio = { version = "1", default-features = false, features = ["fs"] }
@ -22,3 +22,5 @@ dunce = "1.0"
once_cell = "1.17" once_cell = "1.17"
path-clean = "1.0" path-clean = "1.0"
pathdiff = "0.2" pathdiff = "0.2"
parking_lot = "0.12.3"
semver = "1.0"

View file

@ -26,6 +26,11 @@ static STYLED_STACK_END: Lazy<String> = Lazy::new(|| {
) )
}); });
// NOTE: We indent using 4 spaces instead of tabs since
// these errors are most likely to be displayed in a terminal
// or some kind of live output - and tabs don't work well there
const STACK_TRACE_INDENT: &str = " ";
/** /**
Error components parsed from a [`LuaError`]. Error components parsed from a [`LuaError`].
@ -86,7 +91,7 @@ impl fmt::Display for ErrorComponents {
let trace = self.trace.as_ref().unwrap(); let trace = self.trace.as_ref().unwrap();
writeln!(f, "{}", *STYLED_STACK_BEGIN)?; writeln!(f, "{}", *STYLED_STACK_BEGIN)?;
for line in trace.lines() { for line in trace.lines() {
writeln!(f, "\t{line}")?; writeln!(f, "{STACK_TRACE_INDENT}{line}")?;
} }
writeln!(f, "{}", *STYLED_STACK_END)?; writeln!(f, "{}", *STYLED_STACK_END)?;
} }
@ -124,7 +129,7 @@ impl From<LuaError> for ErrorComponents {
} }
// We will then try to extract any stack trace // We will then try to extract any stack trace
let trace = if let LuaError::CallbackError { let mut trace = if let LuaError::CallbackError {
ref traceback, ref traceback,
ref cause, ref cause,
} = *error } = *error
@ -147,6 +152,45 @@ impl From<LuaError> for ErrorComponents {
None None
}; };
// Sometimes, we can get duplicate stack trace lines that only
// mention "[C]", without a function name or path, and these can
// be safely ignored / removed if the following line has more info
if let Some(trace) = &mut trace {
let lines = trace.lines_mut();
loop {
let first_is_c_and_empty = lines
.first()
.is_some_and(|line| line.source().is_c() && line.is_empty());
let second_is_c_and_nonempty = lines
.get(1)
.is_some_and(|line| line.source().is_c() && !line.is_empty());
if first_is_c_and_empty && second_is_c_and_nonempty {
lines.remove(0);
} else {
break;
}
}
}
// Finally, we do some light postprocessing to remove duplicate
// information, such as the location prefix in the error message
if let Some(message) = messages.last_mut() {
if let Some(line) = trace
.iter()
.flat_map(StackTrace::lines)
.find(|line| line.source().is_lua())
{
let location_prefix = format!(
"[string \"{}\"]:{}:",
line.path().unwrap(),
line.line_number().unwrap()
);
if message.starts_with(&location_prefix) {
*message = message[location_prefix.len()..].trim().to_string();
}
}
}
ErrorComponents { messages, trace } ErrorComponents { messages, trace }
} }
} }

View file

@ -39,6 +39,24 @@ pub enum StackTraceSource {
Lua, Lua,
} }
impl StackTraceSource {
/**
Returns `true` if the error originated from a C / Rust function, `false` otherwise.
*/
#[must_use]
pub const fn is_c(self) -> bool {
matches!(self, Self::C)
}
/**
Returns `true` if the error originated from a Lua (user) function, `false` otherwise.
*/
#[must_use]
pub const fn is_lua(self) -> bool {
matches!(self, Self::Lua)
}
}
/** /**
Stack trace line parsed from a [`LuaError`]. Stack trace line parsed from a [`LuaError`].
*/ */
@ -82,6 +100,20 @@ impl StackTraceLine {
pub fn function_name(&self) -> Option<&str> { pub fn function_name(&self) -> Option<&str> {
self.function_name.as_deref() self.function_name.as_deref()
} }
/**
Returns `true` if the stack trace line contains no "useful" information, `false` otherwise.
Useful information is determined as one of:
- A path
- A line number
- A function name
*/
#[must_use]
pub const fn is_empty(&self) -> bool {
self.path.is_none() && self.line_number.is_none() && self.function_name.is_none()
}
} }
impl FromStr for StackTraceLine { impl FromStr for StackTraceLine {
@ -145,6 +177,14 @@ impl StackTrace {
pub fn lines(&self) -> &[StackTraceLine] { pub fn lines(&self) -> &[StackTraceLine] {
&self.lines &self.lines
} }
/**
Returns the individual stack trace lines, mutably.
*/
#[must_use]
pub fn lines_mut(&mut self) -> &mut Vec<StackTraceLine> {
&mut self.lines
}
} }
impl FromStr for StackTrace { impl FromStr for StackTrace {

View file

@ -2,7 +2,7 @@ use mlua::prelude::*;
use crate::fmt::ErrorComponents; use crate::fmt::ErrorComponents;
fn new_lua_result() -> LuaResult<()> { fn new_lua_runtime_error() -> LuaResult<()> {
let lua = Lua::new(); let lua = Lua::new();
lua.globals() lua.globals()
@ -17,13 +17,34 @@ fn new_lua_result() -> LuaResult<()> {
lua.load("f()").set_name("chunk_name").eval() lua.load("f()").set_name("chunk_name").eval()
} }
fn new_lua_script_error() -> LuaResult<()> {
let lua = Lua::new();
lua.load(
"local function inner()\
\n error(\"oh no, a script error\")\
\nend\
\n\
\nlocal function outer()\
\n inner()\
\nend\
\n\
\nouter()\
",
)
.set_name("chunk_name")
.eval()
}
// Tests for error context stack // Tests for error context stack
mod context { mod context {
use super::*; use super::*;
#[test] #[test]
fn preserves_original() { fn preserves_original() {
let lua_error = new_lua_result().context("additional context").unwrap_err(); let lua_error = new_lua_runtime_error()
.context("additional context")
.unwrap_err();
let components = ErrorComponents::from(lua_error); let components = ErrorComponents::from(lua_error);
assert_eq!(components.messages()[0], "additional context"); assert_eq!(components.messages()[0], "additional context");
@ -34,7 +55,7 @@ mod context {
fn preserves_levels() { fn preserves_levels() {
// NOTE: The behavior in mlua is to preserve a single level of context // NOTE: The behavior in mlua is to preserve a single level of context
// and not all levels (context gets replaced on each call to `context`) // and not all levels (context gets replaced on each call to `context`)
let lua_error = new_lua_result() let lua_error = new_lua_runtime_error()
.context("level 1") .context("level 1")
.context("level 2") .context("level 2")
.context("level 3") .context("level 3")
@ -54,7 +75,7 @@ mod error_components {
#[test] #[test]
fn message() { fn message() {
let lua_error = new_lua_result().unwrap_err(); let lua_error = new_lua_runtime_error().unwrap_err();
let components = ErrorComponents::from(lua_error); let components = ErrorComponents::from(lua_error);
assert_eq!(components.messages()[0], "oh no, a runtime error"); assert_eq!(components.messages()[0], "oh no, a runtime error");
@ -62,7 +83,7 @@ mod error_components {
#[test] #[test]
fn stack_begin_end() { fn stack_begin_end() {
let lua_error = new_lua_result().unwrap_err(); let lua_error = new_lua_runtime_error().unwrap_err();
let formatted = format!("{}", ErrorComponents::from(lua_error)); let formatted = format!("{}", ErrorComponents::from(lua_error));
assert!(formatted.contains("Stack Begin")); assert!(formatted.contains("Stack Begin"));
@ -71,7 +92,7 @@ mod error_components {
#[test] #[test]
fn stack_lines() { fn stack_lines() {
let lua_error = new_lua_result().unwrap_err(); let lua_error = new_lua_runtime_error().unwrap_err();
let components = ErrorComponents::from(lua_error); let components = ErrorComponents::from(lua_error);
let mut lines = components.trace().unwrap().lines().iter(); let mut lines = components.trace().unwrap().lines().iter();
@ -83,3 +104,47 @@ mod error_components {
assert_eq!(line_2, "Script 'chunk_name', Line 1"); assert_eq!(line_2, "Script 'chunk_name', Line 1");
} }
} }
// Tests for general formatting
mod general {
use super::*;
#[test]
fn message_does_not_contain_location() {
let lua_error = new_lua_script_error().unwrap_err();
let components = ErrorComponents::from(lua_error);
let trace = components.trace().unwrap();
let first_message = components.messages().first().unwrap();
let first_lua_stack_line = trace
.lines()
.iter()
.find(|line| line.source().is_lua())
.unwrap();
let location_prefix = format!(
"[string \"{}\"]:{}:",
first_lua_stack_line.path().unwrap(),
first_lua_stack_line.line_number().unwrap()
);
assert!(!first_message.starts_with(&location_prefix));
}
#[test]
fn no_redundant_c_mentions() {
let lua_error = new_lua_script_error().unwrap_err();
let components = ErrorComponents::from(lua_error);
let trace = components.trace().unwrap();
let c_stack_lines = trace
.lines()
.iter()
.filter(|line| line.source().is_c())
.collect::<Vec<_>>();
assert_eq!(c_stack_lines.len(), 1); // Just the "error" call
}
}

View file

@ -1,11 +1,9 @@
use std::{ use std::{collections::HashSet, sync::Arc};
collections::HashSet,
sync::{Arc, Mutex},
};
use console::{colors_enabled as get_colors_enabled, set_colors_enabled}; use console::{colors_enabled as get_colors_enabled, set_colors_enabled};
use mlua::prelude::*; use mlua::prelude::*;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use parking_lot::ReentrantMutex;
mod basic; mod basic;
mod config; mod config;
@ -20,7 +18,7 @@ pub use self::config::ValueFormatConfig;
// NOTE: Since the setting for colors being enabled is global, // NOTE: Since the setting for colors being enabled is global,
// and these functions may be called in parallel, we use this global // and these functions may be called in parallel, we use this global
// lock to make sure that we don't mess up the colors for other threads. // lock to make sure that we don't mess up the colors for other threads.
static COLORS_LOCK: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(()))); static COLORS_LOCK: Lazy<Arc<ReentrantMutex<()>>> = Lazy::new(|| Arc::new(ReentrantMutex::new(())));
/** /**
Formats a Lua value into a pretty string using the given config. Formats a Lua value into a pretty string using the given config.
@ -28,7 +26,7 @@ static COLORS_LOCK: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(()))
#[must_use] #[must_use]
#[allow(clippy::missing_panics_doc)] #[allow(clippy::missing_panics_doc)]
pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String { pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String {
let _guard = COLORS_LOCK.lock().unwrap(); let _guard = COLORS_LOCK.lock();
let were_colors_enabled = get_colors_enabled(); let were_colors_enabled = get_colors_enabled();
set_colors_enabled(were_colors_enabled && config.colors_enabled); set_colors_enabled(were_colors_enabled && config.colors_enabled);
@ -48,7 +46,7 @@ pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> Stri
#[must_use] #[must_use]
#[allow(clippy::missing_panics_doc)] #[allow(clippy::missing_panics_doc)]
pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String { pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String {
let _guard = COLORS_LOCK.lock().unwrap(); let _guard = COLORS_LOCK.lock();
let were_colors_enabled = get_colors_enabled(); let were_colors_enabled = get_colors_enabled();
set_colors_enabled(were_colors_enabled && config.colors_enabled); set_colors_enabled(were_colors_enabled && config.colors_enabled);

View file

@ -4,6 +4,7 @@ use std::fmt::{self, Write as _};
use mlua::prelude::*; use mlua::prelude::*;
use super::metamethods::{call_table_tostring_metamethod, get_table_type_metavalue};
use super::{ use super::{
basic::{format_value_styled, lua_value_as_plain_string_key}, basic::{format_value_styled, lua_value_as_plain_string_key},
config::ValueFormatConfig, config::ValueFormatConfig,
@ -46,7 +47,12 @@ pub(crate) fn format_value_recursive(
let mut buffer = String::new(); let mut buffer = String::new();
if let LuaValue::Table(ref t) = value { if let LuaValue::Table(ref t) = value {
if depth >= config.max_depth { if let Some(formatted) = format_typename_and_tostringed(
get_table_type_metavalue(t),
call_table_tostring_metamethod(t),
) {
write!(buffer, "{formatted}")?;
} else if depth >= config.max_depth {
write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?; write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?;
} else if !visited.insert(LuaValueId::from(t)) { } else if !visited.insert(LuaValueId::from(t)) {
write!(buffer, "{}", STYLE_DIM.apply_to("{ recursive }"))?; write!(buffer, "{}", STYLE_DIM.apply_to("{ recursive }"))?;
@ -164,3 +170,15 @@ fn format_table(
}) })
.collect() .collect()
} }
fn format_typename_and_tostringed(
typename: Option<String>,
tostringed: Option<String>,
) -> Option<String> {
match (typename, tostringed) {
(Some(typename), Some(tostringed)) => Some(format!("<{typename}({tostringed})>")),
(Some(typename), None) => Some(format!("<{typename}>")),
(None, Some(tostringed)) => Some(tostringed),
(None, None) => None,
}
}

View file

@ -0,0 +1,30 @@
#[derive(Debug, Clone, Copy, Default)]
pub struct JitStatus(bool);
impl JitStatus {
#[must_use]
pub fn new(enabled: bool) -> Self {
Self(enabled)
}
pub fn set_status(&mut self, enabled: bool) {
self.0 = enabled;
}
#[must_use]
pub fn enabled(self) -> bool {
self.0
}
}
impl From<JitStatus> for bool {
fn from(val: JitStatus) -> Self {
val.enabled()
}
}
impl From<bool> for JitStatus {
fn from(val: bool) -> Self {
Self::new(val)
}
}

View file

@ -4,6 +4,7 @@ mod table_builder;
mod version_string; mod version_string;
pub mod fmt; pub mod fmt;
pub mod jit;
pub mod path; pub mod path;
pub use self::table_builder::TableBuilder; pub use self::table_builder::TableBuilder;

View file

@ -2,6 +2,7 @@ use std::sync::Arc;
use mlua::prelude::*; use mlua::prelude::*;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use semver::Version;
static LUAU_VERSION: Lazy<Arc<String>> = Lazy::new(create_luau_version_string); static LUAU_VERSION: Lazy<Arc<String>> = Lazy::new(create_luau_version_string);
@ -20,12 +21,10 @@ pub fn get_version_string(lune_version: impl AsRef<str>) -> String {
let lune_version = lune_version.as_ref(); let lune_version = lune_version.as_ref();
assert!(!lune_version.is_empty(), "Lune version string is empty"); assert!(!lune_version.is_empty(), "Lune version string is empty");
assert!( match Version::parse(lune_version) {
lune_version.chars().all(is_valid_version_char), Ok(semver) => format!("Lune {semver}+{}", *LUAU_VERSION),
"Lune version string contains invalid characters" Err(e) => panic!("Lune version string is not valid semver: {e}"),
); }
format!("Lune {lune_version}+{}", *LUAU_VERSION)
} }
fn create_luau_version_string() -> Arc<String> { fn create_luau_version_string() -> Arc<String> {

View file

@ -1,6 +1,6 @@
[package] [package]
name = "lune" name = "lune"
version = "0.8.6" version = "0.8.9"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/lune-org/lune" repository = "https://github.com/lune-org/lune"
@ -50,7 +50,7 @@ cli = ["dep:clap", "dep:include_dir", "dep:rustyline", "dep:zip_next"]
workspace = true workspace = true
[dependencies] [dependencies]
mlua = { version = "0.9.7", features = ["luau"] } mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" } mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
anyhow = "1.0" anyhow = "1.0"
@ -71,9 +71,9 @@ reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls", "rustls-tls",
] } ] }
lune-std = { optional = true, version = "0.1.3", path = "../lune-std" } lune-std = { optional = true, version = "0.1.5", path = "../lune-std" }
lune-roblox = { optional = true, version = "0.1.2", path = "../lune-roblox" } lune-roblox = { optional = true, version = "0.1.4", path = "../lune-roblox" }
lune-utils = { version = "0.1.2", path = "../lune-utils" } lune-utils = { version = "0.1.3", path = "../lune-utils" }
### CLI ### CLI

View file

@ -1,4 +1,4 @@
use std::process::ExitCode; use std::{env, process::ExitCode};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::Parser; use clap::Parser;
@ -40,17 +40,27 @@ impl RunCommand {
(file_display_name, file_contents) (file_display_name, file_contents)
}; };
// Create a new lune object with all globals & run the script // Create a new lune runtime with all globals & run the script
let result = Runtime::new() let mut rt = Runtime::new()
.with_args(self.script_args) .with_args(self.script_args)
// Enable JIT compilation unless it was requested to be disabled
.with_jit(
!matches!(
env::var("LUNE_LUAU_JIT").ok(),
Some(jit_enabled) if jit_enabled == "0" || jit_enabled == "false" || jit_enabled == "off"
)
);
let result = rt
.run(&script_display_name, strip_shebang(script_contents)) .run(&script_display_name, strip_shebang(script_contents))
.await; .await;
Ok(match result { Ok(match result {
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");
ExitCode::FAILURE ExitCode::FAILURE
} }
Ok(code) => code, Ok((code, _)) => ExitCode::from(code),
}) })
} }
} }

View file

@ -64,8 +64,8 @@ pub fn discover_script_path(path: impl AsRef<str>, in_home_dir: bool) -> Result<
// NOTE: We use metadata directly here to try to // NOTE: We use metadata directly here to try to
// avoid accessing the file path more than once // avoid accessing the file path more than once
let file_meta = file_path.metadata(); let file_meta = file_path.metadata();
let is_file = file_meta.as_ref().map_or(false, Metadata::is_file); let is_file = file_meta.as_ref().is_ok_and(Metadata::is_file);
let is_dir = file_meta.as_ref().map_or(false, Metadata::is_dir); let is_dir = file_meta.as_ref().is_ok_and(Metadata::is_dir);
let is_abs = file_path.is_absolute(); let is_abs = file_path.is_absolute();
let ext = file_path.extension(); let ext = file_path.extension();
if is_file { if is_file {

View file

@ -1,7 +1,6 @@
#![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_panics_doc)]
use std::{ use std::{
process::ExitCode,
rc::Rc, rc::Rc,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
@ -9,6 +8,7 @@ use std::{
}, },
}; };
use lune_utils::jit::JitStatus;
use mlua::prelude::*; use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler}; use mlua_luau_scheduler::{Functions, Scheduler};
use self_cell::self_cell; use self_cell::self_cell;
@ -101,6 +101,7 @@ impl RuntimeInner {
*/ */
pub struct Runtime { pub struct Runtime {
inner: RuntimeInner, inner: RuntimeInner,
jit_status: JitStatus,
} }
impl Runtime { impl Runtime {
@ -114,6 +115,7 @@ impl Runtime {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
inner: RuntimeInner::create().expect("Failed to create runtime"), inner: RuntimeInner::create().expect("Failed to create runtime"),
jit_status: JitStatus::default(),
} }
} }
@ -131,6 +133,15 @@ impl Runtime {
self self
} }
/**
Enables or disables JIT compilation.
*/
#[must_use]
pub fn with_jit(mut self, jit_status: impl Into<JitStatus>) -> Self {
self.jit_status = jit_status.into();
self
}
/** /**
Runs a Lune script inside of the current runtime. Runs a Lune script inside of the current runtime.
@ -144,7 +155,7 @@ impl Runtime {
&mut self, &mut self,
script_name: impl AsRef<str>, script_name: impl AsRef<str>,
script_contents: impl AsRef<[u8]>, script_contents: impl AsRef<[u8]>,
) -> RuntimeResult<ExitCode> { ) -> RuntimeResult<(u8, Vec<LuaValue>)> {
let lua = self.inner.lua(); let lua = self.inner.lua();
let sched = self.inner.scheduler(); let sched = self.inner.scheduler();
@ -156,24 +167,29 @@ impl Runtime {
eprintln!("{}", RuntimeError::from(e)); eprintln!("{}", RuntimeError::from(e));
}); });
// Enable / disable the JIT as requested and store the current status as AppData
lua.set_app_data(self.jit_status);
lua.enable_jit(self.jit_status.enabled());
// Load our "main" thread // Load our "main" thread
let main = lua let main = lua
.load(script_contents.as_ref()) .load(script_contents.as_ref())
.set_name(script_name.as_ref()); .set_name(script_name.as_ref());
// Run it on our scheduler until it and any other spawned threads complete // Run it on our scheduler until it and any other spawned threads complete
sched.push_thread_back(main, ())?; let main_thread_id = sched.push_thread_back(main, ())?;
sched.run().await; sched.run().await;
// Return the exit code - default to FAILURE if we got any errors let main_thread_res = match sched.get_thread_result(main_thread_id) {
let exit_code = sched.get_exit_code().unwrap_or({ Some(res) => res,
if got_any_error.load(Ordering::SeqCst) { None => LuaValue::Nil.into_lua_multi(lua),
ExitCode::FAILURE }?;
} else {
ExitCode::SUCCESS
}
});
Ok(exit_code) Ok((
sched
.get_exit_code()
.unwrap_or(u8::from(got_any_error.load(Ordering::SeqCst))),
main_thread_res.into_vec(),
))
} }
} }

View file

@ -29,16 +29,15 @@ pub async fn run(patched_bin: impl AsRef<[u8]>) -> Result<ExitCode> {
let args = env::args().skip(1).collect::<Vec<_>>(); let args = env::args().skip(1).collect::<Vec<_>>();
let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary"); let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary");
let result = Runtime::new() let mut rt = Runtime::new().with_args(args);
.with_args(args)
.run("STANDALONE", meta.bytecode) let result = rt.run("STANDALONE", meta.bytecode).await;
.await;
Ok(match result { Ok(match result {
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");
ExitCode::FAILURE ExitCode::FAILURE
} }
Ok(code) => code, Ok((code, _)) => ExitCode::from(code),
}) })
} }

View file

@ -31,7 +31,9 @@ macro_rules! create_tests {
// The rest of the test logic can continue as normal // The rest of the test logic can continue as normal
let full_name = format!("{}/tests/{}.luau", workspace_dir.display(), $value); let full_name = format!("{}/tests/{}.luau", workspace_dir.display(), $value);
let script = read_to_string(&full_name).await?; let script = read_to_string(&full_name).await?;
let mut lune = Runtime::new().with_args( let mut lune = Runtime::new()
.with_jit(true)
.with_args(
ARGS ARGS
.clone() .clone()
.iter() .iter()
@ -42,8 +44,8 @@ macro_rules! create_tests {
.trim_end_matches(".luau") .trim_end_matches(".luau")
.trim_end_matches(".lua") .trim_end_matches(".lua")
.to_string(); .to_string();
let exit_code = lune.run(&script_name, &script).await?; let (exit_code, _) = lune.run(&script_name, &script).await?;
Ok(exit_code) Ok(ExitCode::from(exit_code))
} }
)* } )* }
} }
@ -138,13 +140,16 @@ create_tests! {
process_cwd: "process/cwd", process_cwd: "process/cwd",
process_env: "process/env", process_env: "process/env",
process_exit: "process/exit", process_exit: "process/exit",
process_spawn_async: "process/spawn/async", process_exec_async: "process/exec/async",
process_spawn_basic: "process/spawn/basic", process_exec_basic: "process/exec/basic",
process_spawn_cwd: "process/spawn/cwd", process_exec_cwd: "process/exec/cwd",
process_spawn_no_panic: "process/spawn/no_panic", process_exec_no_panic: "process/exec/no_panic",
process_spawn_shell: "process/spawn/shell", process_exec_shell: "process/exec/shell",
process_spawn_stdin: "process/spawn/stdin", process_exec_stdin: "process/exec/stdin",
process_spawn_stdio: "process/spawn/stdio", process_exec_stdio: "process/exec/stdio",
process_spawn_non_blocking: "process/create/non_blocking",
process_spawn_status: "process/create/status",
process_spawn_stream: "process/create/stream",
} }
#[cfg(feature = "std-regex")] #[cfg(feature = "std-regex")]
@ -162,6 +167,7 @@ create_tests! {
roblox_datatype_color3: "roblox/datatypes/Color3", roblox_datatype_color3: "roblox/datatypes/Color3",
roblox_datatype_color_sequence: "roblox/datatypes/ColorSequence", roblox_datatype_color_sequence: "roblox/datatypes/ColorSequence",
roblox_datatype_color_sequence_keypoint: "roblox/datatypes/ColorSequenceKeypoint", roblox_datatype_color_sequence_keypoint: "roblox/datatypes/ColorSequenceKeypoint",
roblox_datatype_content: "roblox/datatypes/Content",
roblox_datatype_enum: "roblox/datatypes/Enum", roblox_datatype_enum: "roblox/datatypes/Enum",
roblox_datatype_faces: "roblox/datatypes/Faces", roblox_datatype_faces: "roblox/datatypes/Faces",
roblox_datatype_font: "roblox/datatypes/Font", roblox_datatype_font: "roblox/datatypes/Font",

View file

@ -25,7 +25,7 @@ futures-lite = "2.2"
rustc-hash = "1.1" rustc-hash = "1.1"
tracing = "0.1" tracing = "0.1"
mlua = { version = "0.9.6", features = [ mlua = { version = "0.9.9", features = [
"luau", "luau",
"luau-jit", "luau-jit",
"async", "async",

View file

@ -32,7 +32,7 @@ pub fn main() -> LuaResult<()> {
// Verify that we got a correct exit code // Verify that we got a correct exit code
let code = sched.get_exit_code().unwrap_or_default(); let code = sched.get_exit_code().unwrap_or_default();
assert!(format!("{code:?}").contains("(1)")); assert_eq!(code, 1);
Ok(()) Ok(())
} }

View file

@ -1,10 +1,10 @@
use std::{cell::Cell, process::ExitCode, rc::Rc}; use std::{cell::Cell, rc::Rc};
use event_listener::Event; use event_listener::Event;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct Exit { pub(crate) struct Exit {
code: Rc<Cell<Option<ExitCode>>>, code: Rc<Cell<Option<u8>>>,
event: Rc<Event>, event: Rc<Event>,
} }
@ -16,12 +16,12 @@ impl Exit {
} }
} }
pub fn set(&self, code: ExitCode) { pub fn set(&self, code: u8) {
self.code.set(Some(code)); self.code.set(Some(code));
self.event.notify(usize::MAX); self.event.notify(usize::MAX);
} }
pub fn get(&self) -> Option<ExitCode> { pub fn get(&self) -> Option<u8> {
self.code.get() self.code.get()
} }

View file

@ -1,15 +1,11 @@
#![allow(unused_imports)]
#![allow(clippy::too_many_lines)] #![allow(clippy::too_many_lines)]
use std::process::ExitCode;
use mlua::prelude::*; use mlua::prelude::*;
use crate::{ use crate::{
error_callback::ThreadErrorCallback, error_callback::ThreadErrorCallback,
queue::{DeferredThreadQueue, SpawnedThreadQueue}, queue::{DeferredThreadQueue, SpawnedThreadQueue},
result_map::ThreadResultMap, result_map::ThreadResultMap,
scheduler::Scheduler,
thread_id::ThreadId, thread_id::ThreadId,
traits::LuaSchedulerExt, traits::LuaSchedulerExt,
util::{is_poll_pending, LuaThreadOrFunction, ThreadResult}, util::{is_poll_pending, LuaThreadOrFunction, ThreadResult},
@ -232,7 +228,7 @@ impl<'lua> Functions<'lua> {
"exit", "exit",
lua.create_function(|lua, code: Option<u8>| { lua.create_function(|lua, code: Option<u8>| {
let _span = tracing::trace_span!("Scheduler::fn_exit").entered(); let _span = tracing::trace_span!("Scheduler::fn_exit").entered();
let code = code.map(ExitCode::from).unwrap_or_default(); let code = code.unwrap_or_default();
lua.set_exit_code(code); lua.set_exit_code(code);
Ok(()) Ok(())
})?, })?,

View file

@ -2,7 +2,6 @@
use std::{ use std::{
cell::Cell, cell::Cell,
process::ExitCode,
rc::{Rc, Weak as WeakRc}, rc::{Rc, Weak as WeakRc},
sync::{Arc, Weak as WeakArc}, sync::{Arc, Weak as WeakArc},
thread::panicking, thread::panicking,
@ -168,7 +167,7 @@ impl<'lua> Scheduler<'lua> {
Gets the exit code for this scheduler, if one has been set. Gets the exit code for this scheduler, if one has been set.
*/ */
#[must_use] #[must_use]
pub fn get_exit_code(&self) -> Option<ExitCode> { pub fn get_exit_code(&self) -> Option<u8> {
self.exit.get() self.exit.get()
} }
@ -177,7 +176,7 @@ impl<'lua> Scheduler<'lua> {
This will cause [`Scheduler::run`] to exit immediately. This will cause [`Scheduler::run`] to exit immediately.
*/ */
pub fn set_exit_code(&self, code: ExitCode) { pub fn set_exit_code(&self, code: u8) {
self.exit.set(code); self.exit.set(code);
} }

View file

@ -82,7 +82,7 @@ pub trait LuaSchedulerExt<'lua> {
Panics if called outside of a running [`Scheduler`]. Panics if called outside of a running [`Scheduler`].
*/ */
fn set_exit_code(&self, code: ExitCode); fn set_exit_code(&self, code: u8);
/** /**
Pushes (spawns) a lua thread to the **front** of the current scheduler. Pushes (spawns) a lua thread to the **front** of the current scheduler.
@ -283,7 +283,7 @@ pub trait LuaSpawnExt<'lua> {
} }
impl<'lua> LuaSchedulerExt<'lua> for Lua { impl<'lua> LuaSchedulerExt<'lua> for Lua {
fn set_exit_code(&self, code: ExitCode) { fn set_exit_code(&self, code: u8) {
let exit = self let exit = self
.app_data_ref::<Exit>() .app_data_ref::<Exit>()
.expect("exit code can only be set from within an active scheduler"); .expect("exit code can only be set from within an active scheduler");
@ -334,7 +334,7 @@ impl<'lua> LuaSchedulerExt<'lua> for Lua {
} }
} }
impl<'lua> LuaSpawnExt<'lua> for Lua { impl LuaSpawnExt<'_> for Lua {
fn spawn<F, T>(&self, fut: F) -> Task<T> fn spawn<F, T>(&self, fut: F) -> Task<T>
where where
F: Future<Output = T> + Send + 'static, F: Future<Output = T> + Send + 'static,

4
rokit.toml Normal file
View file

@ -0,0 +1,4 @@
[tools]
luau-lsp = "JohnnyMorganz/luau-lsp@1.33.1"
stylua = "JohnnyMorganz/StyLua@0.20.0"
just = "casey/just@1.36.0"

View file

@ -108,7 +108,7 @@ local BIN_ZLIB = if process.os == "macos" then "/opt/homebrew/bin/pigz" else "pi
local function checkInstalled(program: string, args: { string }?) local function checkInstalled(program: string, args: { string }?)
print("Checking if", program, "is installed") print("Checking if", program, "is installed")
local result = process.spawn(program, args) local result = process.exec(program, args)
if not result.ok then if not result.ok then
stdio.ewrite(string.format("Program '%s' is not installed\n", program)) stdio.ewrite(string.format("Program '%s' is not installed\n", program))
process.exit(1) process.exit(1)
@ -123,7 +123,7 @@ checkInstalled(BIN_ZLIB, { "--version" })
-- Run them to generate files -- Run them to generate files
local function run(program: string, args: { string }): string local function run(program: string, args: { string }): string
local result = process.spawn(program, args) local result = process.exec(program, args)
if not result.ok then if not result.ok then
stdio.ewrite(string.format("Command '%s' failed\n", program)) stdio.ewrite(string.format("Command '%s' failed\n", program))
if #result.stdout > 0 then if #result.stdout > 0 then

View file

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

View file

@ -31,7 +31,7 @@ if not runLocaleTests then
return return
end end
local dateCmd = process.spawn("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, { local dateCmd = process.exec("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
env = { env = {
LC_ALL = "fr_FR.UTF-8 ", LC_ALL = "fr_FR.UTF-8 ",
}, },

View file

@ -24,10 +24,10 @@ local handle = net.serve(PORT, {
return "unreachable" return "unreachable"
end, end,
handleWebSocket = function(socket) handleWebSocket = function(socket)
local socketMessage = socket.next() local socketMessage = socket:next()
assert(socketMessage == REQUEST, "Invalid web socket request from client") assert(socketMessage == REQUEST, "Invalid web socket request from client")
socket.send(RESPONSE) socket:send(RESPONSE)
socket.close() socket:close()
end, end,
}) })
@ -43,19 +43,19 @@ end)
local socket = net.socket(WS_URL) local socket = net.socket(WS_URL)
socket.send(REQUEST) socket:send(REQUEST)
local socketMessage = socket.next() local socketMessage = socket:next()
assert(socketMessage ~= nil, "Got no web socket response from server") assert(socketMessage ~= nil, "Got no web socket response from server")
assert(socketMessage == RESPONSE, "Invalid web socket response from server") assert(socketMessage == RESPONSE, "Invalid web socket response from server")
socket.close() socket:close()
task.cancel(thread2) task.cancel(thread2)
-- Wait for the socket to close and make sure we can't send messages afterwards -- Wait for the socket to close and make sure we can't send messages afterwards
task.wait() task.wait()
local success3, err2 = (pcall :: any)(socket.send, "") local success3, err2 = (pcall :: any)(socket.send, socket, "")
assert(not success3, "Sending messages after the socket has been closed should error") assert(not success3, "Sending messages after the socket has been closed should error")
local message2 = tostring(err2) local message2 = tostring(err2)
assert( assert(

View file

@ -8,17 +8,17 @@ assert(type(socket.send) == "function", "send must be a function")
assert(type(socket.close) == "function", "close must be a function") assert(type(socket.close) == "function", "close must be a function")
-- Request to close the socket -- Request to close the socket
socket.close() socket:close()
-- Drain remaining messages, until we got our close message -- Drain remaining messages, until we got our close message
while socket.next() do while socket:next() do
end end
assert(type(socket.closeCode) == "number", "closeCode should exist after closing") assert(type(socket.closeCode) == "number", "closeCode should exist after closing")
assert(socket.closeCode == 1000, "closeCode should be 1000 after closing") assert(socket.closeCode == 1000, "closeCode should be 1000 after closing")
local success, message = pcall(function() local success, message = pcall(function()
socket.send("Hello, world!") socket:send("Hello, world!")
end) end)
assert(not success, "send should fail after closing") assert(not success, "send should fail after closing")

View file

@ -8,7 +8,7 @@ local task = require("@lune/task")
local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json") local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json")
while not socket.closeCode do while not socket.closeCode do
local response = socket.next() local response = socket:next()
if response then if response then
local decodeSuccess, decodeMessage = pcall(serde.decode, "json" :: "json", response) local decodeSuccess, decodeMessage = pcall(serde.decode, "json" :: "json", response)
@ -23,6 +23,6 @@ while not socket.closeCode do
-- Close the connection after a second with the success close code -- Close the connection after a second with the success close code
task.wait(1) task.wait(1)
socket.close(1000) socket:close(1000)
end end
end end

View file

@ -10,7 +10,7 @@ local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json")
local spawnedThread = task.spawn(function() local spawnedThread = task.spawn(function()
while not socket.closeCode do while not socket.closeCode do
socket.next() socket:next()
end end
end) end)
@ -23,9 +23,9 @@ end)
task.wait(1) task.wait(1)
local payload = '{"op":1,"d":null}' local payload = '{"op":1,"d":null}'
socket.send(payload) socket:send(payload)
socket.send(buffer.fromstring(payload)) socket:send(buffer.fromstring(payload))
socket.close(1000) socket:close(1000)
task.cancel(delayedThread) task.cancel(delayedThread)
task.cancel(spawnedThread) task.cancel(spawnedThread)

View file

@ -0,0 +1,21 @@
local process = require("@lune/process")
-- Killing a child process should work as expected
local message = "Hello, world!"
local child = process.create("cat")
child.stdin:write(message)
child.kill()
assert(child.status().code == 9, "Child process should have an exit code of 9 (SIGKILL)")
assert(
child.stdout:readToEnd() == message,
"Reading from stdout of child process should work even after kill"
)
local stdinWriteOk = pcall(function()
child.stdin:write(message)
end)
assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill")

View file

@ -0,0 +1,13 @@
local process = require("@lune/process")
-- Spawning a child process should not block the thread
local childThread = coroutine.create(process.create)
local ok, err = coroutine.resume(childThread, "echo", { "hello, world" })
assert(ok, err)
assert(
coroutine.status(childThread) == "dead",
"Child process should not block the thread it is running on"
)

View file

@ -0,0 +1,15 @@
local process = require("@lune/process")
-- The exit code of an child process should be correct
local randomExitCode = math.random(0, 255)
local isOk = randomExitCode == 0
local child = process.create("exit", { tostring(randomExitCode) }, { shell = true })
local status = child.status()
assert(
status.code == randomExitCode,
`Child process exited with wrong exit code, expected {randomExitCode}`
)
assert(status.ok == isOk, `Child status should be {if status.ok then "ok" else "not ok"}`)

View file

@ -0,0 +1,18 @@
local process = require("@lune/process")
-- Should be able to write and read from child process streams
local msg = "hello, world"
local catChild = process.create("cat")
catChild.stdin:write(msg)
assert(
msg == catChild.stdout:read(#msg),
"Failed to write to stdin or read from stdout of child process"
)
local echoChild = if process.os == "windows"
then process.create("/c", { "echo", msg, "1>&2" }, { shell = "cmd" })
else process.create("echo", { msg, ">>/dev/stderr" }, { shell = true })
assert(msg == echoChild.stderr:read(#msg), "Failed to read from stderr of child process")

View file

@ -4,7 +4,7 @@ local task = require("@lune/task")
local IS_WINDOWS = process.os == "windows" local IS_WINDOWS = process.os == "windows"
-- Spawning a process should not block any lua thread(s) -- Executing a command should not block any lua thread(s)
local SLEEP_DURATION = 1 / 4 local SLEEP_DURATION = 1 / 4
local SLEEP_SAMPLES = 2 local SLEEP_SAMPLES = 2
@ -31,7 +31,7 @@ for i = 1, SLEEP_SAMPLES, 1 do
table.insert(args, 1, "-Milliseconds") table.insert(args, 1, "-Milliseconds")
end end
-- Windows does not have `sleep` as a process, so we use powershell instead. -- Windows does not have `sleep` as a process, so we use powershell instead.
process.spawn("sleep", args, if IS_WINDOWS then { shell = true } else nil) process.exec("sleep", args, if IS_WINDOWS then { shell = true } else nil)
sleepCounter += 1 sleepCounter += 1
end) end)
end end

View file

@ -2,7 +2,7 @@ local process = require("@lune/process")
local stdio = require("@lune/stdio") local stdio = require("@lune/stdio")
local task = require("@lune/task") local task = require("@lune/task")
-- Spawning a child process should work, with options -- Executing a command should work, with options
local thread = task.delay(1, function() local thread = task.delay(1, function()
stdio.ewrite("Spawning a process should take a reasonable amount of time\n") stdio.ewrite("Spawning a process should take a reasonable amount of time\n")
@ -12,7 +12,7 @@ end)
local IS_WINDOWS = process.os == "windows" local IS_WINDOWS = process.os == "windows"
local result = process.spawn( local result = process.exec(
if IS_WINDOWS then "cmd" else "ls", if IS_WINDOWS then "cmd" else "ls",
if IS_WINDOWS then { "/c", "dir" } else { "-a" } if IS_WINDOWS then { "/c", "dir" } else { "-a" }
) )

View file

@ -6,7 +6,7 @@ local pwdCommand = if IS_WINDOWS then "cmd" else "pwd"
local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {} local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {}
-- Make sure the cwd option actually uses the directory we want -- Make sure the cwd option actually uses the directory we want
local rootPwd = process.spawn(pwdCommand, pwdArgs, { local rootPwd = process.exec(pwdCommand, pwdArgs, {
cwd = "/", cwd = "/",
}).stdout }).stdout
rootPwd = string.gsub(rootPwd, "^%s+", "") rootPwd = string.gsub(rootPwd, "^%s+", "")
@ -27,24 +27,24 @@ end
-- Setting cwd should not change the cwd of this process -- Setting cwd should not change the cwd of this process
local pwdBefore = process.spawn(pwdCommand, pwdArgs).stdout local pwdBefore = process.exec(pwdCommand, pwdArgs).stdout
process.spawn("ls", {}, { process.exec("ls", {}, {
cwd = "/", cwd = "/",
shell = true, shell = true,
}) })
local pwdAfter = process.spawn(pwdCommand, pwdArgs).stdout local pwdAfter = process.exec(pwdCommand, pwdArgs).stdout
assert(pwdBefore == pwdAfter, "Current working directory changed after running child process") assert(pwdBefore == pwdAfter, "Current working directory changed after running child process")
-- Setting the cwd on a child process should properly -- Setting the cwd on a child process should properly
-- replace any leading ~ with the users real home dir -- replace any leading ~ with the users real home dir
local homeDir1 = process.spawn("echo $HOME", nil, { local homeDir1 = process.exec("echo $HOME", nil, {
shell = true, shell = true,
}).stdout }).stdout
-- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return -- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return
-- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object -- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object
local homeDir2 = process.spawn(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, { local homeDir2 = process.exec(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, {
shell = true, shell = true,
cwd = "~", cwd = "~",
}).stdout }).stdout

View file

@ -0,0 +1,7 @@
local process = require("@lune/process")
-- Executing a non existent command as a child process
-- should not panic, but should error
local success = pcall(process.exec, "someProgramThatDoesNotExist")
assert(not success, "Spawned a non-existent program")

View file

@ -5,7 +5,7 @@ local IS_WINDOWS = process.os == "windows"
-- Default shell should be /bin/sh on unix and powershell on Windows, -- Default shell should be /bin/sh on unix and powershell on Windows,
-- note that powershell needs slightly different command flags for ls -- note that powershell needs slightly different command flags for ls
local shellResult = process.spawn("ls", { local shellResult = process.exec("ls", {
if IS_WINDOWS then "-Force" else "-a", if IS_WINDOWS then "-Force" else "-a",
}, { }, {
shell = true, shell = true,

View file

@ -10,8 +10,8 @@ local echoMessage = "Hello from child process!"
-- When passing stdin to powershell on windows we must "accept" using the double newline -- When passing stdin to powershell on windows we must "accept" using the double newline
local result = if IS_WINDOWS local result = if IS_WINDOWS
then process.spawn("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" }) then process.exec("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
else process.spawn("xargs", { "echo" }, { stdin = echoMessage }) else process.exec("xargs", { "echo" }, { stdin = echoMessage })
local resultStdout = if IS_WINDOWS local resultStdout = if IS_WINDOWS
then string.sub(result.stdout, #result.stdout - #echoMessage - 1) then string.sub(result.stdout, #result.stdout - #echoMessage - 1)

View file

@ -5,12 +5,12 @@ local IS_WINDOWS = process.os == "windows"
-- Inheriting stdio & environment variables should work -- Inheriting stdio & environment variables should work
local echoMessage = "Hello from child process!" local echoMessage = "Hello from child process!"
local echoResult = process.spawn("echo", { local echoResult = process.exec("echo", {
if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"', if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"',
}, { }, {
env = { TEST_VAR = echoMessage }, env = { TEST_VAR = echoMessage },
shell = if IS_WINDOWS then "powershell" else "bash", shell = if IS_WINDOWS then "powershell" else "bash",
stdio = "inherit", stdio = "inherit" :: process.SpawnOptionsStdioKind, -- FIXME: This should just work without a cast?
}) })
-- Windows uses \r\n (CRLF) and unix uses \n (LF) -- Windows uses \r\n (CRLF) and unix uses \n (LF)

View file

@ -1,7 +0,0 @@
local process = require("@lune/process")
-- Spawning a child process for a non-existent
-- program should not panic, but should error
local success = pcall(process.spawn, "someProgramThatDoesNotExist")
assert(not success, "Spawned a non-existent program")

View file

@ -0,0 +1,62 @@
local roblox = require("@lune/roblox") :: any
local Content = roblox.Content
local Instance = roblox.Instance
local Enum = roblox.Enum
assert(Content.none, "Content.none did not exist")
assert(
Content.none.SourceType == Enum.ContentSourceType.None,
"Content.none's SourceType was wrong"
)
assert(Content.none.Uri == nil, "Content.none's Uri field was wrong")
assert(Content.none.Object == nil, "Content.none's Object field was wrong")
local uri = Content.fromUri("test uri")
assert(uri.SourceType == Enum.ContentSourceType.Uri, "URI Content's SourceType was wrong")
assert(uri.Uri == "test uri", "URI Content's Uri field was wrong")
assert(uri.Object == nil, "URI Content's Object field was wrong")
assert(not pcall(Content.fromUri), "Content.fromUri accepted no argument")
assert(not pcall(Content.fromUri, false), "Content.fromUri accepted a boolean argument")
assert(not pcall(Content.fromUri, Enum), "Content.fromUri accepted a UserData as an argument")
assert(
not pcall(Content.fromUri, buffer.create(0)),
"Content.fromUri accepted a buffer as an argument"
)
-- It feels weird that this is allowed because `EditableImage` is very much
-- not an Instance. But what can you do?
local target = Instance.new("EditableImage")
local object = Content.fromObject(target)
assert(object.SourceType == Enum.ContentSourceType.Object, "Object Content's SourceType was wrong")
assert(object.Uri == nil, "Object Content's Uri field was wrong")
assert(object.Object == target, "Object Content's Object field was wrong")
assert(not pcall(Content.fromObject), "Content.fromObject accepted no argument")
assert(not pcall(Content.fromObject, false), "Content.fromObject accepted a boolean argument")
assert(
not pcall(Content.fromObject, Enum),
"Content.fromObject accepted a non-Instance/Object UserData as an argument"
)
assert(
not pcall(Content.fromObject, buffer.create(0)),
"Content.fromObject accepted a buffer as an argument"
)
assert(
not pcall(Content.fromObject, Instance.new("Folder")),
"Content.fromObject accepted an Instance as an argument"
)
assert(
tostring(Content.none) == "Content(None)",
`expected tostring(Content.none) to be Content(None), it was actually {Content.none}`
)
assert(
tostring(uri) == "Content(Uri=test uri)",
`expected tostring(URI Content) to be Content(Uri=...), it was actually {uri}`
)
assert(
tostring(object) == "Content(Object)",
`expected tostring(Object Content) to be Content(Object), it was actually {object}`
)

View file

@ -42,4 +42,14 @@ assert(Vector2.new(2, 4) / 2 == Vector2.new(1, 2))
assert(Vector2.new(7, 15) // Vector2.new(3, 7) == Vector2.new(2, 2)) assert(Vector2.new(7, 15) // Vector2.new(3, 7) == Vector2.new(2, 2))
assert(Vector2.new(3, 7) // 2 == Vector2.new(1, 3)) assert(Vector2.new(3, 7) // 2 == Vector2.new(1, 3))
-- TODO: Vector math -- Vector math methods
assert(Vector2.new(-1, -2):Abs() == Vector2.new(1, 2))
assert(Vector2.new(-1.7, 2):Sign() == Vector2.new(-1, 1))
assert(Vector2.new(-1.9, 2.1):Ceil() == Vector2.new(-1, 3))
assert(Vector2.new(-1.1, 2.99):Floor() == Vector2.new(-2, 2))
assert(Vector2.new(1, 2):FuzzyEq(Vector2.new(1 - 1e-6, 2 + 1e-6), 1e-5))
assert(not Vector2.new(1, 2):FuzzyEq(Vector2.new(1.2, 2), 0.1))
local angle = Vector2.new(1, 1):Angle(Vector2.new(-1, 1))
assert(math.abs(angle - (math.pi / 2)) < 1e-5)

View file

@ -45,4 +45,10 @@ assert(Vector3.new(2, 4, 8) / 2 == Vector3.new(1, 2, 4))
assert(Vector3.new(7, 11, 15) // Vector3.new(3, 5, 7) == Vector3.new(2, 2, 2)) assert(Vector3.new(7, 11, 15) // Vector3.new(3, 5, 7) == Vector3.new(2, 2, 2))
assert(Vector3.new(3, 5, 7) // 2 == Vector3.new(1, 2, 3)) assert(Vector3.new(3, 5, 7) // 2 == Vector3.new(1, 2, 3))
-- TODO: Vector math -- Vector math methods
assert(Vector3.new(-1, -2, -3):Abs() == Vector3.new(1, 2, 3))
assert(Vector3.new(-1.7, 2, -3):Sign() == Vector3.new(-1, 1, -1))
assert(Vector3.new(-1.9, 2.1, 3.5):Ceil() == Vector3.new(-1, 3, 4))
assert(Vector3.new(-1.1, 2.99, 3.5):Floor() == Vector3.new(-2, 2, 3))
assert(Vector3.new(1, 2, 3):FuzzyEq(Vector3.new(1 - 1e-6, 2 + 1e-6, 3 + 1e-6), 1e-5))

View file

@ -16,6 +16,7 @@ local UDim2 = roblox.UDim2
local Vector2 = roblox.Vector2 local Vector2 = roblox.Vector2
local Vector3 = roblox.Vector3 local Vector3 = roblox.Vector3
local Instance = roblox.Instance local Instance = roblox.Instance
local Enum = roblox.Enum
local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/attributes/binary.rbxm") local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/attributes/binary.rbxm")
local model = roblox.deserializeModel(modelFile)[1] local model = roblox.deserializeModel(modelFile)[1]
@ -114,3 +115,12 @@ model.Parent = game
local placeFile = roblox.serializePlace(game) local placeFile = roblox.serializePlace(game)
fs.writeDir("bin/roblox") fs.writeDir("bin/roblox")
fs.writeFile("bin/roblox/attributes.rbxl", placeFile) fs.writeFile("bin/roblox/attributes.rbxl", placeFile)
local enum_attr = Instance.new("Folder")
enum_attr:SetAttribute("Foo", Enum.NormalId.Front)
assert(enum_attr:GetAttribute("Foo") == Enum.NormalId.Front)
local enum_attr_ser = roblox.serializeModel({ enum_attr })
local enum_attr_de = roblox.deserializeModel(enum_attr_ser)
assert(enum_attr_de[1]:GetAttribute("Foo") == Enum.NormalId.Front)

View file

@ -20,14 +20,10 @@ assert(not pcall(function()
return child1.Name return child1.Name
end)) end))
assert(not pcall(function() assert(not child1.Parent)
return child1.Parent
end))
assert(not pcall(function() assert(not pcall(function()
return child2.Name return child2.Name
end)) end))
assert(not pcall(function() assert(not child2.Parent)
return child2.Parent
end))

View file

@ -14,22 +14,16 @@ assert(not pcall(function()
return root.Name return root.Name
end)) end))
assert(not pcall(function() assert(not root.Parent)
return root.Parent
end))
assert(not pcall(function() assert(not pcall(function()
return child.Name return child.Name
end)) end))
assert(not pcall(function() assert(not child.Parent)
return child.Parent
end))
assert(not pcall(function() assert(not pcall(function()
return descendant.Name return descendant.Name
end)) end))
assert(not pcall(function() assert(not descendant.Parent)
return descendant.Parent
end))

View file

@ -33,6 +33,16 @@ part.Shape = Enum.PartType.Ball
assert(part.Shape == Enum.PartType.Ball) assert(part.Shape == Enum.PartType.Ball)
-- Enums should roundtrip through serde without problem
local decal = Instance.new("Decal")
decal.Face = Enum.NormalId.Top
local decal_ser = roblox.serializeModel({ decal })
local decal_de = roblox.deserializeModel(decal_ser)
assert(decal_de[1].Face == Enum.NormalId.Top)
-- Properties that don't exist for a class should error -- Properties that don't exist for a class should error
local meshPart = Instance.new("MeshPart") local meshPart = Instance.new("MeshPart")

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