mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 20:55:50 +00:00
Compare commits
1 Commits
v7.2.1
...
project-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b96a236333 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
rust_version: [stable, 1.57.0]
|
||||
rust_version: [stable, 1.55.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
# -x86_64 to each release.
|
||||
include:
|
||||
- host: linux
|
||||
os: ubuntu-18.04
|
||||
os: ubuntu-latest
|
||||
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
3
.gitignore
vendored
@@ -19,3 +19,6 @@
|
||||
|
||||
# Snapshot files from the 'insta' Rust crate
|
||||
**/*.snap.new
|
||||
|
||||
# Selene generates a roblox.toml file that should not be checked in.
|
||||
/roblox.toml
|
||||
58
.luacheckrc
Normal file
58
.luacheckrc
Normal file
@@ -0,0 +1,58 @@
|
||||
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",
|
||||
}
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,37 +1,7 @@
|
||||
# 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
555
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -1,7 +1,6 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.2.1"
|
||||
rust-version = "1.57.0"
|
||||
version = "7.1.1"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "Enables professional-grade development tools for Roblox developers"
|
||||
license = "MPL-2.0"
|
||||
@@ -9,7 +8,7 @@ homepage = "https://rojo.space"
|
||||
documentation = "https://rojo.space/docs"
|
||||
repository = "https://github.com/rojo-rbx/rojo"
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
|
||||
exclude = [
|
||||
@@ -28,8 +27,6 @@ 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/*"]
|
||||
|
||||
@@ -42,6 +39,7 @@ 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
|
||||
@@ -51,8 +49,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.5"
|
||||
rbx_dom_weak = "2.4.0"
|
||||
rbx_binary = "0.6.4"
|
||||
rbx_dom_weak = "2.3.0"
|
||||
rbx_reflection = "4.2.0"
|
||||
rbx_reflection_database = "0.2.2"
|
||||
rbx_xml = "0.12.3"
|
||||
@@ -83,8 +81,6 @@ 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"
|
||||
|
||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
Binary file not shown.
2
build.rs
2
build.rs
@@ -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") || file_name.ends_with(".spec.luau") {
|
||||
if file_name.ends_with(".spec.lua") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
16
crates/rojo-project/Cargo.toml
Normal file
16
crates/rojo-project/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[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"
|
||||
4
crates/rojo-project/README.md
Normal file
4
crates/rojo-project/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# rojo-project
|
||||
Project file format crate for [Rojo].
|
||||
|
||||
[Rojo]: https://rojo.space
|
||||
@@ -1,6 +1,3 @@
|
||||
//! 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};
|
||||
@@ -8,6 +5,8 @@ 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,
|
||||
7
crates/rojo-project/src/lib.rs
Normal file
7
crates/rojo-project/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod glob;
|
||||
mod path_serializer;
|
||||
mod project;
|
||||
mod resolution;
|
||||
|
||||
pub use project::{OptionalPathNode, PathNode, Project, ProjectNode};
|
||||
pub use resolution::{AmbiguousValue, UnresolvedValue};
|
||||
21
crates/rojo-project/src/path_serializer.rs
Normal file
21
crates/rojo-project/src/path_serializer.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! 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)
|
||||
}
|
||||
363
crates/rojo-project/src/project.rs
Normal file
363
crates/rojo-project/src/project.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
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"}"#);
|
||||
}
|
||||
}
|
||||
294
crates/rojo-project/src/resolution.rs
Normal file
294
crates/rojo-project/src/resolution.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.18.2" }
|
||||
selene = { source = "Kampfkarren/selene", version = "0.17.0" }
|
||||
|
||||
@@ -23,45 +23,8 @@ 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 = {}
|
||||
@@ -470,6 +433,8 @@ types = {
|
||||
},
|
||||
}
|
||||
|
||||
local EncodedValue = {}
|
||||
|
||||
function EncodedValue.decode(encodedValue)
|
||||
local ty, value = next(encodedValue)
|
||||
|
||||
@@ -494,19 +459,4 @@ 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
|
||||
|
||||
@@ -1,73 +1,4 @@
|
||||
{
|
||||
"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": [
|
||||
|
||||
@@ -5,26 +5,6 @@ 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
@@ -1,198 +0,0 @@
|
||||
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
|
||||
@@ -9,8 +9,6 @@ local Roact = require(Rojo.Roact)
|
||||
local defaultSettings = {
|
||||
openScriptsExternally = false,
|
||||
twoWaySync = false,
|
||||
showNotifications = true,
|
||||
playSounds = true,
|
||||
}
|
||||
|
||||
local Settings = {}
|
||||
@@ -120,4 +118,4 @@ end
|
||||
return {
|
||||
StudioProvider = StudioProvider,
|
||||
with = with,
|
||||
}
|
||||
}
|
||||
@@ -202,28 +202,12 @@ 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 = 4,
|
||||
layoutOrder = 2,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
@@ -243,4 +227,4 @@ function SettingsPage:render()
|
||||
end)
|
||||
end
|
||||
|
||||
return SettingsPage
|
||||
return SettingsPage
|
||||
@@ -103,10 +103,6 @@ local lightTheme = strict("LightTheme", {
|
||||
LogoColor = BRAND_COLOR,
|
||||
VersionColor = hexColor(0x727272),
|
||||
},
|
||||
Notification = {
|
||||
InfoColor = hexColor(0x00000),
|
||||
CloseColor = BRAND_COLOR,
|
||||
},
|
||||
ErrorColor = hexColor(0x000000),
|
||||
ScrollBarColor = hexColor(0x000000),
|
||||
})
|
||||
@@ -181,10 +177,6 @@ local darkTheme = strict("DarkTheme", {
|
||||
LogoColor = BRAND_COLOR,
|
||||
VersionColor = hexColor(0xD3D3D3)
|
||||
},
|
||||
Notification = {
|
||||
InfoColor = hexColor(0xFFFFFF),
|
||||
CloseColor = hexColor(0xFFFFFF),
|
||||
},
|
||||
ErrorColor = hexColor(0xFFFFFF),
|
||||
ScrollBarColor = hexColor(0xFFFFFF),
|
||||
})
|
||||
|
||||
@@ -12,12 +12,10 @@ 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)
|
||||
@@ -46,37 +44,10 @@ 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()
|
||||
@@ -110,7 +81,6 @@ 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({
|
||||
@@ -119,7 +89,8 @@ function App:startSession()
|
||||
address = address,
|
||||
toolbarIcon = Assets.Images.PluginButtonConnected,
|
||||
})
|
||||
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
|
||||
|
||||
Log.info("Connected to session '{}' at {}", details, address)
|
||||
elseif status == ServeSession.Status.Disconnected then
|
||||
self.serveSession = nil
|
||||
|
||||
@@ -133,13 +104,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,
|
||||
})
|
||||
self:addNotification("Disconnected from session.")
|
||||
|
||||
Log.info("Disconnected session")
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -265,22 +236,6 @@ 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",
|
||||
@@ -349,11 +304,10 @@ return function(props)
|
||||
plugin = props.plugin,
|
||||
}, {
|
||||
App = PluginSettings.with(function(settings)
|
||||
local mergedProps = Dictionary.merge(props, {
|
||||
local settingsProps = Dictionary.merge(props, {
|
||||
settings = settings,
|
||||
soundPlayer = soundPlayer.new(settings),
|
||||
})
|
||||
return e(App, mergedProps)
|
||||
return e(App, settingsProps)
|
||||
end),
|
||||
})
|
||||
end
|
||||
|
||||
@@ -45,9 +45,6 @@ local Assets = {
|
||||
[500] = "rbxassetid://2609138523"
|
||||
},
|
||||
},
|
||||
Sounds = {
|
||||
Notification = "rbxassetid://203785492",
|
||||
},
|
||||
StartSession = "",
|
||||
SessionActive = "",
|
||||
Configure = "",
|
||||
@@ -65,4 +62,4 @@ end
|
||||
|
||||
guardForTypos("Assets", Assets)
|
||||
|
||||
return Assets
|
||||
return Assets
|
||||
@@ -5,8 +5,8 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
|
||||
return strict("Config", {
|
||||
isDevBuild = isDevBuild,
|
||||
codename = "Epiphany",
|
||||
version = {7, 2, 1},
|
||||
expectedServerVersionString = "7.2 or newer",
|
||||
version = {7, 1, 1},
|
||||
expectedServerVersionString = "7.0 or newer",
|
||||
protocolVersion = 4,
|
||||
defaultHost = "localhost",
|
||||
defaultPort = 34872,
|
||||
|
||||
@@ -18,7 +18,7 @@ local App = require(script.App)
|
||||
local app = Roact.createElement(App, {
|
||||
plugin = plugin
|
||||
})
|
||||
local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI")
|
||||
local tree = Roact.mount(app, nil, "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
|
||||
@@ -1,35 +0,0 @@
|
||||
-- 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
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,13 +1,14 @@
|
||||
---
|
||||
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>
|
||||
@@ -15,7 +16,8 @@ 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>
|
||||
@@ -106,7 +108,8 @@ 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>
|
||||
@@ -133,7 +136,8 @@ 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>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "issue_546",
|
||||
"tree": {
|
||||
"$path": "hello"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
print("Hello, world!")
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"properties": {
|
||||
"Disabled": true
|
||||
}
|
||||
}
|
||||
@@ -291,7 +291,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
|
||||
}
|
||||
};
|
||||
|
||||
let patch_set = compute_patch_set(snapshot, &tree, id);
|
||||
let patch_set = compute_patch_set(snapshot.as_ref(), &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, &tree, id);
|
||||
let patch_set = compute_patch_set(snapshot.as_ref(), &tree, id);
|
||||
apply_patch_set(tree, patch_set)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
io::{BufWriter, Write},
|
||||
mem::forget,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -62,10 +61,6 @@ impl BuildCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid dropping ServeSession: it's potentially VERY expensive to drop
|
||||
// and we're about to exit anyways.
|
||||
forget(session);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -102,7 +97,6 @@ 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,
|
||||
|
||||
@@ -67,17 +67,15 @@ 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)?;
|
||||
writeln!(&mut buffer, "{}", address_string)?;
|
||||
if bind_address.is_loopback() {
|
||||
writeln!(&mut buffer, "localhost")?;
|
||||
} else {
|
||||
writeln!(&mut buffer, "{}", bind_address)?;
|
||||
}
|
||||
|
||||
buffer.set_color(&ColorSpec::new())?;
|
||||
write!(&mut buffer, " Port: ")?;
|
||||
@@ -90,7 +88,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://{}:{}/", address_string, port)?;
|
||||
write!(&mut buffer, "http://localhost:{}/", port)?;
|
||||
|
||||
buffer.set_color(&ColorSpec::new())?;
|
||||
writeln!(&mut buffer, " in your browser for more information.")?;
|
||||
|
||||
@@ -9,7 +9,6 @@ mod tree_view;
|
||||
|
||||
mod auth_cookie;
|
||||
mod change_processor;
|
||||
mod glob;
|
||||
mod lua_ast;
|
||||
mod message_queue;
|
||||
mod multimap;
|
||||
|
||||
@@ -6,9 +6,6 @@ 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
|
||||
|
||||
380
src/project.rs
380
src/project.rs
@@ -1,379 +1,3 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
fs, io,
|
||||
net::IpAddr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
pub use rojo_project::{OptionalPathNode, PathNode, Project, ProjectNode};
|
||||
|
||||
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"}"#);
|
||||
}
|
||||
}
|
||||
pub use anyhow::Error as ProjectError;
|
||||
|
||||
@@ -2,8 +2,7 @@ use std::borrow::Borrow;
|
||||
|
||||
use anyhow::format_err;
|
||||
use rbx_dom_weak::types::{
|
||||
Attributes, CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2,
|
||||
Vector3,
|
||||
CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
|
||||
};
|
||||
use rbx_reflection::{DataType, PropertyDescriptor};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -41,7 +40,6 @@ pub enum AmbiguousValue {
|
||||
Array3([f64; 3]),
|
||||
Array4([f64; 4]),
|
||||
Array12([f64; 12]),
|
||||
Attributes(Attributes),
|
||||
}
|
||||
|
||||
impl AmbiguousValue {
|
||||
@@ -130,8 +128,6 @@ 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,
|
||||
@@ -158,7 +154,6 @@ 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use thiserror::Error;
|
||||
use crate::{
|
||||
change_processor::ChangeProcessor,
|
||||
message_queue::MessageQueue,
|
||||
project::{Project, ProjectError},
|
||||
project::Project,
|
||||
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, &tree, root_id);
|
||||
let patch_set = compute_patch_set(snapshot.as_ref(), &tree, root_id);
|
||||
|
||||
log::trace!("Applying initial patch set");
|
||||
apply_patch_set(&mut tree, patch_set);
|
||||
@@ -237,12 +237,6 @@ pub enum ServeSessionError {
|
||||
source: io::Error,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
Project {
|
||||
#[from]
|
||||
source: ProjectError,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
Other {
|
||||
#[from]
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use rbx_dom_weak::{
|
||||
types::{Ref, Variant},
|
||||
Instance, WeakDom,
|
||||
WeakDom,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -102,29 +102,22 @@ impl InstanceSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
pub fn from_tree(tree: &WeakDom, id: Ref) -> Self {
|
||||
let instance = tree.get_by_ref(id).expect("instance did not exist in tree");
|
||||
|
||||
let children = instance
|
||||
.children()
|
||||
.iter()
|
||||
.map(|&id| Self::from_raw_tree(raw_tree, id))
|
||||
.copied()
|
||||
.map(|id| Self::from_tree(tree, id))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
snapshot_id: Some(id),
|
||||
metadata: InstanceMetadata::default(),
|
||||
name: Cow::Owned(instance.name),
|
||||
class_name: Cow::Owned(instance.class),
|
||||
properties: instance.properties,
|
||||
name: Cow::Owned(instance.name.clone()),
|
||||
class_name: Cow::Owned(instance.class.clone()),
|
||||
properties: instance.properties.clone(),
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use rojo_project::glob::Glob;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{glob::Glob, path_serializer, project::ProjectNode};
|
||||
use crate::{path_serializer, project::ProjectNode};
|
||||
|
||||
/// Rojo-specific metadata that can be associated with an instance or a snapshot
|
||||
/// of an instance.
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
//! Defines the algorithm for applying generated patches.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
mem::take,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rbx_dom_weak::types::{Ref, Variant};
|
||||
|
||||
@@ -15,31 +12,21 @@ 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();
|
||||
|
||||
{
|
||||
profiling::scope!("removals");
|
||||
for removed_id in patch_set.removed_instances {
|
||||
apply_remove_instance(&mut context, tree, removed_id);
|
||||
}
|
||||
for removed_id in patch_set.removed_instances {
|
||||
apply_remove_instance(&mut context, tree, removed_id);
|
||||
}
|
||||
|
||||
{
|
||||
profiling::scope!("additions");
|
||||
for add_patch in patch_set.added_instances {
|
||||
apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance);
|
||||
}
|
||||
for add_patch in patch_set.added_instances {
|
||||
apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance);
|
||||
}
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
// 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)
|
||||
@@ -68,9 +55,20 @@ struct PatchApplyContext {
|
||||
/// eachother.
|
||||
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
|
||||
|
||||
/// Tracks all of the instances added by this patch that have refs that need
|
||||
/// to be rewritten.
|
||||
has_refs_to_rewrite: HashSet<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>>,
|
||||
|
||||
/// The current applied patch result, describing changes made to the tree.
|
||||
applied_patch_set: AppliedPatchSet,
|
||||
@@ -86,22 +84,23 @@ 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 in context.has_refs_to_rewrite {
|
||||
for (id, properties) in context.added_instance_properties {
|
||||
// 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 value in instance.properties_mut().values_mut() {
|
||||
if let Variant::Ref(referent) = value {
|
||||
for (key, mut property_value) in properties {
|
||||
if let Variant::Ref(referent) = property_value {
|
||||
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent)
|
||||
{
|
||||
*value = Variant::Ref(instance_referent);
|
||||
property_value = Variant::Ref(instance_referent);
|
||||
}
|
||||
}
|
||||
|
||||
instance.properties_mut().insert(key, property_value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,24 +116,24 @@ fn apply_add_child(
|
||||
context: &mut PatchApplyContext,
|
||||
tree: &mut RojoTree,
|
||||
parent_id: Ref,
|
||||
mut snapshot: InstanceSnapshot,
|
||||
snapshot: InstanceSnapshot,
|
||||
) {
|
||||
let snapshot_id = snapshot.snapshot_id;
|
||||
let children = take(&mut snapshot.children);
|
||||
let properties = snapshot.properties;
|
||||
let children = snapshot.children;
|
||||
|
||||
// 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,
|
||||
});
|
||||
// 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);
|
||||
|
||||
let id = tree.insert_instance(parent_id, snapshot);
|
||||
let id = tree.insert_instance(parent_id, remaining_snapshot);
|
||||
context.applied_patch_set.added.push(id);
|
||||
|
||||
if has_refs {
|
||||
context.has_refs_to_rewrite.insert(id);
|
||||
}
|
||||
context.added_instance_properties.insert(id, properties);
|
||||
|
||||
if let Some(snapshot_id) = snapshot_id {
|
||||
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
//! Defines the algorithm for computing a roughly-minimal patch set given an
|
||||
//! existing instance tree and an instance snapshot.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
mem::take,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use rbx_dom_weak::types::{Ref, Variant};
|
||||
|
||||
@@ -13,8 +10,11 @@ use super::{
|
||||
InstanceSnapshot, InstanceWithMeta, RojoTree,
|
||||
};
|
||||
|
||||
#[profiling::function]
|
||||
pub fn compute_patch_set(snapshot: Option<InstanceSnapshot>, tree: &RojoTree, id: Ref) -> PatchSet {
|
||||
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,
|
||||
mut snapshot: InstanceSnapshot,
|
||||
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(&mut snapshot, &instance, patch_set);
|
||||
compute_children_patches(context, &mut snapshot, tree, id, patch_set);
|
||||
compute_property_patches(snapshot, &instance, patch_set);
|
||||
compute_children_patches(context, snapshot, tree, id, patch_set);
|
||||
}
|
||||
|
||||
fn compute_property_patches(
|
||||
snapshot: &mut InstanceSnapshot,
|
||||
snapshot: &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(take(&mut snapshot.name).into_owned())
|
||||
Some(snapshot.name.clone().into_owned())
|
||||
};
|
||||
|
||||
let changed_class_name = if snapshot.class_name == instance.class_name() {
|
||||
None
|
||||
} else {
|
||||
Some(take(&mut snapshot.class_name).into_owned())
|
||||
Some(snapshot.class_name.clone().into_owned())
|
||||
};
|
||||
|
||||
let changed_metadata = if &snapshot.metadata == instance.metadata() {
|
||||
None
|
||||
} else {
|
||||
Some(take(&mut snapshot.metadata))
|
||||
Some(snapshot.metadata.clone())
|
||||
};
|
||||
|
||||
for (name, snapshot_value) in take(&mut snapshot.properties) {
|
||||
visited_properties.insert(name.clone());
|
||||
for (name, snapshot_value) in &snapshot.properties {
|
||||
visited_properties.insert(name.as_str());
|
||||
|
||||
match instance.properties().get(&name) {
|
||||
match instance.properties().get(name) {
|
||||
Some(instance_value) => {
|
||||
if &snapshot_value != instance_value {
|
||||
changed_properties.insert(name, Some(snapshot_value));
|
||||
if snapshot_value != instance_value {
|
||||
changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
changed_properties.insert(name, Some(snapshot_value));
|
||||
changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ fn compute_property_patches(
|
||||
|
||||
fn compute_children_patches(
|
||||
context: &mut ComputePatchContext,
|
||||
snapshot: &mut InstanceSnapshot,
|
||||
snapshot: &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 take(&mut snapshot.children) {
|
||||
for snapshot_child in snapshot.children.iter() {
|
||||
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,
|
||||
instance: snapshot_child.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -87,9 +87,8 @@ impl RojoTree {
|
||||
}
|
||||
|
||||
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
|
||||
let builder = InstanceBuilder::empty()
|
||||
.with_class(snapshot.class_name.into_owned())
|
||||
.with_name(snapshot.name.into_owned())
|
||||
let builder = InstanceBuilder::new(snapshot.class_name.to_owned())
|
||||
.with_name(snapshot.name.to_owned())
|
||||
.with_properties(snapshot.properties);
|
||||
|
||||
let referent = self.inner.insert(parent_ref, builder);
|
||||
|
||||
@@ -10,40 +10,6 @@ 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
|
||||
@@ -82,14 +48,11 @@ pub fn snapshot_dir_no_meta(
|
||||
// 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 snapshot = InstanceSnapshot::new()
|
||||
let mut snapshot = InstanceSnapshot::new()
|
||||
.name(instance_name)
|
||||
.class_name("Folder")
|
||||
.children(snapshot_children)
|
||||
@@ -100,6 +63,11 @@ pub fn snapshot_dir_no_meta(
|
||||
.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))
|
||||
}
|
||||
|
||||
|
||||
@@ -26,25 +26,12 @@ pub fn snapshot_json_model(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut instance: JsonModel = serde_json::from_str(contents_str)
|
||||
let 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
|
||||
.into_snapshot()
|
||||
.core
|
||||
.into_snapshot(name.to_owned())
|
||||
.with_context(|| format!("Could not load JSON model: {}", path.display()))?;
|
||||
|
||||
snapshot.metadata = snapshot
|
||||
@@ -57,37 +44,42 @@ pub fn snapshot_json_model(
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct JsonModel {
|
||||
#[serde(alias = "Name")]
|
||||
name: Option<String>,
|
||||
|
||||
#[serde(alias = "ClassName")]
|
||||
#[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 {
|
||||
class_name: String,
|
||||
|
||||
#[serde(
|
||||
alias = "Children",
|
||||
default = "Vec::new",
|
||||
skip_serializing_if = "Vec::is_empty"
|
||||
)]
|
||||
children: Vec<JsonModel>,
|
||||
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
|
||||
children: Vec<JsonModelInstance>,
|
||||
|
||||
#[serde(
|
||||
alias = "Properties",
|
||||
default = "HashMap::new",
|
||||
skip_serializing_if = "HashMap::is_empty"
|
||||
)]
|
||||
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
|
||||
properties: HashMap<String, UnresolvedValue>,
|
||||
}
|
||||
|
||||
impl JsonModel {
|
||||
fn into_snapshot(self) -> anyhow::Result<InstanceSnapshot> {
|
||||
let name = self.name.unwrap_or_else(|| self.class_name.clone());
|
||||
impl JsonModelCore {
|
||||
fn into_snapshot(self, name: String) -> anyhow::Result<InstanceSnapshot> {
|
||||
let class_name = self.class_name;
|
||||
|
||||
let mut children = Vec::with_capacity(self.children.len());
|
||||
for child in self.children {
|
||||
children.push(child.into_snapshot()?);
|
||||
children.push(child.core.into_snapshot(child.name)?);
|
||||
}
|
||||
|
||||
let mut properties = HashMap::with_capacity(self.properties.len());
|
||||
@@ -121,43 +113,7 @@ mod test {
|
||||
VfsSnapshot::file(
|
||||
r#"
|
||||
{
|
||||
"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#"
|
||||
{
|
||||
"Name": "children",
|
||||
"ClassName": "IntValue",
|
||||
"Properties": {
|
||||
"Value": 5
|
||||
@@ -174,11 +130,11 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let instance_snapshot = snapshot_json_model(
|
||||
&InstanceContext::default(),
|
||||
&vfs,
|
||||
&mut vfs,
|
||||
Path::new("/foo.model.json"),
|
||||
)
|
||||
.unwrap()
|
||||
|
||||
@@ -6,11 +6,7 @@ use memofs::{IoResultExt, Vfs};
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{
|
||||
dir::{dir_meta, snapshot_dir_no_meta},
|
||||
meta_file::AdjacentMetadata,
|
||||
util::match_trailing,
|
||||
};
|
||||
use super::{dir::snapshot_dir, meta_file::AdjacentMetadata, util::match_trailing};
|
||||
|
||||
/// Core routine for turning Lua files into snapshots.
|
||||
pub fn snapshot_lua(
|
||||
@@ -27,12 +23,6 @@ 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);
|
||||
};
|
||||
@@ -76,7 +66,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_no_meta(context, vfs, folder_path)?.unwrap();
|
||||
let dir_snapshot = snapshot_dir(context, vfs, folder_path)?.unwrap();
|
||||
|
||||
if dir_snapshot.class_name != "Folder" {
|
||||
anyhow::bail!(
|
||||
@@ -96,10 +86,6 @@ 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))
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ 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,
|
||||
@@ -57,31 +56,16 @@ 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);
|
||||
@@ -89,11 +73,7 @@ pub fn snapshot_from_vfs(
|
||||
|
||||
snapshot_dir(context, vfs, path)
|
||||
} else {
|
||||
let script_name = path
|
||||
.file_name_trim_end(".lua")
|
||||
.or_else(|_| path.file_name_trim_end(".luau"));
|
||||
|
||||
if let Ok(name) = script_name {
|
||||
if let Ok(name) = path.file_name_trim_end(".lua") {
|
||||
match name {
|
||||
// init scripts are handled elsewhere and should not turn into
|
||||
// their own children.
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::util::PathExt;
|
||||
|
||||
#[profiling::function]
|
||||
pub fn snapshot_rbxm(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
@@ -22,8 +21,7 @@ pub fn snapshot_rbxm(
|
||||
let children = root_instance.children();
|
||||
|
||||
if children.len() == 1 {
|
||||
let child = children[0];
|
||||
let snapshot = InstanceSnapshot::from_tree(temp_tree, child)
|
||||
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])
|
||||
.name(name)
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
|
||||
@@ -24,8 +24,7 @@ pub fn snapshot_rbxmx(
|
||||
let children = root_instance.children();
|
||||
|
||||
if children.len() == 1 {
|
||||
let child = children[0];
|
||||
let snapshot = InstanceSnapshot::from_tree(temp_tree, child)
|
||||
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])
|
||||
.name(name)
|
||||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
|
||||
@@ -11,14 +11,10 @@ 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: []
|
||||
|
||||
|
||||
@@ -11,11 +11,8 @@ 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
|
||||
@@ -30,14 +27,10 @@ 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: []
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
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: []
|
||||
|
||||
@@ -244,7 +244,7 @@ impl ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/// If this instance is represented by a script, try to find the correct .lua or .luau
|
||||
/// If this instance is represented by a script, try to find the correct .lua
|
||||
/// file to open to edit it.
|
||||
fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
|
||||
match instance.class_name() {
|
||||
@@ -252,17 +252,16 @@ fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
// Pick the first listed relevant path that has an extension of .lua or .luau that
|
||||
// Pick the first listed relevant path that has an extension of .lua that
|
||||
// exists.
|
||||
instance
|
||||
.metadata()
|
||||
.relevant_paths
|
||||
.iter()
|
||||
.find(|path| {
|
||||
// We should only ever open Lua or Luau files to be safe.
|
||||
// We should only ever open Lua files to be safe.
|
||||
match path.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("lua") => {}
|
||||
Some("luau") => {}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "attributes",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"Workspace": {
|
||||
"Folder": {
|
||||
"$className": "Folder",
|
||||
"$properties": {
|
||||
"Attributes": {
|
||||
"Hello": { "Vector3": [1, 2, 3] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "tags",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"Workspace": {
|
||||
"Folder": {
|
||||
"$className": "Folder",
|
||||
"$properties": {
|
||||
"Tags": ["Hello", "World"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
testez.toml
Normal file
66
testez.toml
Normal file
@@ -0,0 +1,66 @@
|
||||
[[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
53
testez.yml
@@ -1,53 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -21,7 +21,6 @@ macro_rules! gen_build_tests {
|
||||
}
|
||||
|
||||
gen_build_tests! {
|
||||
attributes,
|
||||
client_in_folder,
|
||||
client_init,
|
||||
csv_bug_145,
|
||||
@@ -37,13 +36,11 @@ 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,
|
||||
@@ -56,6 +53,7 @@ gen_build_tests! {
|
||||
txt,
|
||||
txt_in_folder,
|
||||
unresolved_values,
|
||||
optional,
|
||||
weldconstraint,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user