Compare commits

..

8 Commits

Author SHA1 Message Date
Kenneth Loeffler
af9629c53f Release 7.4.1 (#872) 2024-02-20 17:41:45 -08:00
Micah
9509909f46 Backport #870 (optional project names) to 7.4.x (#871)
Unlike most of the other backports, this code couldn't be directly
translated so it had to be re-implemented. Luckily, it is very simple.
This implementation is a bit messy and heavy handed with potential
panics, but I think it's probably fine since file names that aren't
UTF-8 aren't really supported anyway. The original implementation is a
lot cleaner though.

The test snapshots are (almost) all identical between the 7.5
implementation and this one. The sole exception is with the path in the
`snapshot_middleware::project` test, since I didn't feel like adding a
`name` parameter to `snapshot_project` in this implementation.
2024-02-20 17:25:05 -08:00
Kenneth Loeffler
88efbd433f Backport #868 to 7.4 (custom pivot geter/setter) (#869)
This PR backports some changes to rbx_dom_lua to fix serving model
pivots
2024-02-20 12:22:27 -08:00
Kenneth Loeffler
f716928683 Add entry for model pivot build fix to 7.4.x changelog (#867) 2024-02-20 12:09:13 -08:00
Kenneth Loeffler
e23d024ba3 Insert Model.NeedsPivotMigration in insert_instance when missing (#865) 2024-02-20 09:11:26 -08:00
Kenneth Loeffler
591419611e Backport #854 to Rojo 7.4 (Lua LF normalization) (#857) 2024-02-14 10:18:46 -08:00
Kenneth Loeffler
f68beab1df Backport #847 to 7.4 (gracefully handle gateway timeouts) (#851)
This PR adds a fix for gateway timeout handling to the 7.4.x branch

Co-authored-by: boatbomber <zack@boatbomber.com>
2024-02-03 20:30:10 -08:00
Kenneth Loeffler
2798610afd Backport #848, #846, #845, #844 to 7.4 (#849)
Co-authored-by: boatbomber <zack@boatbomber.com>
2024-02-01 13:23:51 -08:00
64 changed files with 1247 additions and 277 deletions

View File

@@ -72,7 +72,7 @@ jobs:
run: cargo build --locked --verbose
lint:
name: Rustfmt, Clippy, & Stylua
name: Rustfmt, Clippy, Stylua, & Selene
runs-on: ubuntu-latest
steps:
@@ -98,6 +98,9 @@ jobs:
- name: Stylua
run: stylua --check plugin/src
- name: Selene
run: selene plugin/src
- name: Rustfmt
run: cargo fmt -- --check

View File

@@ -2,6 +2,28 @@
## Unreleased Changes
## [7.4.1] - February 20, 2024
* Made the `name` field optional on project files ([#870])
Files named `default.project.json` inherit the name of the folder they're in and all other projects
are named as expect (e.g. `foo.project.json` becomes an Instance named `foo`)
There is no change in behavior if `name` is set.
* Fixed incorrect results when building model pivots ([#865])
* Fixed incorrect results when serving model pivots ([#868])
* Rojo now converts any line endings to LF, preventing spurious diffs when syncing Lua files on Windows ([#854])
* Fixed Rojo plugin failing to connect when project contains certain unreadable properties ([#848])
* Fixed various cases where patch visualizer would not display sync failures ([#845], [#844])
* Fixed http error handling so Rojo can be used in Github Codespaces ([#847])
[#848]: https://github.com/rojo-rbx/rojo/pull/848
[#845]: https://github.com/rojo-rbx/rojo/pull/845
[#844]: https://github.com/rojo-rbx/rojo/pull/844
[#847]: https://github.com/rojo-rbx/rojo/pull/847
[#854]: https://github.com/rojo-rbx/rojo/pull/854
[#865]: https://github.com/rojo-rbx/rojo/pull/865
[#868]: https://github.com/rojo-rbx/rojo/pull/868
[#870]: https://github.com/rojo-rbx/rojo/pull/870
## [7.4.0] - January 16, 2024
* Improved the visualization for array properties like Tags ([#829])
* Significantly improved performance of `rojo serve`, `rojo build --watch`, and `rojo sourcemap --watch` on macOS. ([#830])

2
Cargo.lock generated
View File

@@ -1831,7 +1831,7 @@ dependencies = [
[[package]]
name = "rojo"
version = "7.4.0"
version = "7.4.1"
dependencies = [
"anyhow",
"backtrace",

View File

@@ -1,6 +1,6 @@
[package]
name = "rojo"
version = "7.4.0"
version = "7.4.1"
rust-version = "1.70.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers"

View File

@@ -1,5 +1,5 @@
[tools]
rojo = "rojo-rbx/rojo@7.3.0"
selene = "Kampfkarren/selene@0.25.0"
selene = "Kampfkarren/selene@0.26.1"
stylua = "JohnnyMorganz/stylua@0.18.2"
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"

View File

@@ -2,8 +2,10 @@
## Unreleased Changes
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
[#830]: https://github.com/rojo-rbx/rojo/pull/830
[#854]: https://github.com/rojo-rbx/rojo/pull/854
## 0.2.0 (2021-08-23)
* Updated to `crossbeam-channel` 0.5.1.

View File

@@ -22,9 +22,9 @@ mod noop_backend;
mod snapshot;
mod std_backend;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard};
use std::{io, str};
pub use in_memory_fs::InMemoryFs;
pub use noop_backend::NoopBackend;
@@ -155,6 +155,24 @@ impl VfsInner {
Ok(Arc::new(contents))
}
fn read_to_string<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
let contents = self.backend.read(path)?;
if self.watch_enabled {
self.backend.watch(path)?;
}
let contents_str = str::from_utf8(&contents).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("File was not valid UTF-8: {}", path.display()),
)
})?;
Ok(Arc::new(contents_str.into()))
}
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
let path = path.as_ref();
let contents = contents.as_ref();
@@ -258,6 +276,33 @@ impl Vfs {
self.inner.lock().unwrap().read(path)
}
/// Read a file from the VFS (or from the underlying backend if it isn't
/// resident) into a string.
///
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string].
///
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
#[inline]
pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
self.inner.lock().unwrap().read_to_string(path)
}
/// Read a file from the VFS (or the underlying backend if it isn't
/// resident) into a string, and normalize its line endings to LF.
///
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string], but also performs
/// line ending normalization.
///
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
#[inline]
pub fn read_to_string_lf_normalized<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
let path = path.as_ref();
let contents = self.inner.lock().unwrap().read_to_string(path)?;
Ok(contents.lines().collect::<Vec<&str>>().join("\n").into())
}
/// Write a file to the VFS and the underlying backend.
///
/// Roughly equivalent to [`std::fs::write`][std::fs::write].

View File

@@ -1 +1 @@
7.4.0
7.4.1

View File

@@ -3,12 +3,12 @@ Error.__index = Error
Error.Kind = {
HttpNotEnabled = {
message = "Rojo requires HTTP access, which is not enabled.\n" ..
"Check your game settings, located in the 'Home' tab of Studio.",
message = "Rojo requires HTTP access, which is not enabled.\n"
.. "Check your game settings, located in the 'Home' tab of Studio.",
},
ConnectFailed = {
message = "Couldn't connect to the Rojo server.\n" ..
"Make sure the server is running — use 'rojo serve' to run it!",
message = "Couldn't connect to the Rojo server.\n"
.. "Make sure the server is running — use 'rojo serve' to run it!",
},
Timeout = {
message = "HTTP request timed out.",
@@ -63,4 +63,13 @@ function Error.fromRobloxErrorString(message)
return Error.new(Error.Kind.Unknown, message)
end
function Error.fromResponse(response)
local lower = (response.body or ""):lower()
if response.code == 408 or response.code == 504 or lower:find("timed? ?out") then
return Error.new(Error.Kind.Timeout)
end
return Error.new(Error.Kind.Unknown, string.format("%s: %s", tostring(response.code), tostring(response.body)))
end
return Error

View File

@@ -30,8 +30,13 @@ local function performRequest(requestParams)
end)
if success then
Log.trace("Request {} success, status code {}", requestId, response.StatusCode)
resolve(HttpResponse.fromRobloxResponse(response))
Log.trace("Request {} success, response {:#?}", requestId, response)
local httpResponse = HttpResponse.fromRobloxResponse(response)
if httpResponse:isSuccess() then
resolve(httpResponse)
else
reject(HttpError.fromResponse(httpResponse))
end
else
Log.trace("Request {} failure: {:?}", requestId, response)
reject(HttpError.fromRobloxErrorString(response))
@@ -63,4 +68,4 @@ function Http.jsonDecode(source)
return HttpService:JSONDecode(source)
end
return Http
return Http

View File

@@ -493,9 +493,32 @@ types = {
},
}
types.OptionalCFrame = {
fromPod = function(pod)
if pod == nil then
return nil
else
return types.CFrame.fromPod(pod)
end
end,
toPod = function(roblox)
if roblox == nil then
return nil
else
return types.CFrame.toPod(roblox)
end
end,
}
function EncodedValue.decode(encodedValue)
local ty, value = next(encodedValue)
if ty == nil then
-- If the encoded pair is empty, assume it is an unoccupied optional value
return true, nil
end
local typeImpl = types[ty]
if typeImpl == nil then
return false, "Couldn't decode value " .. tostring(ty)

View File

@@ -370,6 +370,41 @@
},
"ty": "NumberSequence"
},
"OptionalCFrame-None": {
"value": {
"OptionalCFrame": null
},
"ty": "OptionalCFrame"
},
"OptionalCFrame-Some": {
"value": {
"OptionalCFrame": {
"position": [
0.0,
0.0,
0.0
],
"orientation": [
[
1.0,
0.0,
0.0
],
[
0.0,
1.0,
0.0
],
[
0.0,
0.0,
1.0
]
]
}
},
"ty": "OptionalCFrame"
},
"PhysicalProperties-Custom": {
"value": {
"PhysicalProperties": {

View File

@@ -111,6 +111,18 @@ return {
return true, instance:ScaleTo(value)
end,
},
WorldPivotData = {
read = function(instance)
return true, instance:GetPivot()
end,
write = function(instance, _, value)
if value == nil then
return true, nil
else
return true, instance:PivotTo(value)
end
end,
},
},
Terrain = {
MaterialColors = {

View File

@@ -1,9 +1,9 @@
{
"Version": [
0,
607,
612,
0,
6070550
6120532
],
"Classes": {
"Accessory": {
@@ -345,6 +345,17 @@
}
}
},
"ActivityHistoryService": {
"Name": "ActivityHistoryService",
"Tags": [
"NotCreatable",
"NotReplicated",
"Service"
],
"Superclass": "Instance",
"Properties": {},
"DefaultProperties": {}
},
"Actor": {
"Name": "Actor",
"Tags": [],
@@ -11172,10 +11183,12 @@
"DataType": {
"Value": "CFrame"
},
"Tags": [],
"Tags": [
"Hidden"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
"Serialization": "DoesNotSerialize"
}
}
},
@@ -11185,10 +11198,12 @@
"DataType": {
"Value": "Int32"
},
"Tags": [],
"Tags": [
"Hidden"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
"Serialization": "DoesNotSerialize"
}
}
},
@@ -11198,10 +11213,12 @@
"DataType": {
"Value": "String"
},
"Tags": [],
"Tags": [
"Hidden"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
"Serialization": "DoesNotSerialize"
}
}
},
@@ -11211,22 +11228,38 @@
"DataType": {
"Value": "Int32"
},
"Tags": [],
"Tags": [
"Hidden"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
"Serialization": "DoesNotSerialize"
}
}
},
"IsIdle": {
"Name": "IsIdle",
"Scriptability": "ReadWrite",
"DataType": {
"Value": "Bool"
},
"Tags": [
"Hidden"
],
"Kind": {
"Canonical": {
"Serialization": "DoesNotSerialize"
}
}
},
"Status": {
"Name": "Status",
"Scriptability": "None",
"Scriptability": "ReadWrite",
"DataType": {
"Enum": "CollaboratorStatus"
},
"Tags": [
"Hidden",
"NotScriptable"
"Hidden"
],
"Kind": {
"Canonical": {
@@ -11240,10 +11273,12 @@
"DataType": {
"Value": "Int64"
},
"Tags": [],
"Tags": [
"Hidden"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
"Serialization": "DoesNotSerialize"
}
}
},
@@ -11253,10 +11288,12 @@
"DataType": {
"Value": "String"
},
"Tags": [],
"Tags": [
"Hidden"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
"Serialization": "DoesNotSerialize"
}
}
}
@@ -12224,6 +12261,19 @@
"Serialization": "Serializes"
}
}
},
"UpDirection": {
"Name": "UpDirection",
"Scriptability": "ReadWrite",
"DataType": {
"Value": "Vector3"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
}
},
"DefaultProperties": {
@@ -12261,6 +12311,13 @@
},
"Tags": {
"Tags": []
},
"UpDirection": {
"Vector3": [
0.0,
1.0,
0.0
]
}
}
},
@@ -13286,7 +13343,9 @@
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Tags": [
"Deprecated"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
@@ -15878,6 +15937,19 @@
}
}
},
"PermissionPolicy": {
"Name": "PermissionPolicy",
"Scriptability": "ReadWrite",
"DataType": {
"Enum": "DragDetectorPermissionPolicy"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"PhysicalDragClickedPart": {
"Name": "PhysicalDragClickedPart",
"Scriptability": "None",
@@ -16168,10 +16240,13 @@
"Orientation": {
"Vector3": [
-0.0,
180.0,
179.99998,
90.0
]
},
"PermissionPolicy": {
"Enum": 1
},
"ResponseStyle": {
"Enum": 1
},
@@ -22285,7 +22360,9 @@
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Tags": [
"Deprecated"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
@@ -31232,7 +31309,7 @@
},
"WorldPivotData": {
"Name": "WorldPivotData",
"Scriptability": "None",
"Scriptability": "Custom",
"DataType": {
"Value": "OptionalCFrame"
},
@@ -33071,7 +33148,7 @@
},
"SerializedDefaultAttributes": {
"Name": "SerializedDefaultAttributes",
"Scriptability": "Read",
"Scriptability": "None",
"DataType": {
"Value": "BinaryString"
},
@@ -35022,6 +35099,19 @@
"Serialization": "Serializes"
}
}
},
"ZIndex": {
"Name": "ZIndex",
"Scriptability": "None",
"DataType": {
"Value": "Int32"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
}
},
"DefaultProperties": {}
@@ -35734,6 +35824,58 @@
}
}
},
"DrawConstraintsNetForce": {
"Name": "DrawConstraintsNetForce",
"Scriptability": "None",
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DrawContactsNetForce": {
"Name": "DrawContactsNetForce",
"Scriptability": "None",
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DrawTotalNetForce": {
"Name": "DrawTotalNetForce",
"Scriptability": "None",
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"EnableForceVisualizationSmoothing": {
"Name": "EnableForceVisualizationSmoothing",
"Scriptability": "None",
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"FluidForceDrawScale": {
"Name": "FluidForceDrawScale",
"Scriptability": "None",
@@ -35776,6 +35918,19 @@
}
}
},
"ForceVisualizationSmoothingSteps": {
"Name": "ForceVisualizationSmoothingSteps",
"Scriptability": "None",
"DataType": {
"Value": "Int32"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"IsInterpolationThrottleShown": {
"Name": "IsInterpolationThrottleShown",
"Scriptability": "ReadWrite",
@@ -35893,6 +36048,19 @@
}
}
},
"TorqueDrawScale": {
"Name": "TorqueDrawScale",
"Scriptability": "None",
"DataType": {
"Value": "Float32"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"UseCSGv2": {
"Name": "UseCSGv2",
"Scriptability": "ReadWrite",
@@ -38337,6 +38505,17 @@
}
}
},
"ProjectFolderService": {
"Name": "ProjectFolderService",
"Tags": [
"NotCreatable",
"NotReplicated",
"Service"
],
"Superclass": "Instance",
"Properties": {},
"DefaultProperties": {}
},
"ProximityPrompt": {
"Name": "ProximityPrompt",
"Tags": [],
@@ -40859,6 +41038,19 @@
}
}
},
"PreferredUploadId": {
"Name": "PreferredUploadId",
"Scriptability": "ReadWrite",
"DataType": {
"Value": "Int64"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"RestPose": {
"Name": "RestPose",
"Scriptability": "ReadWrite",
@@ -44505,7 +44697,9 @@
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Tags": [
"Deprecated"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
@@ -46931,21 +47125,6 @@
}
}
},
"AnimationCompositorMode": {
"Name": "AnimationCompositorMode",
"Scriptability": "ReadWrite",
"DataType": {
"Enum": "AnimationCompositorMode"
},
"Tags": [
"NotBrowsable"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"AutoJumpEnabled": {
"Name": "AutoJumpEnabled",
"Scriptability": "ReadWrite",
@@ -47488,21 +47667,6 @@
}
}
},
"HumanoidStateMachineMode": {
"Name": "HumanoidStateMachineMode",
"Scriptability": "ReadWrite",
"DataType": {
"Enum": "HumanoidStateMachineMode"
},
"Tags": [
"NotBrowsable"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"LoadCharacterAppearance": {
"Name": "LoadCharacterAppearance",
"Scriptability": "ReadWrite",
@@ -47594,9 +47758,6 @@
"AllowCustomAnimations": {
"Bool": true
},
"AnimationCompositorMode": {
"Enum": 0
},
"Attributes": {
"Attributes": {}
},
@@ -47729,9 +47890,6 @@
"HealthDisplayDistance": {
"Float32": 100.0
},
"HumanoidStateMachineMode": {
"Enum": 0
},
"LoadCharacterAppearance": {
"Bool": true
},
@@ -48619,6 +48777,136 @@
}
}
},
"DraggerActiveColor": {
"Name": "DraggerActiveColor",
"Scriptability": "None",
"DataType": {
"Value": "Color3"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerMajorGridIncrement": {
"Name": "DraggerMajorGridIncrement",
"Scriptability": "None",
"DataType": {
"Value": "Int32"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerMaxSoftSnaps": {
"Name": "DraggerMaxSoftSnaps",
"Scriptability": "None",
"DataType": {
"Value": "Int32"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerPassiveColor": {
"Name": "DraggerPassiveColor",
"Scriptability": "None",
"DataType": {
"Value": "Color3"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerShowHoverRuler": {
"Name": "DraggerShowHoverRuler",
"Scriptability": "None",
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerShowMeasurement": {
"Name": "DraggerShowMeasurement",
"Scriptability": "None",
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerShowTargetSnap": {
"Name": "DraggerShowTargetSnap",
"Scriptability": "None",
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerSoftSnapMarginFactor": {
"Name": "DraggerSoftSnapMarginFactor",
"Scriptability": "None",
"DataType": {
"Value": "Float32"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerSummonMarginFactor": {
"Name": "DraggerSummonMarginFactor",
"Scriptability": "None",
"DataType": {
"Value": "Float32"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"DraggerTiltRotateDuration": {
"Name": "DraggerTiltRotateDuration",
"Scriptability": "None",
"DataType": {
"Value": "Float32"
},
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"Enable Autocomplete": {
"Name": "Enable Autocomplete",
"Scriptability": "ReadWrite",
@@ -55907,6 +56195,17 @@
"Properties": {},
"DefaultProperties": {}
},
"TextureGenerationService": {
"Name": "TextureGenerationService",
"Tags": [
"NotCreatable",
"NotReplicated",
"Service"
],
"Superclass": "Instance",
"Properties": {},
"DefaultProperties": {}
},
"ThirdPartyUserService": {
"Name": "ThirdPartyUserService",
"Tags": [
@@ -59590,6 +59889,22 @@
}
}
},
"ChatTranslationLocale": {
"Name": "ChatTranslationLocale",
"Scriptability": "None",
"DataType": {
"Value": "String"
},
"Tags": [
"Hidden",
"NotReplicated"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"ChatTranslationToggleEnabled": {
"Name": "ChatTranslationToggleEnabled",
"Scriptability": "None",
@@ -59724,6 +60039,22 @@
}
}
},
"FramerateCap": {
"Name": "FramerateCap",
"Scriptability": "None",
"DataType": {
"Value": "Int32"
},
"Tags": [
"Hidden",
"NotReplicated"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
"Fullscreen": {
"Name": "Fullscreen",
"Scriptability": "None",
@@ -61068,6 +61399,7 @@
"VRService": {
"Name": "VRService",
"Tags": [
"NotBrowsable",
"NotCreatable",
"Service"
],
@@ -61084,7 +61416,22 @@
],
"Kind": {
"Canonical": {
"Serialization": "DoesNotSerialize"
"Serialization": "Serializes"
}
}
},
"AvatarGestures": {
"Name": "AvatarGestures",
"Scriptability": "ReadWrite",
"DataType": {
"Value": "Bool"
},
"Tags": [
"NotBrowsable"
],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
}
}
},
@@ -61111,10 +61458,12 @@
"DataType": {
"Value": "Bool"
},
"Tags": [],
"Tags": [
"NotReplicated"
],
"Kind": {
"Canonical": {
"Serialization": "DoesNotSerialize"
"Serialization": "Serializes"
}
}
},
@@ -61124,7 +61473,9 @@
"DataType": {
"Enum": "UserCFrame"
},
"Tags": [],
"Tags": [
"NotReplicated"
],
"Kind": {
"Canonical": {
"Serialization": "DoesNotSerialize"
@@ -61283,12 +61634,21 @@
"Attributes": {
"Attributes": {}
},
"AutomaticScaling": {
"Enum": 0
},
"AvatarGestures": {
"Bool": false
},
"Capabilities": {
"SecurityCapabilities": 0
},
"DefinesCapabilities": {
"Bool": false
},
"FadeOutViewOnCollision": {
"Bool": true
},
"SourceAssetId": {
"Int64": -1
},
@@ -62839,16 +63199,6 @@
"Properties": {},
"DefaultProperties": {}
},
"VisibilityService": {
"Name": "VisibilityService",
"Tags": [
"NotCreatable",
"Service"
],
"Superclass": "Instance",
"Properties": {},
"DefaultProperties": {}
},
"Visit": {
"Name": "Visit",
"Tags": [
@@ -62933,9 +63283,7 @@
"DataType": {
"Enum": "AudioApiRollout"
},
"Tags": [
"NotBrowsable"
],
"Tags": [],
"Kind": {
"Canonical": {
"Serialization": "Serializes"
@@ -65214,14 +65562,6 @@
"Timeout": 10
}
},
"AnimationCompositorMode": {
"name": "AnimationCompositorMode",
"items": {
"Default": 0,
"Disabled": 2,
"Enabled": 1
}
},
"AnimationPriority": {
"name": "AnimationPriority",
"items": {
@@ -65835,10 +66175,10 @@
"CollaboratorStatus": {
"name": "CollaboratorStatus",
"items": {
"Editing3D": 0,
"None": 3,
"PrivateScripting": 2,
"Scripting": 1
"Editing3D": 1,
"None": 0,
"PrivateScripting": 3,
"Scripting": 2
}
},
"CollisionFidelity": {
@@ -66307,6 +66647,14 @@
"TranslateViewPlane": 4
}
},
"DragDetectorPermissionPolicy": {
"name": "DragDetectorPermissionPolicy",
"items": {
"Everybody": 1,
"Nobody": 0,
"Scriptable": 2
}
},
"DragDetectorResponseStyle": {
"name": "DragDetectorResponseStyle",
"items": {
@@ -66810,15 +67158,6 @@
"R6": 0
}
},
"HumanoidStateMachineMode": {
"name": "HumanoidStateMachineMode",
"items": {
"Default": 0,
"Legacy": 1,
"LuaStateMachine": 3,
"NoStateMachine": 2
}
},
"HumanoidStateType": {
"name": "HumanoidStateType",
"items": {
@@ -67469,6 +67808,16 @@
"PersistentPerPlayer": 3
}
},
"ModerationStatus": {
"name": "ModerationStatus",
"items": {
"Invalid": 5,
"NotApplicable": 4,
"NotReviewed": 3,
"ReviewedApproved": 1,
"ReviewedRejected": 2
}
},
"ModifierKey": {
"name": "ModifierKey",
"items": {

View File

@@ -185,10 +185,10 @@ function ApiContext:write(patch)
body = Http.jsonEncode(body)
return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
Log.info("Write response: {:?}", body)
return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(responseBody)
Log.info("Write response: {:?}", responseBody)
return body
return responseBody
end)
end

View File

@@ -167,7 +167,7 @@ function Dropdown:render()
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Roact.createFragment(optionButtons),
Options = Roact.createFragment(optionButtons),
}),
})
else nil,

View File

@@ -57,7 +57,7 @@ local function DisplayValue(props)
-- We don't need to support mixed tables, so checking the first key is enough
-- to determine if it's a simple array
local out, i = table.create(#props.value), 0
for k, v in props.value do
for _, v in props.value do
i += 1
-- Wrap strings in quotes

View File

@@ -97,21 +97,16 @@ function DomLabel:render()
-- Line guides help indent depth remain readable
local lineGuides = {}
for i = 1, props.depth or 0 do
table.insert(
lineGuides,
e("Frame", {
Name = "Line_" .. i,
Size = UDim2.new(0, 2, 1, 2),
Position = UDim2.new(0, (20 * i) + 15, 0, -1),
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
})
)
lineGuides["Line_" .. i] = e("Frame", {
Size = UDim2.new(0, 2, 1, 2),
Position = UDim2.new(0, (20 * i) + 15, 0, -1),
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
})
end
return e("Frame", {
Name = "Change",
ClipsDescendants = true,
BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
BorderSizePixel = 0,

View File

@@ -42,7 +42,7 @@ end
function TextButton:render()
return Theme.with(function(theme)
local textSize =
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamSemibold, Vector2.new(math.huge, math.huge))
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
local style = self.props.style
@@ -83,7 +83,7 @@ function TextButton:render()
Text = e("TextLabel", {
Text = self.props.text,
Font = Enum.Font.GothamSemibold,
Font = Enum.Font.GothamMedium,
TextSize = 18,
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
TextTransparency = self.props.transparency,

View File

@@ -1,5 +1,3 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages

View File

@@ -19,7 +19,6 @@ local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Log = require(Packages.Log)
local strict = require(script.Parent.Parent.strict)

View File

@@ -113,27 +113,29 @@ end
function InstanceMap:destroyInstance(instance)
local id = self.fromInstances[instance]
local descendants = instance:GetDescendants()
instance:Destroy()
-- After the instance is successfully destroyed,
-- we can remove all the id mappings
if id ~= nil then
self:removeId(id)
end
for _, descendantInstance in ipairs(instance:GetDescendants()) do
for _, descendantInstance in descendants do
self:removeInstance(descendantInstance)
end
instance:Destroy()
end
function InstanceMap:destroyId(id)
local instance = self.fromIds[id]
self:removeId(id)
if instance ~= nil then
for _, descendantInstance in ipairs(instance:GetDescendants()) do
self:removeInstance(descendantInstance)
end
instance:Destroy()
self:destroyInstance(instance)
else
-- There is no instance with this id, so we can just remove the id
-- without worrying about instance destruction
self:removeId(id)
end
end

View File

@@ -426,22 +426,71 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
-- Update isWarning metadata
for _, failedChange in unappliedPatch.updated do
local node = tree:getNode(failedChange.id)
if node then
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
if node.changeList then
for _, change in node.changeList do
if failedChange.changedProperties[change[1]] then
Log.trace(" Marked property as warning: {}", change[1])
if change[4] == nil then
change[4] = {}
end
change[4].isWarning = true
end
end
end
if not node then
continue
end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
if not node.changeList then
continue
end
for _, change in node.changeList do
local property = change[1]
local propertyFailedToApply = if property == "Name"
then failedChange.changedName ~= nil -- Name is not in changedProperties, so it needs a special case
else failedChange.changedProperties[property] ~= nil
if not propertyFailedToApply then
-- This change didn't fail, no need to mark
continue
end
if change[4] == nil then
change[4] = { isWarning = true }
else
change[4].isWarning = true
end
Log.trace(" Marked property as warning: {}.{}", node.name, property)
end
end
for failedAdditionId in unappliedPatch.added do
local node = tree:getNode(failedAdditionId)
if not node then
continue
end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
if not node.changeList then
continue
end
for _, change in node.changeList do
-- Failed addition means that all properties failed to be added
if change[4] == nil then
change[4] = { isWarning = true }
else
change[4].isWarning = true
end
Log.trace(" Marked property as warning: {}.{}", node.name, change[1])
end
end
for _, failedRemovalIdOrInstance in unappliedPatch.removed do
local failedRemovalId = if Types.RbxId(failedRemovalIdOrInstance)
then failedRemovalIdOrInstance
else instanceMap.fromInstances[failedRemovalIdOrInstance]
if not failedRemovalId then
continue
end
local node = tree:getNode(failedRemovalId)
if not node then
continue
end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
end
-- Update if instances exist

View File

@@ -25,10 +25,15 @@ local function applyPatch(instanceMap, patch)
local unappliedPatch = PatchSet.newEmpty()
for _, removedIdOrInstance in ipairs(patch.removed) do
if Types.RbxId(removedIdOrInstance) then
instanceMap:destroyId(removedIdOrInstance)
else
instanceMap:destroyInstance(removedIdOrInstance)
local removeInstanceSuccess = pcall(function()
if Types.RbxId(removedIdOrInstance) then
instanceMap:destroyId(removedIdOrInstance)
else
instanceMap:destroyInstance(removedIdOrInstance)
end
end)
if not removeInstanceSuccess then
table.insert(unappliedPatch.removed, removedIdOrInstance)
end
end
@@ -170,7 +175,13 @@ local function applyPatch(instanceMap, patch)
end
if update.changedName ~= nil then
instance.Name = update.changedName
local setNameSuccess = pcall(function()
instance.Name = update.changedName
end)
if not setNameSuccess then
unappliedUpdate.changedName = update.changedName
partiallyApplied = true
end
end
if update.changedMetadata ~= nil then
@@ -183,15 +194,15 @@ local function applyPatch(instanceMap, patch)
if update.changedProperties ~= nil then
for propertyName, propertyValue in pairs(update.changedProperties) do
local ok, decodedValue = decodeValue(propertyValue, instanceMap)
if not ok then
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
if not decodeSuccess then
unappliedUpdate.changedProperties[propertyName] = propertyValue
partiallyApplied = true
continue
end
local ok = setProperty(instance, propertyName, decodedValue)
if not ok then
local setPropertySuccess = setProperty(instance, propertyName, decodedValue)
if not setPropertySuccess then
unappliedUpdate.changedProperties[propertyName] = propertyValue
partiallyApplied = true
end

View File

@@ -27,9 +27,9 @@ local function decodeValue(encodedValue, instanceMap)
end
end
local ok, decodedValue = RbxDom.EncodedValue.decode(encodedValue)
local decodeSuccess, decodedValue = RbxDom.EncodedValue.decode(encodedValue)
if not ok then
if not decodeSuccess then
return false,
Error.new(Error.CannotDecodeValue, {
encodedValue = encodedValue,

View File

@@ -147,13 +147,13 @@ local function diff(instanceMap, virtualInstances, rootId)
local changedProperties = {}
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
local ok, existingValueOrErr = getProperty(instance, propertyName)
local getProperySuccess, existingValueOrErr = getProperty(instance, propertyName)
if ok then
if getProperySuccess then
local existingValue = existingValueOrErr
local ok, decodedValue = decodeValue(virtualValue, instanceMap)
local decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
if ok then
if decodeSuccess then
if not trueEquals(existingValue, decodedValue) then
Log.debug(
"{}.{} changed from '{}' to '{}'",
@@ -165,7 +165,6 @@ local function diff(instanceMap, virtualInstances, rootId)
changedProperties[propertyName] = virtualValue
end
else
local propertyType = next(virtualValue)
Log.warn(
"Failed to decode property {}.{}. Encoded property was: {:#?}",
virtualInstance.ClassName,
@@ -178,10 +177,8 @@ local function diff(instanceMap, virtualInstances, rootId)
if err.kind == Error.UnknownProperty then
Log.trace("Skipping unknown property {}.{}", err.details.className, err.details.propertyName)
elseif err.kind == Error.UnreadableProperty then
Log.trace("Skipping unreadable property {}.{}", err.details.className, err.details.propertyName)
else
return false, err
Log.trace("Skipping unreadable property {}.{}", err.details.className, err.details.propertyName)
end
end
end
@@ -220,9 +217,9 @@ local function diff(instanceMap, virtualInstances, rootId)
table.insert(patch.removed, childInstance)
end
else
local ok, err = diffInternal(childId)
local diffSuccess, err = diffInternal(childId)
if not ok then
if not diffSuccess then
return false, err
end
end
@@ -243,9 +240,9 @@ local function diff(instanceMap, virtualInstances, rootId)
return true
end
local ok, err = diffInternal(rootId)
local diffSuccess, err = diffInternal(rootId)
if not ok then
if not diffSuccess then
return false, err
end

View File

@@ -31,13 +31,13 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
-- We guard accessing Name and ClassName in order to avoid
-- tripping over children of DataModel that Rojo won't have
-- permissions to access at all.
local ok, name, className = pcall(function()
local accessSuccess, name, className = pcall(function()
return childInstance.Name, childInstance.ClassName
end)
-- This rule is very conservative and could be loosened in the
-- future, or more heuristics could be introduced.
if ok and name == virtualChild.Name and className == virtualChild.ClassName then
if accessSuccess and name == virtualChild.Name and className == virtualChild.ClassName then
isExistingChildVisited[childIndex] = true
hydrate(instanceMap, virtualInstances, childId, childInstance)
break

View File

@@ -53,9 +53,9 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
-- Instance.new can fail if we're passing in something that can't be
-- created, like a service, something enabled with a feature flag, or
-- something that requires higher security than we have.
local ok, instance = pcall(Instance.new, virtualInstance.ClassName)
local createSuccess, instance = pcall(Instance.new, virtualInstance.ClassName)
if not ok then
if not createSuccess then
addAllToPatch(unappliedPatch, virtualInstances, id)
return
end
@@ -80,14 +80,14 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
continue
end
local ok, value = decodeValue(virtualValue, instanceMap)
if not ok then
local decodeSuccess, value = decodeValue(virtualValue, instanceMap)
if not decodeSuccess then
unappliedProperties[propertyName] = virtualValue
continue
end
local ok = setProperty(instance, propertyName, value)
if not ok then
local setPropertySuccess = setProperty(instance, propertyName, value)
if not setPropertySuccess then
unappliedProperties[propertyName] = virtualValue
end
end
@@ -148,8 +148,8 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
continue
end
local ok = setProperty(entry.instance, entry.propertyName, targetInstance)
if not ok then
local setPropertySuccess = setProperty(entry.instance, entry.propertyName, targetInstance)
if not setPropertySuccess then
markFailed(entry.id, entry.propertyName, entry.virtualValue)
end
end

View File

@@ -3,7 +3,6 @@ return function()
local PatchSet = require(script.Parent.Parent.PatchSet)
local InstanceMap = require(script.Parent.Parent.InstanceMap)
local Error = require(script.Parent.Error)
local function isEmpty(table)
return next(table) == nil, "Table was not empty"

View File

@@ -28,9 +28,9 @@ local function setProperty(instance, propertyName, value)
})
end
local ok, err = descriptor:write(instance, value)
local writeSuccess, err = descriptor:write(instance, value)
if not ok then
if not writeSuccess then
if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("lacking permission") then
return false,
Error.new(Error.LackingPropertyPermissions, {

View File

@@ -21,8 +21,8 @@ local Status = strict("Session.Status", {
Disconnected = "Disconnected",
})
local function debugPatch(patch)
return Fmt.debugify(patch, function(patch, output)
local function debugPatch(object)
return Fmt.debugify(object, function(patch, output)
output:writeLine("Patch {{")
output:indent()
@@ -197,7 +197,7 @@ function ServeSession:__onActiveScriptChanged(activeScript)
local existingParent = activeScript.Parent
activeScript.Parent = nil
for i = 1, 3 do
for _ = 1, 3 do
RunService.Heartbeat:Wait()
end
@@ -251,7 +251,10 @@ function ServeSession:__initialSync(serverInfo)
if userDecision == "Abort" then
return Promise.reject("Aborted Rojo sync operation")
elseif userDecision == "Reject" and self.__twoWaySync then
elseif userDecision == "Reject" then
if not self.__twoWaySync then
return Promise.reject("Cannot reject sync operation without two-way sync enabled")
end
-- The user wants their studio DOM to write back to their Rojo DOM
-- so we will reverse the patch and send it back
@@ -268,7 +271,7 @@ function ServeSession:__initialSync(serverInfo)
table.insert(inversePatch.updated, update)
end
-- Add the removed instances back to Rojo
-- selene:allow(empty_if, unused_variable)
-- selene:allow(empty_if, unused_variable, empty_loop)
for _, instance in catchUpPatch.removed do
-- TODO: Generate ID for our instance and add it to inversePatch.added
end
@@ -277,7 +280,7 @@ function ServeSession:__initialSync(serverInfo)
table.insert(inversePatch.removed, id)
end
self.__apiContext:write(inversePatch)
return self.__apiContext:write(inversePatch)
elseif userDecision == "Accept" then
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
@@ -287,6 +290,10 @@ function ServeSession:__initialSync(serverInfo)
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
)
end
return Promise.resolve()
else
return Promise.reject("Invalid user decision: " .. userDecision)
end
end)
end

View File

@@ -1,53 +0,0 @@
--[[
Create a new signal that can be connected to, disconnected from, and fired.
Usage:
local signal = createSignal()
local disconnect = signal:connect(function(...)
print("fired:", ...)
end)
signal:fire("a", "b", "c")
disconnect()
Avoids mutating listeners list directly to prevent iterator invalidation if
a listener is disconnected while the signal is firing.
]]
local function createSignal()
local listeners = {}
local function connect(newListener)
local nextListeners = {}
for listener in pairs(listeners) do
nextListeners[listener] = true
end
nextListeners[newListener] = true
listeners = nextListeners
return function()
local nextListeners = {}
for listener in pairs(listeners) do
if listener ~= newListener then
nextListeners[listener] = true
end
end
listeners = nextListeners
end
end
local function fire(...)
for listener in pairs(listeners) do
listener(...)
end
end
return {
connect = connect,
fire = fire,
}
end
return createSignal

View File

@@ -0,0 +1,22 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">top-level</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">second-level</string>
</Properties>
<Item class="IntValue" referent="2">
<Properties>
<string name="Name">third-level</string>
<int64 name="Value">1337</int64>
</Properties>
</Item>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,22 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">no_name_project</string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">second-level</string>
</Properties>
<Item class="BoolValue" referent="2">
<Properties>
<string name="Name">bool_value</string>
<bool name="Value">true</bool>
</Properties>
</Item>
</Item>
</Item>
</roblox>

View File

@@ -0,0 +1,13 @@
---
source: tests/tests/build.rs
assertion_line: 104
expression: contents
---
<roblox version="4">
<Item class="StringValue" referent="0">
<Properties>
<string name="Name">no_name_top_level_project</string>
<string name="Value">If this isn't named `no_name_top_level_project`, something went wrong!</string>
</Properties>
</Item>
</roblox>

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">
@@ -25,6 +24,7 @@ expression: contents
<R21>0</R21>
<R22>1</R22>
</CoordinateFrame>
<bool name="NeedsPivotMigration">false</bool>
<Ref name="PrimaryPart">null</Ref>
<BinaryString name="Tags"></BinaryString>
</Properties>

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="DataModel" referent="0">
@@ -22,6 +21,7 @@ expression: contents
<Item class="Workspace" referent="2">
<Properties>
<string name="Name">Workspace</string>
<bool name="NeedsPivotMigration">false</bool>
</Properties>
<Item class="BoolValue" referent="3">
<Properties>

View File

@@ -0,0 +1,9 @@
{
"name": "top-level",
"tree": {
"$className": "Folder",
"second-level": {
"$path": "src"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"tree": {
"$className": "IntValue",
"$properties": {
"Value": 1337
}
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "no_name_project",
"tree": {
"$className": "Folder",
"second-level": {
"$path": "src"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"tree": {
"$className": "BoolValue",
"$properties": {
"Value": true
}
}
}

View File

@@ -0,0 +1,8 @@
{
"tree": {
"$className": "StringValue",
"$properties": {
"Value": "If this isn't named `no_name_top_level_project`, something went wrong!"
}
}
}

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
@@ -22,7 +21,9 @@ instances:
ignoreUnknownInstances: false
Name: test
Parent: id-2
Properties: {}
Properties:
NeedsPivotMigration:
Bool: false
messageCursor: 1
sessionId: id-1

View File

@@ -1,7 +1,6 @@
---
source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
---
messageCursor: 1
messages:
@@ -14,7 +13,9 @@ messages:
ignoreUnknownInstances: false
Name: test
Parent: id-2
Properties: {}
Properties:
NeedsPivotMigration:
Bool: false
removed: []
updated: []
sessionId: id-1

View File

@@ -0,0 +1,39 @@
---
source: tests/tests/serve.rs
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: Folder
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: top-level
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children:
- id-4
ClassName: Folder
Id: id-3
Metadata:
ignoreUnknownInstances: false
Name: second-level
Parent: id-2
Properties: {}
id-4:
Children: []
ClassName: IntValue
Id: id-4
Metadata:
ignoreUnknownInstances: true
Name: third-level
Parent: id-3
Properties:
Value:
Int64: 1337
messageCursor: 0
sessionId: id-1

View File

@@ -0,0 +1,14 @@
---
source: tests/tests/serve.rs
assertion_line: 316
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: top-level
protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -0,0 +1,40 @@
---
source: tests/tests/serve.rs
assertion_line: 338
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children:
- id-3
ClassName: Folder
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: no_name_project
Parent: "00000000000000000000000000000000"
Properties: {}
id-3:
Children:
- id-4
ClassName: Folder
Id: id-3
Metadata:
ignoreUnknownInstances: false
Name: second-level
Parent: id-2
Properties: {}
id-4:
Children: []
ClassName: BoolValue
Id: id-4
Metadata:
ignoreUnknownInstances: true
Name: bool_value
Parent: id-3
Properties:
Value:
Bool: true
messageCursor: 0
sessionId: id-1

View File

@@ -0,0 +1,14 @@
---
source: tests/tests/serve.rs
assertion_line: 335
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: no_name_project
protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -0,0 +1,20 @@
---
source: tests/tests/serve.rs
assertion_line: 306
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
---
instances:
id-2:
Children: []
ClassName: StringValue
Id: id-2
Metadata:
ignoreUnknownInstances: true
Name: no_name_top_level_project
Parent: "00000000000000000000000000000000"
Properties:
Value:
String: "If this isn't named `no_name_top_level_project`, something went wrong!"
messageCursor: 0
sessionId: id-1

View File

@@ -0,0 +1,14 @@
---
source: tests/tests/serve.rs
assertion_line: 300
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
gameId: ~
placeId: ~
projectName: no_name_top_level_project
protocolVersion: 4
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -0,0 +1,9 @@
{
"name": "top-level",
"tree": {
"$className": "Folder",
"second-level": {
"$path": "src"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"tree": {
"$className": "IntValue",
"$properties": {
"Value": 1337
}
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "no_name_project",
"tree": {
"$className": "Folder",
"second-level": {
"$path": "src"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"tree": {
"$className": "BoolValue",
"$properties": {
"Value": true
}
}
}

View File

@@ -0,0 +1,8 @@
{
"tree": {
"$className": "StringValue",
"$properties": {
"Value": "If this isn't named `no_name_top_level_project`, something went wrong!"
}
}
}

View File

@@ -39,7 +39,7 @@ enum Error {
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Project {
/// The name of the top-level instance described by the project.
pub name: String,
pub name: Option<String>,
/// The tree of instances described by this project. Projects always
/// describe at least one instance.

View File

@@ -110,7 +110,7 @@ impl ServeSession {
log::debug!("Loading project file from {}", project_path.display());
let root_project = match vfs.read(&project_path).with_not_found()? {
let mut root_project = match vfs.read(&project_path).with_not_found()? {
Some(contents) => Project::load_from_slice(&contents, &project_path)?,
None => {
return Err(ServeSessionError::NoProjectFound {
@@ -118,6 +118,35 @@ impl ServeSession {
});
}
};
if root_project.name.is_none() {
if let Some(file_name) = project_path.file_name().and_then(|s| s.to_str()) {
if file_name == "default.project.json" {
let folder_name = project_path
.parent()
.and_then(Path::file_name)
.and_then(|s| s.to_str());
if let Some(folder_name) = folder_name {
root_project.name = Some(folder_name.to_string());
} else {
return Err(ServeSessionError::FolderNameInvalid {
path: project_path.to_path_buf(),
});
}
} else if let Some(trimmed) = file_name.strip_suffix(".project.json") {
root_project.name = Some(trimmed.to_string());
} else {
return Err(ServeSessionError::ProjectNameInvalid {
path: project_path.to_path_buf(),
});
}
} else {
return Err(ServeSessionError::ProjectNameInvalid {
path: project_path.to_path_buf(),
});
}
}
// Rebind it to make it no longer mutable
let root_project = root_project;
let mut tree = RojoTree::new(InstanceSnapshot::new());
@@ -190,7 +219,10 @@ impl ServeSession {
}
pub fn project_name(&self) -> &str {
&self.root_project.name
self.root_project
.name
.as_ref()
.expect("all top-level projects must have their name set")
}
pub fn project_port(&self) -> Option<u16> {
@@ -231,6 +263,14 @@ pub enum ServeSessionError {
)]
NoProjectFound { path: PathBuf },
#[error("The folder for the provided project cannot be used as a project name: {}\n\
Consider setting the `name` field on this project.", .path.display())]
FolderNameInvalid { path: PathBuf },
#[error("The file name of the provided project cannot be used as a project name: {}.\n\
Consider setting the `name` field on this project.", .path.display())]
ProjectNameInvalid { path: PathBuf },
#[error(transparent)]
Io {
#[from]

View File

@@ -87,10 +87,28 @@ impl RojoTree {
}
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
// !!!!!!!!!! UGLY HACK !!!!!!!!!!
//
// This is a set of special cases working around a more general problem upstream
// in rbx-dom that causes pivots to not build to file correctly, described in
// github.com/rojo-rbx/rojo/issues/628.
//
// We need to insert the NeedsPivotMigration property with a value of false on
// every instance that inherits from Model for pivots to build correctly.
let hack_needs_pivot_migration = match snapshot.class_name.as_ref() {
"Model" | "Actor" | "Tool" | "HopperBin" | "Flag" | "WorldModel" | "Workspace"
if !snapshot.properties.contains_key("NeedsPivotMigration") =>
{
vec![("NeedsPivotMigration", Variant::Bool(false))]
}
_ => Vec::new(),
};
let builder = InstanceBuilder::empty()
.with_class(snapshot.class_name.into_owned())
.with_name(snapshot.name.into_owned())
.with_properties(snapshot.properties);
.with_properties(snapshot.properties)
.with_properties(hack_needs_pivot_migration);
let referent = self.inner.insert(parent_ref, builder);
self.insert_metadata(referent, snapshot.metadata);

View File

@@ -1,6 +1,5 @@
use std::{collections::HashMap, path::Path, str};
use std::{collections::HashMap, path::Path};
use anyhow::Context;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::types::Enum;
@@ -58,10 +57,8 @@ pub fn snapshot_lua(
(_, ScriptType::Module) => ("ModuleScript", None),
};
let contents = vfs.read(path)?;
let contents_str = str::from_utf8(&contents)
.with_context(|| format!("File was not valid UTF-8: {}", path.display()))?
.to_owned();
let contents = vfs.read_to_string_lf_normalized(path)?;
let contents_str = contents.as_str();
let mut properties = HashMap::with_capacity(2);
properties.insert("Source".to_owned(), contents_str.into());

View File

@@ -22,6 +22,25 @@ pub fn snapshot_project(
let project = Project::load_from_slice(&vfs.read(path)?, path)
.with_context(|| format!("File was not a valid Rojo project: {}", path.display()))?;
// This is not how I would normally do this, but this is a temporary
// implementation. The one in 7.5+ is better.
let project_name = project.name.as_deref().unwrap_or_else(|| {
let file_name = path
.file_name()
.and_then(|s| s.to_str())
.expect("project file names should be valid UTF-8");
if file_name == "default.project.json" {
path.parent()
.and_then(Path::file_name)
.and_then(|s| s.to_str())
.expect("default.project.json should be inside a folder with a valid UTF-8 name")
} else {
file_name
.strip_suffix(".project.json")
.expect("project file names should end with .project.json")
}
});
let mut context = context.clone();
let rules = project.glob_ignore_paths.iter().map(|glob| PathIgnoreRule {
@@ -37,7 +56,7 @@ pub fn snapshot_project(
.unwrap(),
);
match snapshot_project_node(&context, path, &project.name, &project.tree, vfs, None)? {
match snapshot_project_node(&context, path, project_name, &project.tree, vfs, None)? {
Some(found_snapshot) => {
let mut snapshot = found_snapshot;
// Setting the instigating source to the project file path is a little
@@ -669,4 +688,36 @@ mod test {
insta::assert_yaml_snapshot!(instance_snapshot);
}
#[test]
fn no_name_project() {
let _ = env_logger::try_init();
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/no_name_project",
VfsSnapshot::dir(hashmap! {
"default.project.json" => VfsSnapshot::file(r#"
{
"tree": {
"$className": "Model"
}
}
"#),
}),
)
.unwrap();
let vfs = Vfs::new(imfs);
let instance_snapshot = snapshot_project(
&InstanceContext::default(),
&vfs,
Path::new("/no_name_project/default.project.json"),
)
.expect("snapshot error")
.expect("snapshot returned no instances");
insta::assert_yaml_snapshot!(instance_snapshot);
}
}

View File

@@ -0,0 +1,18 @@
---
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
---
snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
Path: /no_name_project/default.project.json
relevant_paths:
- /no_name_project/default.project.json
context:
emit_legacy_scripts: true
name: no_name_project
class_name: Model
properties: {}
children: []

View File

@@ -1,6 +1,5 @@
use std::{path::Path, str};
use std::path::Path;
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
@@ -14,11 +13,8 @@ pub fn snapshot_txt(
path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let name = path.file_name_trim_end(".txt")?;
let contents = vfs.read(path)?;
let contents_str = str::from_utf8(&contents)
.with_context(|| format!("File was not valid UTF-8: {}", path.display()))?
.to_owned();
let contents = vfs.read_to_string(path)?;
let contents_str = contents.as_str();
let properties = hashmap! {
"Value".to_owned() => contents_str.into(),

View File

@@ -59,6 +59,9 @@ gen_build_tests! {
txt_in_folder,
unresolved_values,
weldconstraint,
no_name_default_project,
no_name_project,
no_name_top_level_project,
}
fn run_build_test(test_name: &str) {
@@ -70,7 +73,7 @@ fn run_build_test(test_name: &str) {
let output_path = output_dir.path().join(format!("{}.rbxmx", test_name));
let output = Command::new(ROJO_PATH)
.args(&[
.args([
"build",
input_path.to_str().unwrap(),
"-o",

View File

@@ -255,3 +255,57 @@ fn add_optional_folder() {
);
});
}
#[test]
fn no_name_default_project() {
run_serve_test("no_name_default_project", |session, mut redactions| {
let info = session.get_api_rojo().unwrap();
let root_id = info.root_instance_id;
assert_yaml_snapshot!(
"no_name_default_project_info",
redactions.redacted_yaml(info)
);
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
"no_name_default_project_all",
read_response.intern_and_redact(&mut redactions, root_id)
);
});
}
#[test]
fn no_name_project() {
run_serve_test("no_name_project", |session, mut redactions| {
let info = session.get_api_rojo().unwrap();
let root_id = info.root_instance_id;
assert_yaml_snapshot!("no_name_project_info", redactions.redacted_yaml(info));
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
"no_name_project_all",
read_response.intern_and_redact(&mut redactions, root_id)
);
});
}
#[test]
fn no_name_top_level_project() {
run_serve_test("no_name_top_level_project", |session, mut redactions| {
let info = session.get_api_rojo().unwrap();
let root_id = info.root_instance_id;
assert_yaml_snapshot!(
"no_name_top_level_project_info",
redactions.redacted_yaml(info)
);
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
"no_name_top_level_project_all",
read_response.intern_and_redact(&mut redactions, root_id)
);
});
}