Compare commits

..

25 Commits

Author SHA1 Message Date
Lucien Greathouse
e482aba030 Release v7.2.1 2022-07-08 20:22:16 -04:00
Samuel P
535e4d42bb Change Notification sound to generic sound (#566)
* Change Notification sound to generic sound

The notification sound causes the game to summon an error due to no experience permissions with no way to grant permission. This is due to the new audio policy update.

* Update Notification sound
2022-07-02 19:33:24 -04:00
boatbomber
54398d4c4b Add setting to toggle sound effects (#568)
* Use soundPlayer object with setting

* Style changes
2022-07-02 05:12:58 -04:00
Lucien Greathouse
0987b44e23 Release v7.2.0 2022-06-29 20:34:06 -04:00
Lucien Greathouse
58098e96d4 Update Changelog 2022-06-29 20:15:24 -04:00
Max
f649c180cf Disambiguate camelCase and PascalCase in *.meta.json and *.model.json (#563)
* Disambiguate camelCase and PascalCase.

*.meta.json forces camelCase while *.model.json forces PascalCase. This commit reinforces camelCase as the preference for both, but allows for PascalCase in both as well.

* Made requested changes, breaking due to serde bug.

* Make work with existing Serde stuff

* Work around MSRV

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-06-29 20:14:35 -04:00
Lucien Greathouse
966478b131 Update Changelog 2022-06-29 19:07:19 -04:00
boatbomber
ca0759a011 Add notification popups (#540)
* Add notifications prototype

* Add timeout

* Improve function name

* Faster timeouts and fully clickable

* Update remove padding from old X button

* Only auto-dismiss when viewport is open

* Start auto dismiss once viewed

* Avoid redundantly displaying widget text as notifs

* Add sound effect

* Add setting for notifications

* Remove duplicate PluginSettings.StudioProvider

* Use short pop sound effect

* Fix broken audio, thanks Roblox

* Use e instead of createElement
2022-06-29 19:06:13 -04:00
Lucien Greathouse
f1cdf2fe79 Update CHANGELOG 2022-06-29 19:05:04 -04:00
Watermelon
04fa5e2719 Added address reference to CLI output (#556)
* Added address reference to CLI output

* Stored loopback check address as a variable

* Changed other loopback references to the new variable

* Fixed mistake on address_string variable

* Merge write calls

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-06-29 19:04:28 -04:00
Lucien Greathouse
eccb95690c Live sync Attributes (#553)
* Add test project for tags

* Update rbx_dom_lua and add attributes project

* Add Attributes shorthand; not working

* Update dependencies

* Update rbx_reflection_database

* Update rbx_types and commit attributes snapshot
2022-06-29 18:53:34 -04:00
Samuel P
acf7456371 Accept .luau files (#552)
* accept .luau files

* Accept .luau in snapshot creation

* Update versioning and snapshots.

* fix versioning

* Run rustfmt

* Reduce repetition in extension detection

* Tidy build script change

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-06-29 18:53:10 -04:00
Lucien Greathouse
8ea41480b7 Bump MSRV (#564)
* Bump MSRV

* Fix updated snapshot
2022-06-29 18:41:37 -04:00
Lucien Greathouse
0d1bc0d7fe Update dependencies 2022-06-29 18:00:50 -04:00
JohnnyMorganz
f9b7774286 Change linux release runner to use ubuntu-18.04 (#561)
ubuntu-latest uses Ubuntu 20.04, this causes issues with glibc as older versions of ubuntu/other distros use an older version.

This is fixed by building the release binary on `ubuntu-18.04`, which uses a version of glibc more widely available.

Ref: https://github.com/JohnnyMorganz/StyLua/pull/444
https://github.com/JohnnyMorganz/StyLua/pull/445
2022-06-26 15:22:16 -04:00
Michael Schmatz
2e672badf2 Update rbx_binary to 0.6.5 (#558) 2022-06-22 04:46:34 -04:00
Lucien Greathouse
cd5d6fd15c Add test project for tags 2022-06-12 05:05:17 -04:00
Micah
cf76982cfa Update selene (#550)
* Update selene

* Update foreman.toml

Co-authored-by: Sasial <44125644+sasial-dev@users.noreply.github.com>

Co-authored-by: Sasial <44125644+sasial-dev@users.noreply.github.com>
Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2022-06-12 03:41:49 -04:00
Micah
2624ea7d2a Remove .luacheckrc (#551) 2022-06-12 02:57:40 -04:00
Lucien Greathouse
2e7c4b6dff Update Changelog 2022-06-10 02:15:07 -04:00
Lucien Greathouse
e5dbee1073 Defer application of init.meta.json until after init Lua files. (#549)
Fixes #546.
2022-06-09 21:42:37 -04:00
Lucien Greathouse
c06463b61d Update CHANGELOG 2022-06-05 17:48:17 -04:00
Lucien Greathouse
10341e3776 Major Performance Improvements (#548)
* Use WeakDom::into_raw for faster snapshot generation from models

* Make compute_patch_set take snapshots by value

* Stop deferring property application in apply_patch_set

* Use InstanceBuilder::empty to avoid extra name allocations

* Git dependencies, skip dropping ServeSession

* Use std::mem::forget instead of ManuallyDrop

* Switch to latest rbx-dom crates.io dependencies

* Update other dependencies
2022-06-05 17:47:31 -04:00
Lucien Greathouse
824cdc5dcd Annotate snapshot_rbxm for profiling 2022-05-27 18:13:07 -04:00
Lucien Greathouse
7aa7a35aa5 Add profiling info and optional profiling with Tracy 2022-05-27 03:08:54 -04:00
68 changed files with 4849 additions and 1948 deletions

View File

@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
rust_version: [stable, 1.55.0]
rust_version: [stable, 1.57.0]
steps:
- uses: actions/checkout@v3

View File

@@ -67,7 +67,7 @@ jobs:
# -x86_64 to each release.
include:
- host: linux
os: ubuntu-latest
os: ubuntu-18.04
target: x86_64-unknown-linux-gnu
label: linux
@@ -150,4 +150,4 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
path: release.zip
path: release.zip

3
.gitignore vendored
View File

@@ -19,6 +19,3 @@
# Snapshot files from the 'insta' Rust crate
**/*.snap.new
# Selene generates a roblox.toml file that should not be checked in.
/roblox.toml

View File

@@ -1,58 +0,0 @@
stds.roblox = {
read_globals = {
game = {
other_fields = true,
},
-- Roblox globals
"script",
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof",
-- Types
"Vector2", "Vector3",
"Vector2int16", "Vector3int16",
"Color3",
"UDim", "UDim2",
"Rect",
"CFrame",
"Enum",
"Instance",
"DockWidgetPluginGuiInfo",
}
}
stds.plugin = {
read_globals = {
"plugin",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP", "itFIXME",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
"421", -- shadowing local variable
"422", -- shadowing argument
"431", -- shadowing upvalue
"432", -- shadowing upvalue argument
}
std = "lua51+roblox"
files["**/*.server.lua"] = {
std = "+plugin",
}
files["**/*.spec.lua"] = {
std = "+testez",
}

View File

@@ -1,7 +1,37 @@
# Rojo Changelog
## Unreleased Changes
## [7.2.1] - July 8, 2022
* Fixed notification sound by changing it to a generic sound. ([#566])
* Added setting to turn off sound effects. ([#568])
[#566]: https://github.com/rojo-rbx/rojo/pull/566
[#568]: https://github.com/rojo-rbx/rojo/pull/568
[7.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.0
## [7.2.0] - June 29, 2022
* Added support for `.luau` files. ([#552])
* Added support for live syncing Attributes and Tags. ([#553])
* Added notification popups in the Roblox Studio plugin. ([#540])
* Fixed `init.meta.json` when used with `init.lua` and related files. ([#549])
* Fixed incorrect output when serving from a non-default address or port ([#556])
* Fixed Linux binaries not running on systems with older glibc. ([#561])
* Added `camelCase` casing for JSON models, deprecating `PascalCase` names. ([#563])
* Switched from structopt to clap for command line argument parsing.
* Significantly improved performance of building and serving. ([#548])
* Increased minimum supported Rust version to 1.57.0. ([#564])
[#540]: https://github.com/rojo-rbx/rojo/pull/540
[#548]: https://github.com/rojo-rbx/rojo/pull/548
[#549]: https://github.com/rojo-rbx/rojo/pull/549
[#552]: https://github.com/rojo-rbx/rojo/pull/552
[#553]: https://github.com/rojo-rbx/rojo/pull/553
[#556]: https://github.com/rojo-rbx/rojo/pull/556
[#561]: https://github.com/rojo-rbx/rojo/pull/561
[#563]: https://github.com/rojo-rbx/rojo/pull/563
[#564]: https://github.com/rojo-rbx/rojo/pull/564
[7.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.0
## [7.1.1] - May 26, 2022
* Fixed sourcemap command not stripping paths correctly ([#544])

555
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
[package]
name = "rojo"
version = "7.1.1"
version = "7.2.1"
rust-version = "1.57.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0"
@@ -8,7 +9,7 @@ homepage = "https://rojo.space"
documentation = "https://rojo.space/docs"
repository = "https://github.com/rojo-rbx/rojo"
readme = "README.md"
edition = "2018"
edition = "2021"
build = "build.rs"
exclude = [
@@ -27,6 +28,8 @@ default = []
# Enable this feature to live-reload assets from the web UI.
dev_live_assets = []
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
[workspace]
members = ["crates/*"]
@@ -39,7 +42,6 @@ name = "build"
harness = false
[dependencies]
rojo-project = { path = "crates/rojo-project" }
memofs = { version = "0.2.0", path = "crates/memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously
@@ -49,8 +51,8 @@ memofs = { version = "0.2.0", path = "crates/memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.6.4"
rbx_dom_weak = "2.3.0"
rbx_binary = "0.6.5"
rbx_dom_weak = "2.4.0"
rbx_reflection = "4.2.0"
rbx_reflection_database = "0.2.2"
rbx_xml = "0.12.3"
@@ -81,6 +83,8 @@ thiserror = "1.0.30"
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
uuid = { version = "1.0.0", features = ["v4", "serde"] }
clap = { version = "3.1.18", features = ["derive"] }
profiling = "1.0.6"
tracy-client = { version = "0.13.2", optional = true }
[target.'cfg(windows)'.dependencies]
winreg = "0.10.1"

View File

@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome!
Rojo supports Rust 1.46.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
Rojo supports Rust 1.57.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

BIN
assets/NotificationPop.mp3 Normal file

Binary file not shown.

View File

@@ -21,7 +21,7 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
// We can skip any TestEZ test files since they aren't necessary for
// the plugin to run.
if file_name.ends_with(".spec.lua") {
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
continue;
}

View File

@@ -1,16 +0,0 @@
[package]
name = "rojo-project"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.57"
globset = { version = "0.4.8", features = ["serde1"] }
log = "0.4.17"
rbx_dom_weak = "2.3.0"
rbx_reflection = "4.2.0"
rbx_reflection_database = "0.2.4"
serde = { version = "1.0.137", features = ["derive"] }
serde_json = "1.0.81"

View File

@@ -1,4 +0,0 @@
# rojo-project
Project file format crate for [Rojo].
[Rojo]: https://rojo.space

View File

@@ -1,7 +0,0 @@
pub mod glob;
mod path_serializer;
mod project;
mod resolution;
pub use project::{OptionalPathNode, PathNode, Project, ProjectNode};
pub use resolution::{AmbiguousValue, UnresolvedValue};

View File

@@ -1,21 +0,0 @@
//! Path serializer is used to serialize absolute paths in a cross-platform way,
//! by replacing all directory separators with /.
use std::path::Path;
use serde::Serializer;
pub fn serialize_absolute<S, T>(path: T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<Path>,
{
let as_str = path
.as_ref()
.as_os_str()
.to_str()
.expect("Invalid Unicode in file path, cannot serialize");
let replaced = as_str.replace("\\", "/");
serializer.serialize_str(&replaced)
}

View File

@@ -1,363 +0,0 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::glob::Glob;
use crate::resolution::UnresolvedValue;
static PROJECT_FILENAME: &str = "default.project.json";
/// Contains all of the configuration for a Rojo-managed project.
///
/// Rojo project files are stored in `.project.json` files.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Project {
/// The name of the top-level instance described by the project.
pub name: String,
/// The tree of instances described by this project. Projects always
/// describe at least one instance.
pub tree: ProjectNode,
/// If specified, sets the default port that `rojo serve` should use when
/// using this project for live sync.
///
/// Can be overriden with the `--port` flag.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_port: Option<u16>,
/// If specified, sets the default IP address that `rojo serve` should use
/// when using this project for live sync.
///
/// Can be overridden with the `--address` flag.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_address: Option<IpAddr>,
/// If specified, contains the set of place IDs that this project is
/// compatible with when doing live sync.
///
/// This setting is intended to help prevent syncing a Rojo project into the
/// wrong Roblox place.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_place_ids: Option<HashSet<u64>>,
/// If specified, sets the current place's place ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub place_id: Option<u64>,
/// If specified, sets the current place's game ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub game_id: Option<u64>,
/// A list of globs, relative to the folder the project file is in, that
/// match files that should be excluded if Rojo encounters them.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub glob_ignore_paths: Vec<Glob>,
/// The path to the file that this project came from. Relative paths in the
/// project should be considered relative to the parent of this field, also
/// given by `Project::folder_location`.
#[serde(skip)]
pub file_location: PathBuf,
}
impl Project {
/// Tells whether the given path describes a Rojo project.
pub fn is_project_file(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with(".project.json"))
.unwrap_or(false)
}
/// Loads a project file from a slice and a path that indicates where the
/// project should resolve paths relative to.
pub fn load_from_slice(contents: &[u8], project_file_location: &Path) -> anyhow::Result<Self> {
let mut project: Self = serde_json::from_slice(&contents).with_context(|| {
format!(
"Error parsing Rojo project at {}",
project_file_location.display()
)
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
/// Fuzzy-find a Rojo project and load it.
pub fn load_fuzzy(fuzzy_project_location: &Path) -> anyhow::Result<Option<Self>> {
if let Some(project_path) = Self::locate(fuzzy_project_location) {
let project = Self::load_exact(&project_path)?;
Ok(Some(project))
} else {
Ok(None)
}
}
/// Gives the path that all project file paths should resolve relative to.
pub fn folder_location(&self) -> &Path {
self.file_location.parent().unwrap()
}
/// Attempt to locate a project represented by the given path.
///
/// This will find a project if the path refers to a `.project.json` file,
/// or is a folder that contains a `default.project.json` file.
fn locate(path: &Path) -> Option<PathBuf> {
let meta = fs::metadata(path).ok()?;
if meta.is_file() {
if Project::is_project_file(path) {
Some(path.to_path_buf())
} else {
None
}
} else {
let child_path = path.join(PROJECT_FILENAME);
let child_meta = fs::metadata(&child_path).ok()?;
if child_meta.is_file() {
Some(child_path)
} else {
// This is a folder with the same name as a Rojo default project
// file.
//
// That's pretty weird, but we can roll with it.
None
}
}
}
fn load_exact(project_file_location: &Path) -> anyhow::Result<Self> {
let contents = fs::read_to_string(project_file_location)?;
let mut project: Project = serde_json::from_str(&contents).with_context(|| {
format!(
"Error parsing Rojo project at {}",
project_file_location.display()
)
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
/// Checks if there are any compatibility issues with this project file and
/// warns the user if there are any.
fn check_compatibility(&self) {
self.tree.validate_reserved_names();
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct OptionalPathNode {
#[serde(serialize_with = "crate::path_serializer::serialize_absolute")]
pub optional: PathBuf,
}
impl OptionalPathNode {
pub fn new(optional: PathBuf) -> Self {
OptionalPathNode { optional }
}
}
/// Describes a path that is either optional or required
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PathNode {
Required(#[serde(serialize_with = "crate::path_serializer::serialize_absolute")] PathBuf),
Optional(OptionalPathNode),
}
impl PathNode {
pub fn path(&self) -> &Path {
match self {
PathNode::Required(pathbuf) => &pathbuf,
PathNode::Optional(OptionalPathNode { optional }) => &optional,
}
}
}
/// Describes an instance and its descendants in a project.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProjectNode {
/// If set, defines the ClassName of the described instance.
///
/// `$className` MUST be set if `$path` is not set.
///
/// `$className` CANNOT be set if `$path` is set and the instance described
/// by that path has a ClassName other than Folder.
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
/// Contains all of the children of the described instance.
#[serde(flatten)]
pub children: BTreeMap<String, ProjectNode>,
/// The properties that will be assigned to the resulting instance.
#[serde(
rename = "$properties",
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
/// a large hammer and used with care.
///
/// If set to `true`, those instances will be left alone. This may cause
/// issues when files that turn into instances are removed while Rojo is not
/// running.
///
/// If set to `false`, Rojo will destroy any instances it does not
/// recognize.
///
/// If unset, its default value depends on other settings:
/// - If `$path` is not set, defaults to `true`
/// - If `$path` is set, defaults to `false`
#[serde(
rename = "$ignoreUnknownInstances",
skip_serializing_if = "Option::is_none"
)]
pub ignore_unknown_instances: Option<bool>,
/// Defines that this instance should come from the given file path. This
/// path can point to any file type supported by Rojo, including Lua files
/// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table
/// spreadsheets (`.csv`).
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
pub path: Option<PathNode>,
}
impl ProjectNode {
fn validate_reserved_names(&self) {
for (name, child) in &self.children {
if name.starts_with('$') {
log::warn!(
"Keys starting with '$' are reserved by Rojo to ensure forward compatibility."
);
log::warn!(
"This project uses the key '{}', which should be renamed.",
name
);
}
child.validate_reserved_names();
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_node_required() {
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
}
#[test]
fn path_node_optional() {
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
assert_eq!(
path_node,
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
);
}
#[test]
fn project_node_required() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "src"
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Required(PathBuf::from("src")))
);
}
#[test]
fn project_node_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "src" }
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Optional(OptionalPathNode::new(PathBuf::from(
"src"
))))
);
}
#[test]
fn project_node_none() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$className": "Folder"
}"#,
)
.unwrap();
assert_eq!(project_node.path, None);
}
#[test]
fn project_node_optional_serialize_absolute() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "..\\src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_absolute_no_change() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "../src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "..\\src"
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":"../src"}"#);
}
}

View File

@@ -1,294 +0,0 @@
use std::borrow::Borrow;
use anyhow::format_err;
use rbx_dom_weak::types::{
CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
/// A user-friendly version of `Variant` that supports specifying ambiguous
/// values. Ambiguous values need a reflection database to be resolved to a
/// usable value.
///
/// This type is used in Rojo projects and JSON models to make specifying the
/// most common types of properties, like strings or vectors, much easier.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UnresolvedValue {
FullyQualified(Variant),
Ambiguous(AmbiguousValue),
}
impl UnresolvedValue {
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
match self {
UnresolvedValue::FullyQualified(full) => Ok(full),
UnresolvedValue::Ambiguous(partial) => partial.resolve(class_name, prop_name),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AmbiguousValue {
Bool(bool),
String(String),
StringArray(Vec<String>),
Number(f64),
Array2([f64; 2]),
Array3([f64; 3]),
Array4([f64; 4]),
Array12([f64; 12]),
}
impl AmbiguousValue {
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
let property = find_descriptor(class_name, prop_name)
.ok_or_else(|| format_err!("Unknown property {}.{}", class_name, prop_name))?;
match &property.data_type {
DataType::Enum(enum_name) => {
let database = rbx_reflection_database::get();
let enum_descriptor = database.enums.get(enum_name).ok_or_else(|| {
format_err!("Unknown enum {}. This is a Rojo bug!", enum_name)
})?;
let error = |what: &str| {
let mut all_values = enum_descriptor
.items
.keys()
.map(|value| value.borrow())
.collect::<Vec<_>>();
all_values.sort();
let examples = nonexhaustive_list(&all_values);
format_err!(
"Invalid value for property {}.{}. Got {} but \
expected a member of the {} enum such as {}",
class_name,
prop_name,
what,
enum_name,
examples,
)
};
let value = match self {
AmbiguousValue::String(value) => value,
unresolved => return Err(error(unresolved.describe())),
};
let resolved = enum_descriptor
.items
.get(value.as_str())
.ok_or_else(|| error(value.as_str()))?;
Ok(Enum::from_u32(*resolved).into())
}
DataType::Value(variant_ty) => match (variant_ty, self) {
(VariantType::Bool, AmbiguousValue::Bool(value)) => Ok(value.into()),
(VariantType::Float32, AmbiguousValue::Number(value)) => Ok((value as f32).into()),
(VariantType::Float64, AmbiguousValue::Number(value)) => Ok(value.into()),
(VariantType::Int32, AmbiguousValue::Number(value)) => Ok((value as i32).into()),
(VariantType::Int64, AmbiguousValue::Number(value)) => Ok((value as i64).into()),
(VariantType::String, AmbiguousValue::String(value)) => Ok(value.into()),
(VariantType::Tags, AmbiguousValue::StringArray(value)) => {
Ok(Tags::from(value).into())
}
(VariantType::Content, AmbiguousValue::String(value)) => {
Ok(Content::from(value).into())
}
(VariantType::Vector2, AmbiguousValue::Array2(value)) => {
Ok(Vector2::new(value[0] as f32, value[1] as f32).into())
}
(VariantType::Vector3, AmbiguousValue::Array3(value)) => {
Ok(Vector3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
}
(VariantType::Color3, AmbiguousValue::Array3(value)) => {
Ok(Color3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
}
(VariantType::CFrame, AmbiguousValue::Array12(value)) => {
let value = value.map(|v| v as f32);
let pos = Vector3::new(value[0], value[1], value[2]);
let orientation = Matrix3::new(
Vector3::new(value[3], value[4], value[5]),
Vector3::new(value[6], value[7], value[8]),
Vector3::new(value[9], value[10], value[11]),
);
Ok(CFrame::new(pos, orientation).into())
}
(_, unresolved) => Err(format_err!(
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
class_name,
prop_name,
variant_ty,
unresolved.describe(),
)),
},
_ => Err(format_err!(
"Unknown data type for property {}.{}",
class_name,
prop_name
)),
}
}
fn describe(&self) -> &'static str {
match self {
AmbiguousValue::Bool(_) => "a bool",
AmbiguousValue::String(_) => "a string",
AmbiguousValue::StringArray(_) => "an array of strings",
AmbiguousValue::Number(_) => "a number",
AmbiguousValue::Array2(_) => "an array of two numbers",
AmbiguousValue::Array3(_) => "an array of three numbers",
AmbiguousValue::Array4(_) => "an array of four numbers",
AmbiguousValue::Array12(_) => "an array of twelve numbers",
}
}
}
fn find_descriptor(
class_name: &str,
prop_name: &str,
) -> Option<&'static PropertyDescriptor<'static>> {
let database = rbx_reflection_database::get();
let mut current_class_name = class_name;
loop {
let class = database.classes.get(current_class_name)?;
if let Some(descriptor) = class.properties.get(prop_name) {
return Some(descriptor);
}
current_class_name = class.superclass.as_deref()?;
}
}
/// Outputs a string containing up to MAX_ITEMS entries from the given list. If
/// there are more than MAX_ITEMS items, the number of remaining items will be
/// listed.
fn nonexhaustive_list(values: &[&str]) -> String {
use std::fmt::Write;
const MAX_ITEMS: usize = 8;
let mut output = String::new();
let last_index = values.len() - 1;
let main_length = last_index.min(9);
let main_list = &values[..main_length];
for value in main_list {
output.push_str(value);
output.push_str(", ");
}
if values.len() > MAX_ITEMS {
write!(output, "or {} more", values.len() - main_length).unwrap();
} else {
output.push_str("or ");
output.push_str(values[values.len() - 1]);
}
output
}
#[cfg(test)]
mod test {
use super::*;
fn resolve(class: &str, prop: &str, json_value: &str) -> Variant {
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
unresolved.resolve(class, prop).unwrap()
}
#[test]
fn bools() {
assert_eq!(resolve("BoolValue", "Value", "false"), Variant::Bool(false));
// Script.Disabled is inherited from BaseScript
assert_eq!(resolve("Script", "Disabled", "true"), Variant::Bool(true));
}
#[test]
fn strings() {
// String literals can stay as strings
assert_eq!(
resolve("StringValue", "Value", "\"Hello!\""),
Variant::String("Hello!".into()),
);
// String literals can also turn into Content
assert_eq!(
resolve("Sky", "MoonTextureId", "\"rbxassetid://12345\""),
Variant::Content("rbxassetid://12345".into()),
);
// What about BinaryString values? For forward-compatibility reasons, we
// don't support any shorthands for BinaryString.
//
// assert_eq!(
// resolve("Folder", "Tags", "\"a\\u0000b\\u0000c\""),
// Variant::BinaryString(b"a\0b\0c".to_vec().into()),
// );
}
#[test]
fn numbers() {
assert_eq!(
resolve("Part", "CollisionGroupId", "123"),
Variant::Int32(123),
);
assert_eq!(
resolve("Folder", "SourceAssetId", "532413"),
Variant::Int64(532413),
);
assert_eq!(resolve("Part", "Transparency", "1"), Variant::Float32(1.0));
assert_eq!(resolve("NumberValue", "Value", "1"), Variant::Float64(1.0));
}
#[test]
fn vectors() {
assert_eq!(
resolve("ParticleEmitter", "SpreadAngle", "[1, 2]"),
Variant::Vector2(Vector2::new(1.0, 2.0)),
);
assert_eq!(
resolve("Part", "Position", "[4, 5, 6]"),
Variant::Vector3(Vector3::new(4.0, 5.0, 6.0)),
);
}
#[test]
fn colors() {
assert_eq!(
resolve("Part", "Color", "[1, 1, 1]"),
Variant::Color3(Color3::new(1.0, 1.0, 1.0)),
);
// There aren't any user-facing Color3uint8 properties. If there are
// some, we should treat them the same in the future.
}
#[test]
fn enums() {
assert_eq!(
resolve("Lighting", "Technology", "\"Voxel\""),
Variant::Enum(Enum::from_u32(1)),
);
}
}

View File

@@ -1,4 +1,4 @@
[tools]
rojo = { source = "rojo-rbx/rojo", version = "7.1.1" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
selene = { source = "Kampfkarren/selene", version = "0.17.0" }
selene = { source = "Kampfkarren/selene", version = "0.18.2" }

View File

@@ -23,8 +23,45 @@ end
local ALL_AXES = {"X", "Y", "Z"}
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
local EncodedValue = {}
local types
types = {
Attributes = {
fromPod = function(pod)
local output = {}
for key, value in pairs(pod) do
local ok, result = EncodedValue.decode(value)
if ok then
output[key] = result
else
local warning = ("Could not decode attribute value of type %q: %s"):format(typeof(value), tostring(result))
warn(warning)
end
end
return output
end,
toPod = function(roblox)
local output = {}
for key, value in pairs(roblox) do
local ok, result = EncodedValue.encodeNaive(value)
if ok then
output[key] = result
else
local warning = ("Could not encode attribute value of type %q: %s"):format(typeof(value), tostring(result))
warn(warning)
end
end
return output
end,
},
Axes = {
fromPod = function(pod)
local axes = {}
@@ -433,8 +470,6 @@ types = {
},
}
local EncodedValue = {}
function EncodedValue.decode(encodedValue)
local ty, value = next(encodedValue)
@@ -459,4 +494,19 @@ function EncodedValue.encode(rbxValue, propertyType)
}
end
local propertyTypeRenames = {
number = "Float64",
boolean = "Bool",
string = "String",
}
function EncodedValue.encodeNaive(rbxValue)
local propertyType = typeof(rbxValue)
if propertyTypeRenames[propertyType] ~= nil then
propertyType = propertyTypeRenames[propertyType]
end
return EncodedValue.encode(rbxValue, propertyType)
end
return EncodedValue

View File

@@ -1,4 +1,73 @@
{
"Attributes": {
"value": {
"Attributes": {
"TestBool": {
"Bool": true
},
"TestBrickColor": {
"BrickColor": 24
},
"TestColor3": {
"Color3": [
1.0,
0.5,
0.0
]
},
"TestNumber": {
"Float64": 1337.0
},
"TestRect": {
"Rect": [
[
1.0,
2.0
],
[
3.0,
4.0
]
]
},
"TestString": {
"String": "Test"
},
"TestUDim": {
"UDim": [
1.0,
2
]
},
"TestUDim2": {
"UDim2": [
[
1.0,
2
],
[
3.0,
4
]
]
},
"TestVector2": {
"Vector2": [
1.0,
2.0
]
},
"TestVector3": {
"Vector3": [
1.0,
2.0,
3.0
]
}
}
},
"ty": "Attributes"
},
"Axes": {
"value": {
"Axes": [

View File

@@ -5,6 +5,26 @@ local CollectionService = game:GetService("CollectionService")
-- The reflection database refers to these as having scriptability = "Custom"
return {
Instance = {
Attributes = {
read = function(instance)
return true, instance:GetAttributes()
end,
write = function(instance, _, value)
local existing = instance:GetAttributes()
for key, attr in pairs(value) do
instance:SetAttribute(key, attr)
end
for key in pairs(existing) do
if value[key] == nil then
instance:SetAttribute(key, nil)
end
end
return true
end,
},
Tags = {
read = function(instance)
return true, CollectionService:GetTags(instance)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
local TextService = game:GetService("TextService")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Flipper = require(Rojo.Flipper)
local bindingUtil = require(script.Parent.bindingUtil)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local baseClock = DateTime.now().UnixTimestampMillis
local e = Roact.createElement
local Notification = Roact.Component:extend("Notification")
function Notification:init()
self.motor = Flipper.SingleMotor.new(0)
self.binding = bindingUtil.fromMotor(self.motor)
self.lifetime = self.props.timeout
self.motor:onStep(function(value)
if value <= 0 then
if self.props.onClose then
self.props.onClose()
end
end
end)
end
function Notification:dismiss()
self.motor:setGoal(
Flipper.Spring.new(0, {
frequency = 5,
dampingRatio = 1,
})
)
end
function Notification:didMount()
self.motor:setGoal(
Flipper.Spring.new(1, {
frequency = 3,
dampingRatio = 1,
})
)
self.props.soundPlayer:play(Assets.Sounds.Notification)
self.timeout = task.spawn(function()
local clock = os.clock()
local seen = false
while task.wait(1/10) do
local now = os.clock()
local dt = now - clock
clock = now
if not seen then
seen = StudioService.ActiveScript == nil
end
if not seen then
-- Don't run down timer before being viewed
continue
end
self.lifetime -= dt
if self.lifetime <= 0 then
self:dismiss()
break
end
end
end)
end
function Notification:willUnmount()
task.cancel(self.timeout)
end
function Notification:render()
local time = DateTime.fromUnixTimestampMillis(self.props.timestamp)
local textBounds = TextService:GetTextSize(
self.props.text,
15,
Enum.Font.GothamSemibold,
Vector2.new(350, 700)
)
local transparency = self.binding:map(function(value)
return 1 - value
end)
local size = self.binding:map(function(value)
return UDim2.fromOffset(
(35+40+textBounds.X)*value,
math.max(14+20+textBounds.Y, 32+20)
)
end)
return Theme.with(function(theme)
return e("TextButton", {
BackgroundTransparency = 1,
Size = size,
LayoutOrder = self.props.layoutOrder,
Text = "",
ClipsDescendants = true,
[Roact.Event.Activated] = function()
self:dismiss()
end,
}, {
e(BorderedContainer, {
transparency = transparency,
size = UDim2.new(1, 0, 1, 0),
}, {
TextContainer = e("Frame", {
Size = UDim2.new(0, 35+textBounds.X, 1, -20),
Position = UDim2.new(0, 0, 0, 10),
BackgroundTransparency = 1
}, {
Logo = e("ImageLabel", {
ImageTransparency = transparency,
Image = Assets.Images.PluginButton,
BackgroundTransparency = 1,
Size = UDim2.new(0, 32, 0, 32),
Position = UDim2.new(0, 0, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
}),
Info = e("TextLabel", {
Text = self.props.text,
Font = Enum.Font.GothamSemibold,
TextSize = 15,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
Position = UDim2.fromOffset(35, 0),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Time = e("TextLabel", {
Text = time:FormatLocalTime("LTS", "en-us"),
Font = Enum.Font.Code,
TextSize = 12,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, -35, 0, 14),
Position = UDim2.new(0, 35, 1, -14),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15),
}),
})
})
end)
end
local Notifications = Roact.Component:extend("Notifications")
function Notifications:render()
local notifs = {}
for index, notif in ipairs(self.props.notifications) do
notifs[notif] = e(Notification, {
soundPlayer = self.props.soundPlayer,
text = notif.text,
timestamp = notif.timestamp,
timeout = notif.timeout,
layoutOrder = (notif.timestamp - baseClock),
onClose = function()
self.props.onClose(index)
end,
})
end
return Roact.createFragment(notifs)
end
return Notifications

View File

@@ -9,6 +9,8 @@ local Roact = require(Rojo.Roact)
local defaultSettings = {
openScriptsExternally = false,
twoWaySync = false,
showNotifications = true,
playSounds = true,
}
local Settings = {}
@@ -118,4 +120,4 @@ end
return {
StudioProvider = StudioProvider,
with = with,
}
}

View File

@@ -202,12 +202,28 @@ function SettingsPage:render()
layoutOrder = 1,
}),
ShowNotifications = e(Setting, {
id = "showNotifications",
name = "Show Notifications",
description = "Popup notifications in viewport",
transparency = self.props.transparency,
layoutOrder = 2,
}),
PlaySounds = e(Setting, {
id = "playSounds",
name = "Play Sounds",
description = "Toggle sound effects",
transparency = self.props.transparency,
layoutOrder = 3,
}),
TwoWaySync = e(Setting, {
id = "twoWaySync",
name = "Two-Way Sync",
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
transparency = self.props.transparency,
layoutOrder = 2,
layoutOrder = 4,
}),
Layout = e("UIListLayout", {
@@ -227,4 +243,4 @@ function SettingsPage:render()
end)
end
return SettingsPage
return SettingsPage

View File

@@ -103,6 +103,10 @@ local lightTheme = strict("LightTheme", {
LogoColor = BRAND_COLOR,
VersionColor = hexColor(0x727272),
},
Notification = {
InfoColor = hexColor(0x00000),
CloseColor = BRAND_COLOR,
},
ErrorColor = hexColor(0x000000),
ScrollBarColor = hexColor(0x000000),
})
@@ -177,6 +181,10 @@ local darkTheme = strict("DarkTheme", {
LogoColor = BRAND_COLOR,
VersionColor = hexColor(0xD3D3D3)
},
Notification = {
InfoColor = hexColor(0xFFFFFF),
CloseColor = hexColor(0xFFFFFF),
},
ErrorColor = hexColor(0xFFFFFF),
ScrollBarColor = hexColor(0xFFFFFF),
})

View File

@@ -12,10 +12,12 @@ local Dictionary = require(Plugin.Dictionary)
local ServeSession = require(Plugin.ServeSession)
local ApiContext = require(Plugin.ApiContext)
local preloadAssets = require(Plugin.preloadAssets)
local soundPlayer = require(Plugin.soundPlayer)
local Theme = require(script.Theme)
local PluginSettings = require(script.PluginSettings)
local Page = require(script.Page)
local Notifications = require(script.Notifications)
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
@@ -44,10 +46,37 @@ function App:init()
self:setState({
appStatus = AppStatus.NotConnected,
guiEnabled = false,
notifications = {},
toolbarIcon = Assets.Images.PluginButton,
})
end
function App:addNotification(text: string, timeout: number?)
if not self.props.settings:get("showNotifications") then
return
end
local notifications = table.clone(self.state.notifications)
table.insert(notifications, {
text = text,
timestamp = DateTime.now().UnixTimestampMillis,
timeout = timeout or 3,
})
self:setState({
notifications = notifications,
})
end
function App:closeNotification(index: number)
local notifications = table.clone(self.state.notifications)
table.remove(notifications, index)
self:setState({
notifications = notifications,
})
end
function App:getHostAndPort()
local host = self.host:getValue()
local port = self.port:getValue()
@@ -81,6 +110,7 @@ function App:startSession()
appStatus = AppStatus.Connecting,
toolbarIcon = Assets.Images.PluginButton,
})
self:addNotification("Connecting to session...")
elseif status == ServeSession.Status.Connected then
local address = ("%s:%s"):format(host, port)
self:setState({
@@ -89,8 +119,7 @@ function App:startSession()
address = address,
toolbarIcon = Assets.Images.PluginButtonConnected,
})
Log.info("Connected to session '{}' at {}", details, address)
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil
@@ -104,13 +133,13 @@ function App:startSession()
errorMessage = tostring(details),
toolbarIcon = Assets.Images.PluginButtonWarning,
})
self:addNotification(tostring(details), 10)
else
self:setState({
appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton,
})
Log.info("Disconnected session")
self:addNotification("Disconnected from session.")
end
end
end)
@@ -236,6 +265,22 @@ function App:render()
end),
}),
RojoNotifications = e("ScreenGui", {}, {
layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, 5),
}),
notifs = e(Notifications, {
soundPlayer = self.props.soundPlayer,
notifications = self.state.notifications,
onClose = function(index)
self:closeNotification(index)
end,
}),
}),
toggleAction = e(StudioPluginAction, {
name = "RojoConnection",
title = "Rojo: Connect/Disconnect",
@@ -304,10 +349,11 @@ return function(props)
plugin = props.plugin,
}, {
App = PluginSettings.with(function(settings)
local settingsProps = Dictionary.merge(props, {
local mergedProps = Dictionary.merge(props, {
settings = settings,
soundPlayer = soundPlayer.new(settings),
})
return e(App, settingsProps)
return e(App, mergedProps)
end),
})
end

View File

@@ -45,6 +45,9 @@ local Assets = {
[500] = "rbxassetid://2609138523"
},
},
Sounds = {
Notification = "rbxassetid://203785492",
},
StartSession = "",
SessionActive = "",
Configure = "",
@@ -62,4 +65,4 @@ end
guardForTypos("Assets", Assets)
return Assets
return Assets

View File

@@ -5,8 +5,8 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
return strict("Config", {
isDevBuild = isDevBuild,
codename = "Epiphany",
version = {7, 1, 1},
expectedServerVersionString = "7.0 or newer",
version = {7, 2, 1},
expectedServerVersionString = "7.2 or newer",
protocolVersion = 4,
defaultHost = "localhost",
defaultPort = 34872,

View File

@@ -18,7 +18,7 @@ local App = require(script.App)
local app = Roact.createElement(App, {
plugin = plugin
})
local tree = Roact.mount(app, nil, "Rojo UI")
local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI")
plugin.Unloading:Connect(function()
Roact.unmount(tree)
@@ -28,4 +28,4 @@ if Config.isDevBuild then
local TestEZ = require(script.Parent.TestEZ)
require(script.runTests)(TestEZ)
end
end

View File

@@ -0,0 +1,35 @@
-- Sounds only play in Edit mode when parented to a plugin widget, for some reason
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
local widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new(
Enum.InitialDockState.Float,
false, true,
10, 10,
10, 10
))
widget.Name = "Rojo_soundPlayer"
widget.Title = "Rojo Sound Player"
local SoundPlayer = {}
SoundPlayer.__index = SoundPlayer
function SoundPlayer.new(settings)
return setmetatable({
settings = settings,
}, SoundPlayer)
end
function SoundPlayer:play(soundId)
if self.settings and self.settings:get("playSounds") == false then return end
local sound = Instance.new("Sound")
sound.SoundId = soundId
sound.Parent = widget
sound.Ended:Connect(function()
sound:Destroy()
end)
sound:Play()
end
return SoundPlayer

View File

@@ -0,0 +1,24 @@
---
source: tests/tests/build.rs
assertion_line: 99
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">attributes</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">Explicit</string>
<BinaryString name="AttributesSerialize">AgAAAAUAAABIZWxsbwIFAAAAV29ybGQGAAAAVmVjdG9yEQAAgD8AAABAAABAQA==</BinaryString>
</Properties>
</Item>
<Item class="Folder" referent="2">
<Properties>
<string name="Name">ImplicitAttributes</string>
<BinaryString name="AttributesSerialize">AgAAAAMAAABIZXkCBwAAAEdyYW5kbWEGAAAAVmVjdG9yEQAAgEAAAKBAAADAQA==</BinaryString>
</Properties>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,14 @@
---
source: tests/tests/build.rs
assertion_line: 98
expression: contents
---
<roblox version="4">
<Item class="LocalScript" referent="0">
<Properties>
<string name="Name">issue_546</string>
<bool name="Disabled">true</bool>
<string name="Source">print("Hello, world!")</string>
</Properties>
</Item>
</roblox>

View File

@@ -1,14 +1,13 @@
---
source: tests/tests/build.rs
assertion_line: 99
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">weldconstraint</string>
<BinaryString name="AttributesSerialize">
</BinaryString>
<BinaryString name="AttributesSerialize"></BinaryString>
<int64 name="SourceAssetId">-1</int64>
<BinaryString name="Tags"></BinaryString>
</Properties>
@@ -16,8 +15,7 @@ expression: contents
<Properties>
<string name="Name">A</string>
<bool name="Anchored">false</bool>
<BinaryString name="AttributesSerialize">
</BinaryString>
<BinaryString name="AttributesSerialize"></BinaryString>
<float name="BackParamA">-0.5</float>
<float name="BackParamB">0.5</float>
<token name="BackSurface">0</token>
@@ -108,8 +106,7 @@ expression: contents
<Item class="WeldConstraint" referent="2">
<Properties>
<string name="Name">WeldConstraint</string>
<BinaryString name="AttributesSerialize">
</BinaryString>
<BinaryString name="AttributesSerialize"></BinaryString>
<CoordinateFrame name="CFrame0">
<X>7</X>
<Y>0.000001013279</Y>
@@ -136,8 +133,7 @@ expression: contents
<Properties>
<string name="Name">B</string>
<bool name="Anchored">false</bool>
<BinaryString name="AttributesSerialize">
</BinaryString>
<BinaryString name="AttributesSerialize"></BinaryString>
<float name="BackParamA">-0.5</float>
<float name="BackParamB">0.5</float>
<token name="BackSurface">0</token>

View File

@@ -0,0 +1,36 @@
{
"name": "attributes",
"tree": {
"$className": "Folder",
"Explicit": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Attributes": {
"Hello": {
"String": "World"
},
"Vector": {
"Vector3": [1, 2, 3]
}
}
}
}
},
"ImplicitAttributes": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Hey": {
"String": "Grandma"
},
"Vector": {
"Vector3": [4, 5, 6]
}
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
# Issue #546 (https://github.com/rojo-rbx/rojo/issues/546)
Regression from Rojo 6.2.0 to Rojo 7.0.0. Meta files named as init.meta.json should apply after init.client.lua and other init files.

View File

@@ -0,0 +1,6 @@
{
"name": "issue_546",
"tree": {
"$path": "hello"
}
}

View File

@@ -0,0 +1 @@
print("Hello, world!")

View File

@@ -0,0 +1,5 @@
{
"properties": {
"Disabled": true
}
}

View File

@@ -291,7 +291,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
}
};
let patch_set = compute_patch_set(snapshot.as_ref(), &tree, id);
let patch_set = compute_patch_set(snapshot, &tree, id);
apply_patch_set(tree, patch_set)
}
Ok(None) => {
@@ -334,7 +334,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
}
};
let patch_set = compute_patch_set(snapshot.as_ref(), &tree, id);
let patch_set = compute_patch_set(snapshot, &tree, id);
apply_patch_set(tree, patch_set)
}
};

View File

@@ -1,5 +1,6 @@
use std::{
io::{BufWriter, Write},
mem::forget,
path::{Path, PathBuf},
};
@@ -61,6 +62,10 @@ impl BuildCommand {
}
}
// Avoid dropping ServeSession: it's potentially VERY expensive to drop
// and we're about to exit anyways.
forget(session);
Ok(())
}
}
@@ -97,6 +102,7 @@ fn xml_encode_config() -> rbx_xml::EncodeOptions {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
}
#[profiling::function]
fn write_model(
session: &ServeSession,
output: &Path,

View File

@@ -67,15 +67,17 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
let writer = BufferWriter::stdout(color);
let mut buffer = writer.buffer();
let address_string = if bind_address.is_loopback() {
"localhost".to_owned()
} else {
bind_address.to_string()
};
writeln!(&mut buffer, "Rojo server listening:")?;
write!(&mut buffer, " Address: ")?;
buffer.set_color(&green)?;
if bind_address.is_loopback() {
writeln!(&mut buffer, "localhost")?;
} else {
writeln!(&mut buffer, "{}", bind_address)?;
}
writeln!(&mut buffer, "{}", address_string)?;
buffer.set_color(&ColorSpec::new())?;
write!(&mut buffer, " Port: ")?;
@@ -88,7 +90,7 @@ fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io
write!(&mut buffer, "Visit ")?;
buffer.set_color(&green)?;
write!(&mut buffer, "http://localhost:{}/", port)?;
write!(&mut buffer, "http://{}:{}/", address_string, port)?;
buffer.set_color(&ColorSpec::new())?;
writeln!(&mut buffer, " in your browser for more information.")?;

View File

@@ -1,3 +1,6 @@
//! Wrapper around globset's Glob type that has better serialization
//! characteristics by coupling Glob and GlobMatcher into a single type.
use std::path::Path;
use globset::{Glob as InnerGlob, GlobMatcher};
@@ -5,8 +8,6 @@ use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
pub use globset::Error;
/// Wrapper around globset's Glob type that has better serialization
/// characteristics by coupling Glob and GlobMatcher into a single type.
#[derive(Debug, Clone)]
pub struct Glob {
inner: InnerGlob,

View File

@@ -9,6 +9,7 @@ mod tree_view;
mod auth_cookie;
mod change_processor;
mod glob;
mod lua_ast;
mod message_queue;
mod multimap;

View File

@@ -6,6 +6,9 @@ use clap::Parser;
use librojo::cli::Options;
fn main() {
#[cfg(feature = "profile-with-tracy")]
tracy_client::Client::start();
panic::set_hook(Box::new(|panic_info| {
// PanicInfo's payload is usually a &'static str or String.
// See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload

View File

@@ -1,3 +1,379 @@
pub use rojo_project::{OptionalPathNode, PathNode, Project, ProjectNode};
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs, io,
net::IpAddr,
path::{Path, PathBuf},
};
pub use anyhow::Error as ProjectError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{glob::Glob, resolution::UnresolvedValue};
static PROJECT_FILENAME: &str = "default.project.json";
/// Error type returned by any function that handles projects.
#[derive(Debug, Error)]
#[error(transparent)]
pub struct ProjectError(#[from] Error);
#[derive(Debug, Error)]
enum Error {
#[error(transparent)]
Io {
#[from]
source: io::Error,
},
#[error("Error parsing Rojo project in path {}", .path.display())]
Json {
source: serde_json::Error,
path: PathBuf,
},
}
/// Contains all of the configuration for a Rojo-managed project.
///
/// Project files are stored in `.project.json` files.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Project {
/// The name of the top-level instance described by the project.
pub name: String,
/// The tree of instances described by this project. Projects always
/// describe at least one instance.
pub tree: ProjectNode,
/// If specified, sets the default port that `rojo serve` should use when
/// using this project for live sync.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_port: Option<u16>,
/// If specified, contains the set of place IDs that this project is
/// compatible with when doing live sync.
///
/// This setting is intended to help prevent syncing a Rojo project into the
/// wrong Roblox place.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_place_ids: Option<HashSet<u64>>,
/// If specified, sets the current place's place ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub place_id: Option<u64>,
/// If specified, sets the current place's game ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub game_id: Option<u64>,
/// If specified, this address will be used in place of the default address
/// As long as --address is unprovided.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_address: Option<IpAddr>,
/// A list of globs, relative to the folder the project file is in, that
/// match files that should be excluded if Rojo encounters them.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub glob_ignore_paths: Vec<Glob>,
/// The path to the file that this project came from. Relative paths in the
/// project should be considered relative to the parent of this field, also
/// given by `Project::folder_location`.
#[serde(skip)]
pub file_location: PathBuf,
}
impl Project {
/// Tells whether the given path describes a Rojo project.
pub fn is_project_file(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with(".project.json"))
.unwrap_or(false)
}
/// Attempt to locate a project represented by the given path.
///
/// This will find a project if the path refers to a `.project.json` file,
/// or is a folder that contains a `default.project.json` file.
fn locate(path: &Path) -> Option<PathBuf> {
let meta = fs::metadata(path).ok()?;
if meta.is_file() {
if Project::is_project_file(path) {
Some(path.to_path_buf())
} else {
None
}
} else {
let child_path = path.join(PROJECT_FILENAME);
let child_meta = fs::metadata(&child_path).ok()?;
if child_meta.is_file() {
Some(child_path)
} else {
// This is a folder with the same name as a Rojo default project
// file.
//
// That's pretty weird, but we can roll with it.
None
}
}
}
pub fn load_from_slice(
contents: &[u8],
project_file_location: &Path,
) -> Result<Self, ProjectError> {
let mut project: Self =
serde_json::from_slice(&contents).map_err(|source| Error::Json {
source,
path: project_file_location.to_owned(),
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
pub fn load_fuzzy(fuzzy_project_location: &Path) -> Result<Option<Self>, ProjectError> {
if let Some(project_path) = Self::locate(fuzzy_project_location) {
let project = Self::load_exact(&project_path)?;
Ok(Some(project))
} else {
Ok(None)
}
}
fn load_exact(project_file_location: &Path) -> Result<Self, Error> {
let contents = fs::read_to_string(project_file_location)?;
let mut project: Project =
serde_json::from_str(&contents).map_err(|source| Error::Json {
source,
path: project_file_location.to_owned(),
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
/// Checks if there are any compatibility issues with this project file and
/// warns the user if there are any.
fn check_compatibility(&self) {
self.tree.validate_reserved_names();
}
pub fn folder_location(&self) -> &Path {
self.file_location.parent().unwrap()
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct OptionalPathNode {
#[serde(serialize_with = "crate::path_serializer::serialize_absolute")]
pub optional: PathBuf,
}
impl OptionalPathNode {
pub fn new(optional: PathBuf) -> Self {
OptionalPathNode { optional }
}
}
/// Describes a path that is either optional or required
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PathNode {
Required(#[serde(serialize_with = "crate::path_serializer::serialize_absolute")] PathBuf),
Optional(OptionalPathNode),
}
impl PathNode {
pub fn path(&self) -> &Path {
match self {
PathNode::Required(pathbuf) => &pathbuf,
PathNode::Optional(OptionalPathNode { optional }) => &optional,
}
}
}
/// Describes an instance and its descendants in a project.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProjectNode {
/// If set, defines the ClassName of the described instance.
///
/// `$className` MUST be set if `$path` is not set.
///
/// `$className` CANNOT be set if `$path` is set and the instance described
/// by that path has a ClassName other than Folder.
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
/// Contains all of the children of the described instance.
#[serde(flatten)]
pub children: BTreeMap<String, ProjectNode>,
/// The properties that will be assigned to the resulting instance.
///
// TODO: Is this legal to set if $path is set?
#[serde(
rename = "$properties",
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
/// a large hammer and used with care.
///
/// If set to `true`, those instances will be left alone. This may cause
/// issues when files that turn into instances are removed while Rojo is not
/// running.
///
/// If set to `false`, Rojo will destroy any instances it does not
/// recognize.
///
/// If unset, its default value depends on other settings:
/// - If `$path` is not set, defaults to `true`
/// - If `$path` is set, defaults to `false`
#[serde(
rename = "$ignoreUnknownInstances",
skip_serializing_if = "Option::is_none"
)]
pub ignore_unknown_instances: Option<bool>,
/// Defines that this instance should come from the given file path. This
/// path can point to any file type supported by Rojo, including Lua files
/// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table
/// spreadsheets (`.csv`).
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
pub path: Option<PathNode>,
}
impl ProjectNode {
fn validate_reserved_names(&self) {
for (name, child) in &self.children {
if name.starts_with('$') {
log::warn!(
"Keys starting with '$' are reserved by Rojo to ensure forward compatibility."
);
log::warn!(
"This project uses the key '{}', which should be renamed.",
name
);
}
child.validate_reserved_names();
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_node_required() {
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
}
#[test]
fn path_node_optional() {
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
assert_eq!(
path_node,
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
);
}
#[test]
fn project_node_required() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "src"
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Required(PathBuf::from("src")))
);
}
#[test]
fn project_node_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "src" }
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Optional(OptionalPathNode::new(PathBuf::from(
"src"
))))
);
}
#[test]
fn project_node_none() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$className": "Folder"
}"#,
)
.unwrap();
assert_eq!(project_node.path, None);
}
#[test]
fn project_node_optional_serialize_absolute() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "..\\src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_absolute_no_change() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "../src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "..\\src"
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":"../src"}"#);
}
}

View File

@@ -2,7 +2,8 @@ use std::borrow::Borrow;
use anyhow::format_err;
use rbx_dom_weak::types::{
CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
Attributes, CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2,
Vector3,
};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
@@ -40,6 +41,7 @@ pub enum AmbiguousValue {
Array3([f64; 3]),
Array4([f64; 4]),
Array12([f64; 12]),
Attributes(Attributes),
}
impl AmbiguousValue {
@@ -128,6 +130,8 @@ impl AmbiguousValue {
Ok(CFrame::new(pos, orientation).into())
}
(VariantType::Attributes, AmbiguousValue::Attributes(value)) => Ok(value.into()),
(_, unresolved) => Err(format_err!(
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
class_name,
@@ -154,6 +158,7 @@ impl AmbiguousValue {
AmbiguousValue::Array3(_) => "an array of three numbers",
AmbiguousValue::Array4(_) => "an array of four numbers",
AmbiguousValue::Array12(_) => "an array of twelve numbers",
AmbiguousValue::Attributes(_) => "an object containing attributes",
}
}
}

View File

@@ -16,7 +16,7 @@ use thiserror::Error;
use crate::{
change_processor::ChangeProcessor,
message_queue::MessageQueue,
project::Project,
project::{Project, ProjectError},
session_id::SessionId,
snapshot::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
@@ -130,7 +130,7 @@ impl ServeSession {
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?;
log::trace!("Computing initial patch set");
let patch_set = compute_patch_set(snapshot.as_ref(), &tree, root_id);
let patch_set = compute_patch_set(snapshot, &tree, root_id);
log::trace!("Applying initial patch set");
apply_patch_set(&mut tree, patch_set);
@@ -237,6 +237,12 @@ pub enum ServeSessionError {
source: io::Error,
},
#[error(transparent)]
Project {
#[from]
source: ProjectError,
},
#[error(transparent)]
Other {
#[from]

View File

@@ -4,7 +4,7 @@ use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{
types::{Ref, Variant},
WeakDom,
Instance, WeakDom,
};
use serde::{Deserialize, Serialize};
@@ -102,22 +102,29 @@ impl InstanceSnapshot {
}
}
pub fn from_tree(tree: &WeakDom, id: Ref) -> Self {
let instance = tree.get_by_ref(id).expect("instance did not exist in tree");
#[profiling::function]
pub fn from_tree(tree: WeakDom, id: Ref) -> Self {
let (_, mut raw_tree) = tree.into_raw();
Self::from_raw_tree(&mut raw_tree, id)
}
fn from_raw_tree(raw_tree: &mut HashMap<Ref, Instance>, id: Ref) -> Self {
let instance = raw_tree
.remove(&id)
.expect("instance did not exist in tree");
let children = instance
.children()
.iter()
.copied()
.map(|id| Self::from_tree(tree, id))
.map(|&id| Self::from_raw_tree(raw_tree, id))
.collect();
Self {
snapshot_id: Some(id),
metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name.clone()),
class_name: Cow::Owned(instance.class.clone()),
properties: instance.properties.clone(),
name: Cow::Owned(instance.name),
class_name: Cow::Owned(instance.class),
properties: instance.properties,
children,
}
}

View File

@@ -4,10 +4,9 @@ use std::{
sync::Arc,
};
use rojo_project::glob::Glob;
use serde::{Deserialize, Serialize};
use crate::{path_serializer, project::ProjectNode};
use crate::{glob::Glob, path_serializer, project::ProjectNode};
/// Rojo-specific metadata that can be associated with an instance or a snapshot
/// of an instance.

View File

@@ -1,6 +1,9 @@
//! Defines the algorithm for applying generated patches.
use std::collections::HashMap;
use std::{
collections::{HashMap, HashSet},
mem::take,
};
use rbx_dom_weak::types::{Ref, Variant};
@@ -12,21 +15,31 @@ use super::{
/// Consumes the input `PatchSet`, applying all of its prescribed changes to the
/// tree and returns an `AppliedPatchSet`, which can be used to keep another
/// tree in sync with Rojo's.
#[profiling::function]
pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatchSet {
let mut context = PatchApplyContext::default();
for removed_id in patch_set.removed_instances {
apply_remove_instance(&mut context, tree, removed_id);
{
profiling::scope!("removals");
for removed_id in patch_set.removed_instances {
apply_remove_instance(&mut context, tree, removed_id);
}
}
for add_patch in patch_set.added_instances {
apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance);
{
profiling::scope!("additions");
for add_patch in patch_set.added_instances {
apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance);
}
}
// Updates need to be applied after additions, which reduces the complexity
// of updates significantly.
for update_patch in patch_set.updated_instances {
apply_update_child(&mut context, tree, update_patch);
{
profiling::scope!("updates");
// Updates need to be applied after additions, which reduces the complexity
// of updates significantly.
for update_patch in patch_set.updated_instances {
apply_update_child(&mut context, tree, update_patch);
}
}
finalize_patch_application(context, tree)
@@ -55,20 +68,9 @@ struct PatchApplyContext {
/// eachother.
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
/// The properties of instances added by the current `PatchSet`.
///
/// Instances added to the tree can refer to eachother via Ref properties,
/// but we need to make sure they're correctly transformed from snapshot
/// space into tree space (via `snapshot_id_to_instance_id`).
///
/// It's not possible to do that transformation for refs that refer to added
/// instances until all the instances have actually been inserted into the
/// tree. For simplicity, we defer application of _all_ properties on added
/// instances instead of just Refs.
///
/// This doesn't affect updated instances, since they're always applied
/// after we've added all the instances from the patch.
added_instance_properties: HashMap<Ref, HashMap<String, Variant>>,
/// Tracks all of the instances added by this patch that have refs that need
/// to be rewritten.
has_refs_to_rewrite: HashSet<Ref>,
/// The current applied patch result, describing changes made to the tree.
applied_patch_set: AppliedPatchSet,
@@ -84,23 +86,22 @@ struct PatchApplyContext {
/// The remaining Ref properties need to be handled during patch application,
/// where we build up a map of snapshot IDs to instance IDs as they're created,
/// then apply properties all at once at the end.
#[profiling::function]
fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -> AppliedPatchSet {
for (id, properties) in context.added_instance_properties {
for id in context.has_refs_to_rewrite {
// This should always succeed since instances marked as added in our
// patch should be added without fail.
let mut instance = tree
.get_instance_mut(id)
.expect("Invalid instance ID in deferred property map");
for (key, mut property_value) in properties {
if let Variant::Ref(referent) = property_value {
for value in instance.properties_mut().values_mut() {
if let Variant::Ref(referent) = value {
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent)
{
property_value = Variant::Ref(instance_referent);
*value = Variant::Ref(instance_referent);
}
}
instance.properties_mut().insert(key, property_value);
}
}
@@ -116,24 +117,24 @@ fn apply_add_child(
context: &mut PatchApplyContext,
tree: &mut RojoTree,
parent_id: Ref,
snapshot: InstanceSnapshot,
mut snapshot: InstanceSnapshot,
) {
let snapshot_id = snapshot.snapshot_id;
let properties = snapshot.properties;
let children = snapshot.children;
let children = take(&mut snapshot.children);
// Property application is deferred until after all children
// are constructed. This helps apply referents correctly.
let remaining_snapshot = InstanceSnapshot::new()
.name(snapshot.name)
.class_name(snapshot.class_name)
.metadata(snapshot.metadata)
.snapshot_id(snapshot.snapshot_id);
// If an object we're adding has a non-null referent, we'll note this
// instance down as needing to be revisited later.
let has_refs = snapshot.properties.values().any(|value| match value {
Variant::Ref(value) => value.is_some(),
_ => false,
});
let id = tree.insert_instance(parent_id, remaining_snapshot);
let id = tree.insert_instance(parent_id, snapshot);
context.applied_patch_set.added.push(id);
context.added_instance_properties.insert(id, properties);
if has_refs {
context.has_refs_to_rewrite.insert(id);
}
if let Some(snapshot_id) = snapshot_id {
context.snapshot_id_to_instance_id.insert(snapshot_id, id);

View File

@@ -1,7 +1,10 @@
//! Defines the algorithm for computing a roughly-minimal patch set given an
//! existing instance tree and an instance snapshot.
use std::collections::{HashMap, HashSet};
use std::{
collections::{HashMap, HashSet},
mem::take,
};
use rbx_dom_weak::types::{Ref, Variant};
@@ -10,11 +13,8 @@ use super::{
InstanceSnapshot, InstanceWithMeta, RojoTree,
};
pub fn compute_patch_set(
snapshot: Option<&InstanceSnapshot>,
tree: &RojoTree,
id: Ref,
) -> PatchSet {
#[profiling::function]
pub fn compute_patch_set(snapshot: Option<InstanceSnapshot>, tree: &RojoTree, id: Ref) -> PatchSet {
let mut patch_set = PatchSet::new();
if let Some(snapshot) = snapshot {
@@ -74,7 +74,7 @@ fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut Instan
fn compute_patch_set_internal(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
mut snapshot: InstanceSnapshot,
tree: &RojoTree,
id: Ref,
patch_set: &mut PatchSet,
@@ -87,12 +87,12 @@ fn compute_patch_set_internal(
.get_instance(id)
.expect("Instance did not exist in tree");
compute_property_patches(snapshot, &instance, patch_set);
compute_children_patches(context, snapshot, tree, id, patch_set);
compute_property_patches(&mut snapshot, &instance, patch_set);
compute_children_patches(context, &mut snapshot, tree, id, patch_set);
}
fn compute_property_patches(
snapshot: &InstanceSnapshot,
snapshot: &mut InstanceSnapshot,
instance: &InstanceWithMeta,
patch_set: &mut PatchSet,
) {
@@ -102,32 +102,32 @@ fn compute_property_patches(
let changed_name = if snapshot.name == instance.name() {
None
} else {
Some(snapshot.name.clone().into_owned())
Some(take(&mut snapshot.name).into_owned())
};
let changed_class_name = if snapshot.class_name == instance.class_name() {
None
} else {
Some(snapshot.class_name.clone().into_owned())
Some(take(&mut snapshot.class_name).into_owned())
};
let changed_metadata = if &snapshot.metadata == instance.metadata() {
None
} else {
Some(snapshot.metadata.clone())
Some(take(&mut snapshot.metadata))
};
for (name, snapshot_value) in &snapshot.properties {
visited_properties.insert(name.as_str());
for (name, snapshot_value) in take(&mut snapshot.properties) {
visited_properties.insert(name.clone());
match instance.properties().get(name) {
match instance.properties().get(&name) {
Some(instance_value) => {
if snapshot_value != instance_value {
changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
if &snapshot_value != instance_value {
changed_properties.insert(name, Some(snapshot_value));
}
}
None => {
changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
changed_properties.insert(name, Some(snapshot_value));
}
}
}
@@ -159,7 +159,7 @@ fn compute_property_patches(
fn compute_children_patches(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
snapshot: &mut InstanceSnapshot,
tree: &RojoTree,
id: Ref,
patch_set: &mut PatchSet,
@@ -172,7 +172,7 @@ fn compute_children_patches(
let mut paired_instances = vec![false; instance_children.len()];
for snapshot_child in snapshot.children.iter() {
for snapshot_child in take(&mut snapshot.children) {
let matching_instance =
instance_children
.iter()
@@ -209,7 +209,7 @@ fn compute_children_patches(
None => {
patch_set.added_instances.push(PatchAdd {
parent_id: id,
instance: snapshot_child.clone(),
instance: snapshot_child,
});
}
}
@@ -257,7 +257,7 @@ mod test {
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(&snapshot), &tree, root_id);
let patch_set = compute_patch_set(Some(snapshot), &tree, root_id);
let expected_patch_set = PatchSet {
updated_instances: vec![PatchUpdate {
@@ -307,7 +307,7 @@ mod test {
class_name: Cow::Borrowed("foo"),
};
let patch_set = compute_patch_set(Some(&snapshot), &tree, root_id);
let patch_set = compute_patch_set(Some(snapshot), &tree, root_id);
let expected_patch_set = PatchSet {
added_instances: vec![PatchAdd {

View File

@@ -23,7 +23,7 @@ fn set_name_and_class_name() {
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(&snapshot), &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
@@ -47,7 +47,7 @@ fn set_property() {
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(&snapshot), &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
@@ -78,7 +78,7 @@ fn remove_property() {
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(&snapshot), &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
@@ -107,7 +107,7 @@ fn add_child() {
}],
};
let patch_set = compute_patch_set(Some(&snapshot), &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);
@@ -139,7 +139,7 @@ fn remove_child() {
children: Vec::new(),
};
let patch_set = compute_patch_set(Some(&snapshot), &tree, tree.get_root_id());
let patch_set = compute_patch_set(Some(snapshot), &tree, tree.get_root_id());
let patch_value = redactions.redacted_yaml(patch_set);
assert_yaml_snapshot!(patch_value);

View File

@@ -87,8 +87,9 @@ impl RojoTree {
}
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
let builder = InstanceBuilder::new(snapshot.class_name.to_owned())
.with_name(snapshot.name.to_owned())
let builder = InstanceBuilder::empty()
.with_class(snapshot.class_name.into_owned())
.with_name(snapshot.name.into_owned())
.with_properties(snapshot.properties);
let referent = self.inner.insert(parent_ref, builder);

View File

@@ -10,6 +10,40 @@ pub fn snapshot_dir(
context: &InstanceContext,
vfs: &Vfs,
path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let mut snapshot = match snapshot_dir_no_meta(context, vfs, path)? {
Some(snapshot) => snapshot,
None => return Ok(None),
};
if let Some(mut meta) = dir_meta(vfs, path)? {
meta.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))
}
/// Retrieves the meta file that should be applied for this directory, if it
/// exists.
pub fn dir_meta(vfs: &Vfs, path: &Path) -> anyhow::Result<Option<DirectoryMetadata>> {
let meta_path = path.join("init.meta.json");
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
Ok(Some(metadata))
} else {
Ok(None)
}
}
/// Snapshot a directory without applying meta files; useful for if the
/// directory's ClassName will change before metadata should be applied. For
/// example, this can happen if the directory contains an `init.client.lua`
/// file.
pub fn snapshot_dir_no_meta(
context: &InstanceContext,
vfs: &Vfs,
path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let passes_filter_rules = |child: &DirEntry| {
context
@@ -48,11 +82,14 @@ pub fn snapshot_dir(
// middleware. Should we figure out a way for that function to add
// relevant paths to this middleware?
path.join("init.lua"),
path.join("init.luau"),
path.join("init.server.lua"),
path.join("init.server.luau"),
path.join("init.client.lua"),
path.join("init.client.luau"),
];
let mut snapshot = InstanceSnapshot::new()
let snapshot = InstanceSnapshot::new()
.name(instance_name)
.class_name("Folder")
.children(snapshot_children)
@@ -63,11 +100,6 @@ pub fn snapshot_dir(
.context(context),
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))
}

View File

@@ -26,12 +26,25 @@ pub fn snapshot_json_model(
return Ok(None);
}
let instance: JsonModel = serde_json::from_str(contents_str)
let mut instance: JsonModel = serde_json::from_str(contents_str)
.with_context(|| format!("File is not a valid JSON model: {}", path.display()))?;
if let Some(top_level_name) = &instance.name {
let new_name = format!("{}.model.json", top_level_name);
log::warn!(
"Model at path {} had a top-level Name field. \
This field has been ignored since Rojo 6.0.\n\
Consider removing this field and renaming the file to {}.",
new_name,
path.display()
);
}
instance.name = Some(name.to_owned());
let mut snapshot = instance
.core
.into_snapshot(name.to_owned())
.into_snapshot()
.with_context(|| format!("Could not load JSON model: {}", path.display()))?;
snapshot.metadata = snapshot
@@ -44,42 +57,37 @@ pub fn snapshot_json_model(
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
#[serde(rename_all = "camelCase")]
struct JsonModel {
#[serde(alias = "Name")]
name: Option<String>,
#[serde(flatten)]
core: JsonModelCore,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelInstance {
name: String,
#[serde(flatten)]
core: JsonModelCore,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelCore {
#[serde(alias = "ClassName")]
class_name: String,
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
children: Vec<JsonModelInstance>,
#[serde(
alias = "Children",
default = "Vec::new",
skip_serializing_if = "Vec::is_empty"
)]
children: Vec<JsonModel>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
#[serde(
alias = "Properties",
default = "HashMap::new",
skip_serializing_if = "HashMap::is_empty"
)]
properties: HashMap<String, UnresolvedValue>,
}
impl JsonModelCore {
fn into_snapshot(self, name: String) -> anyhow::Result<InstanceSnapshot> {
impl JsonModel {
fn into_snapshot(self) -> anyhow::Result<InstanceSnapshot> {
let name = self.name.unwrap_or_else(|| self.class_name.clone());
let class_name = self.class_name;
let mut children = Vec::with_capacity(self.children.len());
for child in self.children {
children.push(child.core.into_snapshot(child.name)?);
children.push(child.into_snapshot()?);
}
let mut properties = HashMap::with_capacity(self.properties.len());
@@ -113,7 +121,43 @@ mod test {
VfsSnapshot::file(
r#"
{
"Name": "children",
"className": "IntValue",
"properties": {
"Value": 5
},
"children": [
{
"name": "The Child",
"className": "StringValue"
}
]
}
"#,
),
)
.unwrap();
let vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_json_model(
&InstanceContext::default(),
&vfs,
Path::new("/foo.model.json"),
)
.unwrap()
.unwrap();
insta::assert_yaml_snapshot!(instance_snapshot);
}
#[test]
fn model_from_vfs_legacy() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.model.json",
VfsSnapshot::file(
r#"
{
"ClassName": "IntValue",
"Properties": {
"Value": 5
@@ -130,11 +174,11 @@ mod test {
)
.unwrap();
let mut vfs = Vfs::new(imfs);
let vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_json_model(
&InstanceContext::default(),
&mut vfs,
&vfs,
Path::new("/foo.model.json"),
)
.unwrap()

View File

@@ -6,7 +6,11 @@ use memofs::{IoResultExt, Vfs};
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::{dir::snapshot_dir, meta_file::AdjacentMetadata, util::match_trailing};
use super::{
dir::{dir_meta, snapshot_dir_no_meta},
meta_file::AdjacentMetadata,
util::match_trailing,
};
/// Core routine for turning Lua files into snapshots.
pub fn snapshot_lua(
@@ -23,6 +27,12 @@ pub fn snapshot_lua(
("LocalScript", name)
} else if let Some(name) = match_trailing(&file_name, ".lua") {
("ModuleScript", name)
} else if let Some(name) = match_trailing(&file_name, ".server.luau") {
("Script", name)
} else if let Some(name) = match_trailing(&file_name, ".client.luau") {
("LocalScript", name)
} else if let Some(name) = match_trailing(&file_name, ".luau") {
("ModuleScript", name)
} else {
return Ok(None);
};
@@ -66,7 +76,7 @@ pub fn snapshot_lua_init(
init_path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let folder_path = init_path.parent().unwrap();
let dir_snapshot = snapshot_dir(context, vfs, folder_path)?.unwrap();
let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap();
if dir_snapshot.class_name != "Folder" {
anyhow::bail!(
@@ -86,6 +96,10 @@ pub fn snapshot_lua_init(
init_snapshot.children = dir_snapshot.children;
init_snapshot.metadata = dir_snapshot.metadata;
if let Some(mut meta) = dir_meta(vfs, folder_path)? {
meta.apply_all(&mut init_snapshot)?;
}
Ok(Some(init_snapshot))
}

View File

@@ -40,6 +40,7 @@ pub use self::project::snapshot_project_node;
/// The main entrypoint to the snapshot function. This function can be pointed
/// at any path and will return something if Rojo knows how to deal with it.
#[profiling::function]
pub fn snapshot_from_vfs(
context: &InstanceContext,
vfs: &Vfs,
@@ -56,16 +57,31 @@ pub fn snapshot_from_vfs(
return snapshot_project(context, vfs, &project_path);
}
let init_path = path.join("init.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.server.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.server.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.client.luau");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
}
let init_path = path.join("init.client.lua");
if vfs.metadata(&init_path).with_not_found()?.is_some() {
return snapshot_lua_init(context, vfs, &init_path);
@@ -73,7 +89,11 @@ pub fn snapshot_from_vfs(
snapshot_dir(context, vfs, path)
} else {
if let Ok(name) = path.file_name_trim_end(".lua") {
let script_name = path
.file_name_trim_end(".lua")
.or_else(|_| path.file_name_trim_end(".luau"));
if let Ok(name) = script_name {
match name {
// init scripts are handled elsewhere and should not turn into
// their own children.

View File

@@ -7,6 +7,7 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::util::PathExt;
#[profiling::function]
pub fn snapshot_rbxm(
context: &InstanceContext,
vfs: &Vfs,
@@ -21,7 +22,8 @@ pub fn snapshot_rbxm(
let children = root_instance.children();
if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])
let child = children[0];
let snapshot = InstanceSnapshot::from_tree(temp_tree, child)
.name(name)
.metadata(
InstanceMetadata::new()

View File

@@ -24,7 +24,8 @@ pub fn snapshot_rbxmx(
let children = root_instance.children();
if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])
let child = children[0];
let snapshot = InstanceSnapshot::from_tree(temp_tree, child)
.name(name)
.metadata(
InstanceMetadata::new()

View File

@@ -11,10 +11,14 @@ metadata:
- /foo
- /foo/init.meta.json
- /foo/init.lua
- /foo/init.luau
- /foo/init.server.lua
- /foo/init.server.luau
- /foo/init.client.lua
- /foo/init.client.luau
context: {}
name: foo
class_name: Folder
properties: {}
children: []

View File

@@ -11,8 +11,11 @@ metadata:
- /foo
- /foo/init.meta.json
- /foo/init.lua
- /foo/init.luau
- /foo/init.server.lua
- /foo/init.server.luau
- /foo/init.client.lua
- /foo/init.client.luau
context: {}
name: foo
class_name: Folder
@@ -27,10 +30,14 @@ children:
- /foo/Child
- /foo/Child/init.meta.json
- /foo/Child/init.lua
- /foo/Child/init.luau
- /foo/Child/init.server.lua
- /foo/Child/init.server.luau
- /foo/Child/init.client.lua
- /foo/Child/init.client.luau
context: {}
name: Child
class_name: Folder
properties: {}
children: []

View File

@@ -0,0 +1,29 @@
---
source: src/snapshot_middleware/json_model.rs
assertion_line: 186
expression: instance_snapshot
---
snapshot_id: ~
metadata:
ignore_unknown_instances: false
instigating_source:
Path: /foo.model.json
relevant_paths:
- /foo.model.json
context: {}
name: foo
class_name: IntValue
properties:
Value:
Int64: 5
children:
- snapshot_id: ~
metadata:
ignore_unknown_instances: false
relevant_paths: []
context: {}
name: The Child
class_name: StringValue
properties: {}
children: []

View File

@@ -244,7 +244,7 @@ impl ApiService {
}
}
/// If this instance is represented by a script, try to find the correct .lua
/// If this instance is represented by a script, try to find the correct .lua or .luau
/// file to open to edit it.
fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
match instance.class_name() {
@@ -252,16 +252,17 @@ fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
_ => return None,
}
// Pick the first listed relevant path that has an extension of .lua that
// Pick the first listed relevant path that has an extension of .lua or .luau that
// exists.
instance
.metadata()
.relevant_paths
.iter()
.find(|path| {
// We should only ever open Lua files to be safe.
// We should only ever open Lua or Luau files to be safe.
match path.extension().and_then(|ext| ext.to_str()) {
Some("lua") => {}
Some("luau") => {}
_ => return false,
}

View File

@@ -0,0 +1,16 @@
{
"name": "attributes",
"tree": {
"$className": "DataModel",
"Workspace": {
"Folder": {
"$className": "Folder",
"$properties": {
"Attributes": {
"Hello": { "Vector3": [1, 2, 3] }
}
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "tags",
"tree": {
"$className": "DataModel",
"Workspace": {
"Folder": {
"$className": "Folder",
"$properties": {
"Tags": ["Hello", "World"]
}
}
}
}
}

View File

@@ -1,66 +0,0 @@
[[afterAll.args]]
type = "function"
[[afterEach.args]]
type = "function"
[[beforeAll.args]]
type = "function"
[[beforeEach.args]]
type = "function"
[[describe.args]]
type = "string"
[[describe.args]]
type = "function"
[[describeFOCUS.args]]
type = "string"
[[describeFOCUS.args]]
type = "function"
[[describeSKIP.args]]
type = "string"
[[describeSKIP.args]]
type = "function"
[[expect.args]]
type = "any"
[[FIXME.args]]
type = "string"
required = false
[FOCUS]
args = []
[[it.args]]
type = "string"
[[it.args]]
type = "function"
[[itFIXME.args]]
type = "string"
[[itFIXME.args]]
type = "function"
[[itFOCUS.args]]
type = "string"
[[itFOCUS.args]]
type = "function"
[[itSKIP.args]]
type = "string"
[[itSKIP.args]]
type = "function"
[SKIP]
args = []

53
testez.yml Normal file
View File

@@ -0,0 +1,53 @@
---
globals:
FIXME:
args:
- required: false
type: string
FOCUS:
args: []
SKIP:
args: []
afterAll:
args:
- type: function
afterEach:
args:
- type: function
beforeAll:
args:
- type: function
beforeEach:
args:
- type: function
describe:
args:
- type: string
- type: function
describeFOCUS:
args:
- type: string
- type: function
describeSKIP:
args:
- type: string
- type: function
expect:
args:
- type: any
it:
args:
- type: string
- type: function
itFIXME:
args:
- type: string
- type: function
itFOCUS:
args:
- type: string
- type: function
itSKIP:
args:
- type: string
- type: function

View File

@@ -21,6 +21,7 @@ macro_rules! gen_build_tests {
}
gen_build_tests! {
attributes,
client_in_folder,
client_init,
csv_bug_145,
@@ -36,11 +37,13 @@ gen_build_tests! {
init_meta_class_name,
init_meta_properties,
init_with_children,
issue_546,
json_as_lua,
json_model_in_folder,
json_model_legacy_name,
module_in_folder,
module_init,
optional,
project_composed_default,
project_composed_file,
project_root_name,
@@ -53,7 +56,6 @@ gen_build_tests! {
txt,
txt_in_folder,
unresolved_values,
optional,
weldconstraint,
}