Compare commits

..

9 Commits

Author SHA1 Message Date
Lucien Greathouse
eddc469f95 Release 6.2.0 2021-06-10 00:45:10 -04:00
Lucien Greathouse
21a4667fe4 Fix failing snapshot 2021-06-10 00:44:53 -04:00
Lucien Greathouse
b25f2fcd5d Update dependencies 2021-06-10 00:41:35 -04:00
Lucien Greathouse
0f7c9493d2 Fix 'Open Scripts Externally' crashing studio.
Closes #369.
2021-04-23 17:08:11 -04:00
Lucien Greathouse
f1c4102d7f Update changelog 2021-04-23 16:00:45 -04:00
Lucien Greathouse
8b5bfd5f44 Mark two-way sync as experimental in UI 2021-04-23 15:59:31 -04:00
Lucien Greathouse
0599b50235 Release 6.1.0 2021-04-12 17:19:35 -04:00
Lucien Greathouse
21f7ef6186 Update dependencies 2021-04-09 18:44:03 -04:00
MSAA
de6470bb45 change server bind address (#403)
* web/mod.rs - change server bind address

127.0.0.1 is a loopback interface, and only works on the same host
0.0.0.0 will allow connections from other hosts

ideally, this should be a console arg - but it's a quick fix

* implement --address option, revert default bind address to 127.0.0.1

* revert silly autoformatting

* ok, actually using rustfmt now

* More precise --address flag description

* Use SocketAddr where available, take advantage of const-ness

* Display 'localhost' if address is loopback

* Update Changelog

Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
2021-03-31 16:44:10 -04:00
77 changed files with 21292 additions and 46046 deletions

View File

@@ -2,25 +2,21 @@
## Unreleased Changes
## [7.0.0-alpha.2][7.0.0-alpha.2] (February 19, 2021)
* Fixed incorrect protocol version between the client and server.
## [6.2.0][6.2.0] (June 10, 2021)
* Added "EXPERIMENTAL!" label to two-way sync toggle in Rojo's Roblox Studio plugin.
* Fixed "Open Scripts Externally" feature crashing Studio ([#369][issue-369])
* Updated dependencies, fixing `HumanoidDescription` ID issues.
[7.0.0-alpha.2]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0-alpha.2
[issue-369]: https://github.com/rojo-rbx/rojo/issues/369
[6.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v6.2.0
## [7.0.0-alpha.1][7.0.0-alpha.1] (February 18, 2021)
This release includes a brand new implementation of the Roblox DOM. It brings performance improvements, much better support for `rbxl` and `rbxm` files, and a better internal API.
## [6.1.0][6.1.0] (April 12, 2021)
* Updated dependencies, fixing OptionalCoordinateFrame-related issues.
* Added support for all remaining property types.
* Added support for the entire Roblox binary model format.
* Changed `rojo upload` to upload binary places and models instead of XML.
* This should make using `rojo upload` much more feasible for large places.
* **Breaking**: Changed format of some types of values in `project.json`, `model.json`, and `meta.json` files.
* This should impact few projects. See [this file][allValues.json] for new examples of each property type.
* Added `--address` flag to `rojo serve` to allow for external connections. ([#403][pr-403])
Formatting of types will change more before the stable release of Rojo 7. We're hoping to use this opportunity to normalize some of the case inconsistency introduced in Rojo 0.5.
[7.0.0-alpha.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.0.0-alpha.1
[allValues.json]: https://github.com/rojo-rbx/rojo/blob/f4a790eb50b74e482000bad1dcfe22533992fb20/plugin/rbx_dom_lua/src/allValues.json
[pr-403]: https://github.com/rojo-rbx/rojo/pull/403
[6.1.0]: https://github.com/rojo-rbx/rojo/releases/tag/v6.1.0
## [6.0.2](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.2) (February 9, 2021)
* Fixed `rojo upload` to handle CSRF challenges.

View File

@@ -41,9 +41,9 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
7. Publish the CLI
* `cargo publish`
8. Publish the Plugin
* `rojo publish plugin --asset_id 6415005344`
8. Build and upload the plugin
* `rojo build plugin -o Rojo.rbxm`
* Upload `Rojo.rbxm` to Roblox.com, keep it for later
9. Push commits and tags
* `git push && git push --tags`
10. Copy GitHub release content from previous release

740
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "rojo"
version = "7.0.0-alpha.2"
version = "6.2.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0"
@@ -47,19 +47,6 @@ harness = false
[dependencies]
memofs = { version = "0.1.2", path = "memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.6.0-alpha.1"
rbx_dom_weak = "2.0.0-alpha.1"
rbx_reflection = "4.0.0-alpha.1"
rbx_reflection_database = "0.1.0"
rbx_xml = "0.12.0-alpha.1"
anyhow = "1.0.27"
backtrace = "0.3"
bincode = "1.2.1"
@@ -77,6 +64,10 @@ log = "0.4.8"
maplit = "1.0.1"
notify = "4.0.14"
opener = "0.4.1"
rbx_binary = "0.5.0"
rbx_dom_weak = "1.10.1"
rbx_reflection = "3.3.408"
rbx_xml = "0.11.3"
regex = "1.3.1"
reqwest = "0.9.20"
ritz = "0.1.0"

View File

@@ -1 +0,0 @@
require(game.ReplicatedStorage.TestEZ).TestBootstrap:run({game.ReplicatedStorage.RbxDom})

View File

@@ -20,437 +20,223 @@ local function serializeFloat(value)
return value
end
local ALL_AXES = {"X", "Y", "Z"}
local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
local encoders
encoders = {
Bool = identity,
Content = identity,
Float32 = serializeFloat,
Float64 = serializeFloat,
Int32 = identity,
Int64 = identity,
String = identity,
local types
types = {
Axes = {
fromPod = function(pod)
local axes = {}
BinaryString = base64.encode,
SharedString = base64.encode,
for index, axisName in ipairs(pod) do
axes[index] = Enum.Axis[axisName]
end
BrickColor = function(value)
return value.Number
end,
return Axes.new(unpack(axes))
end,
CFrame = function(value)
return {value:GetComponents()}
end,
Color3 = function(value)
return {value.r, value.g, value.b}
end,
NumberRange = function(value)
return {value.Min, value.Max}
end,
NumberSequence = function(value)
local keypoints = {}
toPod = function(roblox)
local json = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = {
Time = keypoint.Time,
Value = keypoint.Value,
Envelope = keypoint.Envelope,
}
end
for _, axis in ipairs(ALL_AXES) do
if roblox[axis] then
table.insert(json, axis)
end
end
return {
Keypoints = keypoints,
}
end,
ColorSequence = function(value)
local keypoints = {}
return json
end,
},
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = {
Time = keypoint.Time,
Color = encoders.Color3(keypoint.Value),
}
end
BinaryString = {
fromPod = base64.decode,
toPod = base64.encode,
},
return {
Keypoints = keypoints,
}
end,
Rect = function(value)
return {
Min = {value.Min.X, value.Min.Y},
Max = {value.Max.X, value.Max.Y},
}
end,
UDim = function(value)
return {value.Scale, value.Offset}
end,
UDim2 = function(value)
return {value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset}
end,
Vector2 = function(value)
return {
serializeFloat(value.X),
serializeFloat(value.Y),
}
end,
Vector2int16 = function(value)
return {value.X, value.Y}
end,
Vector3 = function(value)
return {
serializeFloat(value.X),
serializeFloat(value.Y),
serializeFloat(value.Z),
}
end,
Vector3int16 = function(value)
return {value.X, value.Y, value.Z}
end,
Bool = {
fromPod = identity,
toPod = identity,
},
PhysicalProperties = function(value)
if value == nil then
return nil
else
return {
Density = value.Density,
Friction = value.Friction,
Elasticity = value.Elasticity,
FrictionWeight = value.FrictionWeight,
ElasticityWeight = value.ElasticityWeight,
}
end
end,
BrickColor = {
fromPod = function(pod)
return BrickColor.new(pod)
end,
Ref = function(value)
return nil
end,
}
toPod = function(roblox)
return roblox.Number
end,
},
local decoders = {
Bool = identity,
Content = identity,
Enum = identity,
Float32 = identity,
Float64 = identity,
Int32 = identity,
Int64 = identity,
String = identity,
CFrame = {
fromPod = function(pod)
local pos = pod.Position
local orient = pod.Orientation
BinaryString = base64.decode,
SharedString = base64.decode,
return CFrame.new(
pos[1], pos[2], pos[3],
orient[1][1], orient[1][2], orient[1][3],
orient[2][1], orient[2][2], orient[2][3],
orient[3][1], orient[3][2], orient[3][3]
BrickColor = BrickColor.new,
CFrame = unpackDecoder(CFrame.new),
Color3 = unpackDecoder(Color3.new),
Color3uint8 = unpackDecoder(Color3.fromRGB),
NumberRange = unpackDecoder(NumberRange.new),
UDim = unpackDecoder(UDim.new),
UDim2 = unpackDecoder(UDim2.new),
Vector2 = unpackDecoder(Vector2.new),
Vector2int16 = unpackDecoder(Vector2int16.new),
Vector3 = unpackDecoder(Vector3.new),
Vector3int16 = unpackDecoder(Vector3int16.new),
Rect = function(value)
return Rect.new(value.Min[1], value.Min[2], value.Max[1], value.Max[2])
end,
NumberSequence = function(value)
local keypoints = {}
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = NumberSequenceKeypoint.new(
keypoint.Time,
keypoint.Value,
keypoint.Envelope
)
end,
end
toPod = function(roblox)
local x, y, z,
r00, r01, r02,
r10, r11, r12,
r20, r21, r22 = roblox:GetComponents()
return NumberSequence.new(keypoints)
end,
return {
Position = {x, y, z},
Orientation = {
{r00, r01, r02},
{r10, r11, r12},
{r20, r21, r22},
},
}
end,
},
ColorSequence = function(value)
local keypoints = {}
Color3 = {
fromPod = unpackDecoder(Color3.new),
toPod = function(roblox)
return {roblox.r, roblox.g, roblox.b}
end,
},
Color3uint8 = {
fromPod = unpackDecoder(Color3.fromRGB),
toPod = function(roblox)
return {
math.round(roblox.R * 255),
math.round(roblox.G * 255),
math.round(roblox.B * 255),
}
end,
},
ColorSequence = {
fromPod = function(pod)
local keypoints = {}
for index, keypoint in ipairs(pod.Keypoints) do
keypoints[index] = ColorSequenceKeypoint.new(
keypoint.Time,
types.Color3.fromPod(keypoint.Color)
)
end
return ColorSequence.new(keypoints)
end,
toPod = function(roblox)
local keypoints = {}
for index, keypoint in ipairs(roblox.Keypoints) do
keypoints[index] = {
Time = keypoint.Time,
Color = types.Color3.toPod(keypoint.Value),
}
end
return {
Keypoints = keypoints,
}
end,
},
Content = {
fromPod = identity,
toPod = identity,
},
Enum = {
fromPod = identity,
toPod = function(roblox)
-- FIXME: More robust handling of enums
if typeof(roblox) == "number" then
return roblox
else
return roblox.Value
end
end,
},
Faces = {
fromPod = function(pod)
local faces = {}
for index, faceName in ipairs(pod) do
faces[index] = Enum.NormalId[faceName]
end
return Faces.new(unpack(faces))
end,
toPod = function(roblox)
local pod = {}
for _, face in ipairs(ALL_FACES) do
if roblox[face] then
table.insert(pod, face)
end
end
return pod
end,
},
Float32 = {
fromPod = identity,
toPod = serializeFloat,
},
Float64 = {
fromPod = identity,
toPod = serializeFloat,
},
Int32 = {
fromPod = identity,
toPod = identity,
},
Int64 = {
fromPod = identity,
toPod = identity,
},
NumberRange = {
fromPod = unpackDecoder(NumberRange.new),
toPod = function(roblox)
return {roblox.Min, roblox.Max}
end,
},
NumberSequence = {
fromPod = function(pod)
local keypoints = {}
for index, keypoint in ipairs(pod.Keypoints) do
keypoints[index] = NumberSequenceKeypoint.new(
keypoint.Time,
keypoint.Value,
keypoint.Envelope
)
end
return NumberSequence.new(keypoints)
end,
toPod = function(roblox)
local keypoints = {}
for index, keypoint in ipairs(roblox.Keypoints) do
keypoints[index] = {
Time = keypoint.Time,
Value = keypoint.Value,
Envelope = keypoint.Envelope,
}
end
return {
Keypoints = keypoints,
}
end,
},
PhysicalProperties = {
fromPod = function(pod)
if pod == "Default" then
return nil
else
return PhysicalProperties.new(
pod.Density,
pod.Friction,
pod.Elasticity,
pod.FrictionWeight,
pod.ElasticityWeight
)
end
end,
toPod = function(roblox)
if roblox == nil then
return "Default"
else
return {
Density = roblox.Density,
Friction = roblox.Friction,
Elasticity = roblox.Elasticity,
FrictionWeight = roblox.FrictionWeight,
ElasticityWeight = roblox.ElasticityWeight,
}
end
end,
},
Ray = {
fromPod = function(pod)
return Ray.new(
types.Vector3.fromPod(pod.Origin),
types.Vector3.fromPod(pod.Direction)
for index, keypoint in ipairs(value.Keypoints) do
keypoints[index] = ColorSequenceKeypoint.new(
keypoint.Time,
Color3.new(unpack(keypoint.Color))
)
end,
end
toPod = function(roblox)
return {
Origin = types.Vector3.toPod(roblox.Origin),
Direction = types.Vector3.toPod(roblox.Direction),
}
end,
},
return ColorSequence.new(keypoints)
end,
Rect = {
fromPod = function(pod)
return Rect.new(
types.Vector2.fromPod(pod[1]),
types.Vector2.fromPod(pod[2])
PhysicalProperties = function(properties)
if properties == nil then
return nil
else
return PhysicalProperties.new(
properties.Density,
properties.Friction,
properties.Elasticity,
properties.FrictionWeight,
properties.ElasticityWeight
)
end,
end
end,
toPod = function(roblox)
return {
types.Vector2.toPod(roblox.Min),
types.Vector2.toPod(roblox.Max),
}
end,
},
Ref = {
fromPod = function(_pod)
error("Ref cannot be decoded on its own")
end,
toPod = function(_roblox)
error("Ref can not be encoded on its own")
end,
},
Region3 = {
fromPod = function(pod)
error("Region3 is not implemented")
end,
toPod = function(roblox)
error("Region3 is not implemented")
end,
},
Region3int16 = {
fromPod = function(pod)
return Region3int16.new(
types.Vector3int16.fromPod(pod[1]),
types.Vector3int16.fromPod(pod[2])
)
end,
toPod = function(roblox)
return {
types.Vector3int16.toPod(roblox.Min),
types.Vector3int16.toPod(roblox.Max),
}
end,
},
SharedString = {
fromPod = function(pod)
error("SharedString is not supported")
end,
toPod = function(roblox)
error("SharedString is not supported")
end,
},
String = {
fromPod = identity,
toPod = identity,
},
UDim = {
fromPod = unpackDecoder(UDim.new),
toPod = function(roblox)
return {roblox.Scale, roblox.Offset}
end,
},
UDim2 = {
fromPod = function(pod)
return UDim2.new(
types.UDim.fromPod(pod[1]),
types.UDim.fromPod(pod[2])
)
end,
toPod = function(roblox)
return {
types.UDim.toPod(roblox.X),
types.UDim.toPod(roblox.Y),
}
end,
},
Vector2 = {
fromPod = unpackDecoder(Vector2.new),
toPod = function(roblox)
return {
serializeFloat(roblox.X),
serializeFloat(roblox.Y),
}
end,
},
Vector2int16 = {
fromPod = unpackDecoder(Vector2int16.new),
toPod = function(roblox)
return {roblox.X, roblox.Y}
end,
},
Vector3 = {
fromPod = unpackDecoder(Vector3.new),
toPod = function(roblox)
return {
serializeFloat(roblox.X),
serializeFloat(roblox.Y),
serializeFloat(roblox.Z),
}
end,
},
Vector3int16 = {
fromPod = unpackDecoder(Vector3int16.new),
toPod = function(roblox)
return {roblox.X, roblox.Y, roblox.Z}
end,
},
Ref = function()
return nil
end,
}
local EncodedValue = {}
function EncodedValue.decode(encodedValue)
local typeImpl = types[encodedValue.Type]
if typeImpl == nil then
return false, "Couldn't decode value " .. tostring(encodedValue.Type)
local decoder = decoders[encodedValue.Type]
if decoder ~= nil then
return true, decoder(encodedValue.Value)
end
return true, typeImpl.fromPod(encodedValue.Value)
return false, "Couldn't decode value " .. tostring(encodedValue.Type)
end
function EncodedValue.encode(rbxValue, propertyType)
assert(propertyType ~= nil, "Property type descriptor is required")
local typeImpl = types[propertyType]
if typeImpl == nil then
return false, ("Missing encoder for property type %q"):format(propertyType)
if propertyType.type == "Data" then
local encoder = encoders[propertyType.name]
if encoder == nil then
return false, ("Missing encoder for property type %q"):format(propertyType.name)
end
if encoder ~= nil then
return true, {
Type = propertyType.name,
Value = encoder(rbxValue),
}
end
elseif propertyType.type == "Enum" then
return true, {
Type = "Enum",
Value = rbxValue.Value,
}
end
return true, {
Type = propertyType,
Value = typeImpl.toPod(rbxValue),
}
return false, ("Unknown property descriptor type %q"):format(tostring(propertyType.type))
end
return EncodedValue
return EncodedValue

View File

@@ -1,72 +1,127 @@
return function()
local HttpService = game:GetService("HttpService")
local RbxDom = require(script.Parent)
local EncodedValue = require(script.Parent.EncodedValue)
local allValues = require(script.Parent.allValues)
local function deepEq(a, b)
if typeof(a) ~= typeof(b) then
return false
end
it("should decode Rect values", function()
local input = {
Type = "Rect",
Value = {
Min = {1, 2},
Max = {3, 4},
},
}
local ty = typeof(a)
local output = Rect.new(1, 2, 3, 4)
if ty == "table" then
local visited = {}
for key, valueA in pairs(a) do
visited[key] = true
if not deepEq(valueA, b[key]) then
return false
end
end
local ok, decoded = EncodedValue.decode(input)
for key, valueB in pairs(b) do
if visited[key] then
continue
end
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
if not deepEq(valueB, a[key]) then
return false
end
end
it("should decode ColorSequence values", function()
local input = {
Type = "ColorSequence",
Value = {
Keypoints = {
{
Time = 0,
Color = { 0.12, 0.34, 0.56 },
},
return true
else
return a == b
end
end
{
Time = 1,
Color = { 0.13, 0.33, 0.37 },
},
}
},
}
local extraAssertions = {
CFrame = function(value)
expect(value).to.equal(CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
end,
}
local output = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(0.12, 0.34, 0.56)),
ColorSequenceKeypoint.new(1, Color3.new(0.13, 0.33, 0.37)),
})
for testName, testEntry in pairs(allValues) do
it("round trip " .. testName, function()
local ok, decoded = EncodedValue.decode(testEntry.value)
assert(ok, decoded)
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
if extraAssertions[testName] ~= nil then
extraAssertions[testName](decoded)
end
it("should decode NumberSequence values", function()
local input = {
Type = "NumberSequence",
Value = {
Keypoints = {
{
Time = 0,
Value = 0.5,
Envelope = 0,
},
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty)
assert(ok, encoded)
{
Time = 1,
Value = 0.5,
Envelope = 0,
},
}
},
}
if not deepEq(encoded, testEntry.value) then
local expected = HttpService:JSONEncode(testEntry.value)
local actual = HttpService:JSONEncode(encoded)
local output = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.5, 0),
NumberSequenceKeypoint.new(1, 0.5, 0),
})
local message = string.format(
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
expected, actual
)
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
error(message)
end
end)
end
end
it("should decode PhysicalProperties values", function()
local input = {
Type = "PhysicalProperties",
Value = {
Density = 0.1,
Friction = 0.2,
Elasticity = 0.3,
FrictionWeight = 0.4,
ElasticityWeight = 0.5,
},
}
local output = PhysicalProperties.new(
0.1,
0.2,
0.3,
0.4,
0.5
)
local ok, decoded = EncodedValue.decode(input)
assert(ok, decoded)
expect(decoded).to.equal(output)
end)
-- This part of rbx_dom_lua needs some work still.
itSKIP("should encode Rect values", function()
local input = Rect.new(10, 20, 30, 40)
local output = {
Type = "Rect",
Value = {
Min = {10, 20},
Max = {30, 40},
},
}
local descriptor = RbxDom.findCanonicalPropertyDescriptor("ImageLabel", "SliceCenter")
local ok, encoded = EncodedValue.encode(input, descriptor)
assert(ok, encoded)
expect(encoded.Type).to.equal(output.Type)
expect(encoded.Value.Min[1]).to.equal(output.Value.Min[1])
expect(encoded.Value.Min[2]).to.equal(output.Value.Min[2])
expect(encoded.Value.Max[1]).to.equal(output.Value.Max[1])
expect(encoded.Value.Max[2]).to.equal(output.Value.Max[2])
end)
end

View File

@@ -21,7 +21,7 @@ end
function PropertyDescriptor.fromRaw(data, className, propertyName)
return setmetatable({
scriptability = data.Scriptability,
scriptability = data.scriptability,
className = className,
name = propertyName,
}, PropertyDescriptor)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
return {
classes = require(script.classes)
}

View File

@@ -1,345 +0,0 @@
{
"Axes": {
"value": {
"Type": "Axes",
"Value": [
"X",
"Y",
"Z"
]
},
"ty": "Axes"
},
"BinaryString": {
"value": {
"Type": "BinaryString",
"Value": "SGVsbG8h"
},
"ty": "BinaryString"
},
"Bool": {
"value": {
"Type": "Bool",
"Value": true
},
"ty": "Bool"
},
"BrickColor": {
"value": {
"Type": "BrickColor",
"Value": 1004
},
"ty": "BrickColor"
},
"CFrame": {
"value": {
"Type": "CFrame",
"Value": {
"Position": [
1.0,
2.0,
3.0
],
"Orientation": [
[
4.0,
5.0,
6.0
],
[
7.0,
8.0,
9.0
],
[
10.0,
11.0,
12.0
]
]
}
},
"ty": "CFrame"
},
"Color3": {
"value": {
"Type": "Color3",
"Value": [
1.0,
2.0,
3.0
]
},
"ty": "Color3"
},
"Color3uint8": {
"value": {
"Type": "Color3uint8",
"Value": [
0,
128,
255
]
},
"ty": "Color3uint8"
},
"ColorSequence": {
"value": {
"Type": "ColorSequence",
"Value": {
"Keypoints": [
{
"Time": 0.0,
"Color": [
1.0,
1.0,
0.5
]
},
{
"Time": 1.0,
"Color": [
0.0,
0.0,
0.0
]
}
]
}
},
"ty": "ColorSequence"
},
"Content": {
"value": {
"Type": "Content",
"Value": "rbxassetid://12345"
},
"ty": "Content"
},
"Enum": {
"value": {
"Type": "Enum",
"Value": 1234
},
"ty": "Enum"
},
"Faces": {
"value": {
"Type": "Faces",
"Value": [
"Right",
"Top",
"Back",
"Left",
"Bottom",
"Front"
]
},
"ty": "Faces"
},
"Float32": {
"value": {
"Type": "Float32",
"Value": 15.0
},
"ty": "Float32"
},
"Float64": {
"value": {
"Type": "Float64",
"Value": 15123.0
},
"ty": "Float64"
},
"Int32": {
"value": {
"Type": "Int32",
"Value": 6014
},
"ty": "Int32"
},
"Int64": {
"value": {
"Type": "Int64",
"Value": 23491023
},
"ty": "Int64"
},
"NumberRange": {
"value": {
"Type": "NumberRange",
"Value": [
-36.0,
94.0
]
},
"ty": "NumberRange"
},
"NumberSequence": {
"value": {
"Type": "NumberSequence",
"Value": {
"Keypoints": [
{
"Time": 0.0,
"Value": 5.0,
"Envelope": 2.0
},
{
"Time": 1.0,
"Value": 22.0,
"Envelope": 0.0
}
]
}
},
"ty": "NumberSequence"
},
"PhysicalProperties-Custom": {
"value": {
"Type": "PhysicalProperties",
"Value": {
"Density": 0.5,
"Friction": 1.0,
"Elasticity": 0.0,
"FrictionWeight": 50.0,
"ElasticityWeight": 25.0
}
},
"ty": "PhysicalProperties"
},
"PhysicalProperties-Default": {
"value": {
"Type": "PhysicalProperties",
"Value": "Default"
},
"ty": "PhysicalProperties"
},
"Ray": {
"value": {
"Type": "Ray",
"Value": {
"Origin": [
1.0,
2.0,
3.0
],
"Direction": [
4.0,
5.0,
6.0
]
}
},
"ty": "Ray"
},
"Rect": {
"value": {
"Type": "Rect",
"Value": [
[
0.0,
5.0
],
[
10.0,
15.0
]
]
},
"ty": "Rect"
},
"Region3int16": {
"value": {
"Type": "Region3int16",
"Value": [
[
-10,
-5,
0
],
[
5,
10,
15
]
]
},
"ty": "Region3int16"
},
"String": {
"value": {
"Type": "String",
"Value": "Hello, world!"
},
"ty": "String"
},
"UDim": {
"value": {
"Type": "UDim",
"Value": [
1.0,
32
]
},
"ty": "UDim"
},
"UDim2": {
"value": {
"Type": "UDim2",
"Value": [
[
-1.0,
100
],
[
1.0,
-100
]
]
},
"ty": "UDim2"
},
"Vector2": {
"value": {
"Type": "Vector2",
"Value": [
-50.0,
50.0
]
},
"ty": "Vector2"
},
"Vector2int16": {
"value": {
"Type": "Vector2int16",
"Value": [
-300,
300
]
},
"ty": "Vector2int16"
},
"Vector3": {
"value": {
"Type": "Vector3",
"Value": [
-300.0,
0.0,
1500.0
]
},
"ty": "Vector3"
},
"Vector3int16": {
"value": {
"Type": "Vector3int16",
"Value": [
60,
37,
-450
]
},
"ty": "Vector3int16"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
local database = require(script.database)
local ReflectionDatabase = require(script.ReflectionDatabase)
local Error = require(script.Error)
local PropertyDescriptor = require(script.PropertyDescriptor)
@@ -6,31 +6,29 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
local currentClassName = className
repeat
local currentClass = database.Classes[currentClassName]
local currentClass = ReflectionDatabase.classes[currentClassName]
if currentClass == nil then
return currentClass
end
local propertyData = currentClass.Properties[propertyName]
local propertyData = currentClass.properties[propertyName]
if propertyData ~= nil then
local canonicalData = propertyData.Kind.Canonical
if canonicalData ~= nil then
if propertyData.isCanonical then
return PropertyDescriptor.fromRaw(propertyData, currentClassName, propertyName)
end
local aliasData = propertyData.Kind.Alias
if aliasData ~= nil then
if propertyData.canonicalName ~= nil then
return PropertyDescriptor.fromRaw(
currentClass.properties[aliasData.AliasFor],
currentClass.properties[propertyData.canonicalName],
currentClassName,
aliasData.AliasFor)
propertyData.canonicalName)
end
return nil
end
currentClassName = currentClass.Superclass
currentClassName = currentClass.superclass
until currentClassName == nil
return nil

View File

@@ -1,4 +0,0 @@
#!/bin/sh
rojo build test-place.project.json -o TestPlace.rbxlx
run-in-roblox --script run-tests.lua --place TestPlace.rbxlx

View File

@@ -16,7 +16,7 @@
"$className": "ServerScriptService",
"Run Tests": {
"$path": "run-tests.lua"
"$path": "test.server.lua"
}
},
"Players": {

View File

@@ -0,0 +1,7 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LIB_ROOT = ReplicatedStorage.RbxDom
local TestEZ = require(ReplicatedStorage.TestEZ)
TestEZ.TestBootstrap:run({LIB_ROOT})

View File

@@ -205,7 +205,7 @@ function SettingsPage:render()
TwoWaySync = e(Setting, {
id = "twoWaySync",
name = "Two-Way Sync",
description = "Editing files in Studio will sync them into the filesystem",
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
transparency = self.props.transparency,
layoutOrder = 2,
}),

View File

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

View File

@@ -1,4 +1,5 @@
local StudioService = game:GetService("StudioService")
local RunService = game:GetService("RunService")
local Log = require(script.Parent.Parent.Log)
local Fmt = require(script.Parent.Parent.Fmt)
@@ -150,10 +151,18 @@ function ServeSession:__onActiveScriptChanged(activeScript)
Log.debug("Trying to open script {} externally...", activeScript)
-- Force-close the script inside Studio
local existingParent = activeScript.Parent
activeScript.Parent = nil
activeScript.Parent = existingParent
-- Force-close the script inside Studio... with a small delay in the middle
-- to prevent Studio from crashing.
spawn(function()
local existingParent = activeScript.Parent
activeScript.Parent = nil
for i = 1, 3 do
RunService.Heartbeat:Wait()
end
activeScript.Parent = existingParent
end)
-- Notify the Rojo server to open this script
self.__apiContext:open(scriptId)

View File

@@ -1,6 +1,7 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="Folder" referent="0">

View File

@@ -1,55 +0,0 @@
---
source: tests/tests/build.rs
expression: contents
---
<roblox version="4">
<Item class="DataModel" referent="0">
<Properties>
<string name="Name">unresolved-values</string>
</Properties>
<Item class="Lighting" referent="1">
<Properties>
<string name="Name">Lighting</string>
<Color3 name="Ambient">
<R>1</R>
<G>0</G>
<B>0</B>
</Color3>
<token name="Technology">1</token>
</Properties>
</Item>
<Item class="Workspace" referent="2">
<Properties>
<string name="Name">Workspace</string>
</Properties>
<Item class="BoolValue" referent="3">
<Properties>
<string name="Name">Bool</string>
<bool name="Value">true</bool>
</Properties>
</Item>
<Item class="Part" referent="4">
<Properties>
<string name="Name">Color</string>
<Color3 name="Color3uint8">
<R>0.5</R>
<G>0.25</G>
<B>0</B>
</Color3>
</Properties>
</Item>
<Item class="NumberValue" referent="5">
<Properties>
<string name="Name">Float</string>
<double name="Value">123.5</double>
</Properties>
</Item>
<Item class="IntValue" referent="6">
<Properties>
<string name="Name">Int</string>
<int64 name="Value">65</int64>
</Properties>
</Item>
</Item>
</Item>
</roblox>

View File

@@ -1,43 +0,0 @@
{
"name": "unresolved-values",
"tree": {
"$className": "DataModel",
"Lighting": {
"$properties": {
"Technology": "Voxel",
"Ambient": [1, 0, 0]
}
},
"Workspace": {
"Color": {
"$className": "Part",
"$properties": {
"Color": [0.5, 0.25, 0]
}
},
"Bool": {
"$className": "BoolValue",
"$properties": {
"Value": true
}
},
"Int": {
"$className": "IntValue",
"$properties": {
"Value": 65
}
},
"Float": {
"$className": "NumberValue",
"$properties": {
"Value": 123.5
}
}
}
}
}

View File

@@ -11,7 +11,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: add_folder
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children: []

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: add_folder
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
messageCursor: 0
sessionId: id-1

View File

@@ -1,12 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
projectName: add_folder
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: edit_init
Parent: "00000000000000000000000000000000"
Parent: ~
Properties:
Source:
Type: String

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: edit_init
Parent: "00000000000000000000000000000000"
Parent: ~
Properties:
Source:
Type: String

View File

@@ -1,12 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
projectName: edit_init
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: true
Name: empty
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
messageCursor: 0
sessionId: id-1

View File

@@ -1,12 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
projectName: empty
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -59,7 +59,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: move_folder_of_stuff
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children:

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: move_folder_of_stuff
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
messageCursor: 0
sessionId: id-1

View File

@@ -1,12 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
projectName: move_folder_of_stuff
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -10,7 +10,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: remove_file
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
messageCursor: 1
sessionId: id-1

View File

@@ -11,7 +11,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: remove_file
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children: []

View File

@@ -1,12 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
projectName: remove_file
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -12,7 +12,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: scripts
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children: []

View File

@@ -12,7 +12,7 @@ instances:
Metadata:
ignoreUnknownInstances: false
Name: scripts
Parent: "00000000000000000000000000000000"
Parent: ~
Properties: {}
id-3:
Children: []

View File

@@ -1,12 +1,10 @@
---
source: tests/tests/serve.rs
expression: redactions.redacted_yaml(info)
---
expectedPlaceIds: ~
projectName: scripts
protocolVersion: 4
protocolVersion: 3
rootInstanceId: id-2
serverVersion: "[server-version]"
sessionId: id-1

View File

@@ -6,7 +6,7 @@ use std::{
use crossbeam_channel::{select, Receiver, RecvError, Sender};
use jod_thread::JoinHandle;
use memofs::{IoResultExt, Vfs, VfsEvent};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use crate::{
message_queue::MessageQueue,
@@ -179,7 +179,7 @@ impl JobThreadContext {
InstigatingSource::Path(path) => fs::remove_file(path).unwrap(),
InstigatingSource::ProjectNode(_, _, _, _) => {
log::warn!(
"Cannot remove instance {:?}, it's from a project file",
"Cannot remove instance {}, it's from a project file",
id
);
}
@@ -187,12 +187,12 @@ impl JobThreadContext {
} else {
// TODO
log::warn!(
"Cannot remove instance {:?}, it is not an instigating source.",
"Cannot remove instance {}, it is not an instigating source.",
id
);
}
} else {
log::warn!("Cannot remove instance {:?}, it does not exist.", id);
log::warn!("Cannot remove instance {}, it does not exist.", id);
}
}
@@ -219,7 +219,7 @@ impl JobThreadContext {
{
match instigating_source {
InstigatingSource::Path(path) => {
if let Some(Variant::String(value)) = changed_value {
if let Some(RbxValue::String { value }) = changed_value {
fs::write(path, value).unwrap();
} else {
log::warn!("Cannot change Source to non-string value.");
@@ -227,14 +227,14 @@ impl JobThreadContext {
}
InstigatingSource::ProjectNode(_, _, _, _) => {
log::warn!(
"Cannot remove instance {:?}, it's from a project file",
"Cannot remove instance {}, it's from a project file",
id
);
}
}
} else {
log::warn!(
"Cannot update instance {:?}, it is not an instigating source.",
"Cannot update instance {}, it is not an instigating source.",
id
);
}
@@ -243,7 +243,7 @@ impl JobThreadContext {
}
}
} else {
log::warn!("Cannot update instance {:?}, it does not exist.", id);
log::warn!("Cannot update instance {}, it does not exist.", id);
}
}
@@ -254,7 +254,7 @@ impl JobThreadContext {
}
}
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<AppliedPatchSet> {
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Option<AppliedPatchSet> {
let metadata = tree
.get_metadata(id)
.expect("metadata missing for instance present in tree");
@@ -263,7 +263,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
Some(path) => path,
None => {
log::error!(
"Instance {:?} did not have an instigating source, but was considered for an update.",
"Instance {} did not have an instigating source, but was considered for an update.",
id
);
log::error!("This is a bug. Please file an issue!");

View File

@@ -88,7 +88,7 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Er
}
OutputKind::Rbxlx => {
// Place files don't contain an entry for the DataModel, but our
// WeakDom representation does.
// RbxTree representation does.
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
@@ -96,13 +96,17 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Er
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
}
OutputKind::Rbxm => {
rbx_binary::to_writer_default(&mut file, tree.inner(), &[root_id])?;
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
}
OutputKind::Rbxl => {
log::warn!("Support for building binary places (rbxl) is still experimental.");
log::warn!("Using the XML place format (rbxlx) is recommended instead.");
log::warn!("For more info, see https://github.com/rojo-rbx/rojo/issues/180");
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_binary::to_writer_default(&mut file, tree.inner(), top_level_ids)?;
rbx_binary::encode(tree.inner(), top_level_ids, &mut file)?;
}
}

View File

@@ -12,6 +12,7 @@ use std::{
env,
error::Error,
fmt,
net::IpAddr,
path::{Path, PathBuf},
str::FromStr,
};
@@ -186,7 +187,11 @@ pub struct ServeCommand {
#[structopt(default_value = "")]
pub project: PathBuf,
/// The port to listen on. Defaults to the project's preference, or 34872 if
/// The IP address to listen on. Defaults to `127.0.0.1`.
#[structopt(long)]
pub address: Option<IpAddr>,
/// The port to listen on. Defaults to the project's preference, or `34872` if
/// it has none.
#[structopt(long)]
pub port: Option<u16>,

View File

@@ -49,7 +49,7 @@ pub fn install_plugin() -> Result<()> {
let tree = session.tree();
let root_id = tree.get_root_id();
rbx_binary::to_writer_default(&mut file, tree.inner(), &[root_id])?;
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
Ok(())
}

View File

@@ -1,5 +1,7 @@
use std::{
io::{self, Write},
net::IpAddr,
net::Ipv4Addr,
sync::Arc,
};
@@ -13,6 +15,7 @@ use crate::{
web::LiveServer,
};
const DEFAULT_BIND_ADDRESS: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const DEFAULT_PORT: u16 = 34872;
pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
@@ -20,6 +23,8 @@ pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
let session = Arc::new(ServeSession::new(vfs, &options.absolute_project())?);
let ip = options.address.unwrap_or(DEFAULT_BIND_ADDRESS.into());
let port = options
.port
.or_else(|| session.project_port())
@@ -27,13 +32,13 @@ pub fn serve(global: GlobalOptions, options: ServeCommand) -> Result<()> {
let server = LiveServer::new(session);
let _ = show_start_message(port, global.color.into());
server.start(port);
let _ = show_start_message(ip, port, global.color.into());
server.start((ip, port).into());
Ok(())
}
fn show_start_message(port: u16, color: ColorChoice) -> io::Result<()> {
fn show_start_message(bind_address: IpAddr, port: u16, color: ColorChoice) -> io::Result<()> {
let writer = BufferWriter::stdout(color);
let mut buffer = writer.buffer();
@@ -41,7 +46,12 @@ fn show_start_message(port: u16, color: ColorChoice) -> io::Result<()> {
write!(&mut buffer, " Address: ")?;
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
writeln!(&mut buffer, "localhost")?;
if bind_address.is_loopback() {
writeln!(&mut buffer, "localhost")?;
} else {
writeln!(&mut buffer, "{}", bind_address)?;
}
buffer.set_color(&ColorSpec::new())?;
write!(&mut buffer, " Port: ")?;

View File

@@ -29,17 +29,21 @@ pub fn upload(options: UploadCommand) -> Result<(), anyhow::Error> {
let tree = session.tree();
let inner_tree = tree.inner();
let root = inner_tree.root();
let root_id = inner_tree.get_root_id();
let root_instance = inner_tree.get_instance(root_id).unwrap();
let encode_ids = match root.class.as_str() {
"DataModel" => root.children().to_vec(),
_ => vec![root.referent()],
let encode_ids = match root_instance.class_name.as_str() {
"DataModel" => root_instance.get_children_ids().to_vec(),
_ => vec![root_id],
};
let mut buffer = Vec::new();
log::trace!("Encoding binary model");
rbx_binary::to_writer_default(&mut buffer, tree.inner(), &encode_ids)?;
log::trace!("Encoding XML model");
let config = rbx_xml::EncodeOptions::new()
.property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown);
rbx_xml::to_writer(&mut buffer, tree.inner(), &encode_ids, config)?;
do_upload(buffer, options.asset_id, &cookie)
}

View File

@@ -15,7 +15,6 @@ mod message_queue;
mod multimap;
mod path_serializer;
mod project;
mod resolution;
mod serve_session;
mod session_id;
mod snapshot;

View File

@@ -4,10 +4,11 @@ use std::{
path::{Path, PathBuf},
};
use rbx_dom_weak::UnresolvedRbxValue;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{glob::Glob, resolution::UnresolvedValue};
use crate::glob::Glob;
static PROJECT_FILENAME: &str = "default.project.json";
@@ -182,7 +183,7 @@ pub struct ProjectNode {
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedValue>,
pub properties: HashMap<String, UnresolvedRbxValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered

View File

@@ -1,157 +0,0 @@
use anyhow::format_err;
use rbx_dom_weak::types::{
Color3, Color3uint8, Content, Enum, Variant, VariantType, Vector2, Vector3,
};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
#[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),
Number(f64),
Array2([f64; 2]),
Array3([f64; 3]),
Array4([f64; 4]),
}
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 sample_values = enum_descriptor
.items
.keys()
.take(3)
.map(|name| format!(r#""{}""#, name))
.collect::<Vec<_>>()
.join(", ");
format_err!(
"Invalid value for property {}.{}. Got {} but \
expected a member of the {} enum such as {}",
class_name,
prop_name,
what,
enum_name,
sample_values
)
};
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::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::Color3uint8, AmbiguousValue::Array3(value)) => {
let value = Color3uint8::new(
(value[0] / 255.0) as u8,
(value[1] / 255.0) as u8,
(value[2] / 255.0) as u8,
);
Ok(value.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::Number(_) => "a number",
AmbiguousValue::Array2(_) => "an array of two numbers",
AmbiguousValue::Array3(_) => "an array of three numbers",
AmbiguousValue::Array4(_) => "an array of four 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()?;
}
}

View File

@@ -10,6 +10,7 @@ use std::{
use crossbeam_channel::Sender;
use memofs::IoResultExt;
use memofs::Vfs;
use rbx_dom_weak::RbxInstanceProperties;
use thiserror::Error;
use crate::{
@@ -18,8 +19,8 @@ use crate::{
project::{Project, ProjectError},
session_id::SessionId,
snapshot::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
PatchSet, RojoTree,
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext,
InstancePropertiesWithMeta, PatchSet, RojoTree,
},
snapshot_middleware::snapshot_from_vfs,
};
@@ -117,7 +118,14 @@ impl ServeSession {
}
};
let mut tree = RojoTree::new(InstanceSnapshot::new());
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();

View File

@@ -2,10 +2,7 @@
use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{
types::{Ref, Variant},
WeakDom,
};
use rbx_dom_weak::{RbxId, RbxTree, RbxValue};
use serde::{Deserialize, Serialize};
use super::InstanceMetadata;
@@ -14,12 +11,11 @@ use super::InstanceMetadata;
///
// Possible future improvements:
// - Use refcounted/interned strings
// - Replace use of Variant with a sum of Variant + borrowed value
// - Replace use of RbxValue with a sum of RbxValue + borrowed value
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InstanceSnapshot {
// FIXME: Don't use Option<Ref> anymore!
/// A temporary ID applied to the snapshot that's used for Ref properties.
pub snapshot_id: Option<Ref>,
pub snapshot_id: Option<RbxId>,
/// Rojo-specific metadata associated with the instance.
pub metadata: InstanceMetadata,
@@ -31,7 +27,7 @@ pub struct InstanceSnapshot {
pub class_name: Cow<'static, str>,
/// All other properties of the instance, weakly-typed.
pub properties: HashMap<String, Variant>,
pub properties: HashMap<String, RbxValue>,
/// The children of the instance represented as more snapshots.
///
@@ -65,16 +61,7 @@ impl InstanceSnapshot {
}
}
pub fn property<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<Variant>,
{
self.properties.insert(key.into(), value.into());
self
}
pub fn properties(self, properties: impl Into<HashMap<String, Variant>>) -> Self {
pub fn properties(self, properties: impl Into<HashMap<String, RbxValue>>) -> Self {
Self {
properties: properties.into(),
..self
@@ -88,13 +75,6 @@ impl InstanceSnapshot {
}
}
pub fn snapshot_id(self, snapshot_id: Option<Ref>) -> Self {
Self {
snapshot_id,
..self
}
}
pub fn metadata(self, metadata: impl Into<InstanceMetadata>) -> Self {
Self {
metadata: metadata.into(),
@@ -102,13 +82,15 @@ impl InstanceSnapshot {
}
}
pub fn from_tree(tree: &WeakDom, id: Ref) -> Self {
let instance = tree.get_by_ref(id).expect("instance did not exist in tree");
pub fn from_tree(tree: &RbxTree, id: RbxId) -> Self {
let instance = tree
.get_instance(id)
.expect("instance did not exist in tree");
let children = instance
.children()
.get_children_ids()
.iter()
.copied()
.cloned()
.map(|id| Self::from_tree(tree, id))
.collect();
@@ -116,7 +98,7 @@ impl InstanceSnapshot {
snapshot_id: Some(id),
metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name.clone()),
class_name: Cow::Owned(instance.class.clone()),
class_name: Cow::Owned(instance.class_name.clone()),
properties: instance.properties.clone(),
children,
}

View File

@@ -2,19 +2,19 @@
use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use serde::{Deserialize, Serialize};
use super::{InstanceMetadata, InstanceSnapshot};
/// A set of different kinds of patches that can be applied to an WeakDom.
/// A set of different kinds of patches that can be applied to an RbxTree.
///
/// These patches shouldn't be persisted: there's no mechanism in place to make
/// sure that another patch wasn't applied before this one that could cause a
/// conflict!
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchSet {
pub removed_instances: Vec<Ref>,
pub removed_instances: Vec<RbxId>,
pub added_instances: Vec<PatchAdd>,
pub updated_instances: Vec<PatchUpdate>,
}
@@ -32,20 +32,20 @@ impl<'a> PatchSet {
/// A patch containing an instance that was added to the tree.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchAdd {
pub parent_id: Ref,
pub parent_id: RbxId,
pub instance: InstanceSnapshot,
}
/// A patch indicating that properties of an instance changed.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchUpdate {
pub id: Ref,
pub id: RbxId,
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
/// Contains all changed properties. If a property is assigned to `None`,
/// then that property has been removed.
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_properties: HashMap<String, Option<RbxValue>>,
/// Changed Rojo-specific metadata, if any of it changed.
pub changed_metadata: Option<InstanceMetadata>,
@@ -63,8 +63,8 @@ pub struct PatchUpdate {
// current values in all fields.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AppliedPatchSet {
pub removed: Vec<Ref>,
pub added: Vec<Ref>,
pub removed: Vec<RbxId>,
pub added: Vec<RbxId>,
pub updated: Vec<AppliedPatchUpdate>,
}
@@ -80,17 +80,17 @@ impl AppliedPatchSet {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedPatchUpdate {
pub id: Ref,
pub id: RbxId,
// TODO: Store previous values in order to detect application conflicts
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_properties: HashMap<String, Option<RbxValue>>,
pub changed_metadata: Option<InstanceMetadata>,
}
impl AppliedPatchUpdate {
pub fn new(id: Ref) -> Self {
pub fn new(id: RbxId) -> Self {
Self {
id,
changed_name: None,

View File

@@ -2,11 +2,11 @@
use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxInstanceProperties, RbxValue};
use super::{
patch::{AppliedPatchSet, AppliedPatchUpdate, PatchSet, PatchUpdate},
InstanceSnapshot, RojoTree,
InstancePropertiesWithMeta, InstanceSnapshot, RojoTree,
};
/// Consumes the input `PatchSet`, applying all of its prescribed changes to the
@@ -37,7 +37,7 @@ pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatch
struct PatchApplyContext {
/// A map from transient snapshot IDs (generated by snapshot middleware) to
/// instance IDs in the actual tree. These are both the same data type so
/// that they fit into the same `Variant::Ref` type.
/// that they fit into the same `RbxValue::Ref` type.
///
/// At this point in the patch process, IDs in instance properties have been
/// partially translated from 'snapshot space' into 'tree space' by the
@@ -53,7 +53,7 @@ struct PatchApplyContext {
/// #2 should not occur in well-formed projects, but is indistinguishable
/// from #1 right now. It could happen if two model files try to reference
/// eachother.
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
snapshot_id_to_instance_id: HashMap<RbxId, RbxId>,
/// The properties of instances added by the current `PatchSet`.
///
@@ -68,7 +68,7 @@ struct PatchApplyContext {
///
/// 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>>,
added_instance_properties: HashMap<RbxId, HashMap<String, RbxValue>>,
/// The current applied patch result, describing changes made to the tree.
applied_patch_set: AppliedPatchSet,
@@ -93,10 +93,11 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
.expect("Invalid instance ID in deferred property map");
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)
{
property_value = Variant::Ref(instance_referent);
if let RbxValue::Ref { value: Some(id) } = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(&id) {
property_value = RbxValue::Ref {
value: Some(instance_id),
};
}
}
@@ -107,40 +108,50 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
context.applied_patch_set
}
fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: Ref) {
tree.remove(removed_id);
context.applied_patch_set.removed.push(removed_id);
fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: RbxId) {
match tree.remove_instance(removed_id) {
Some(_) => context.applied_patch_set.removed.push(removed_id),
None => {
log::warn!(
"Patch misapplication: Tried to remove instance {} but it did not exist.",
removed_id
);
}
}
}
fn apply_add_child(
context: &mut PatchApplyContext,
tree: &mut RojoTree,
parent_id: Ref,
parent_id: RbxId,
snapshot: InstanceSnapshot,
) {
let snapshot_id = snapshot.snapshot_id;
let properties = snapshot.properties;
let children = snapshot.children;
let properties = InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: snapshot.name.into_owned(),
class_name: snapshot.class_name.into_owned(),
// 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);
// Property assignment is deferred until after we know about all
// instances in this patch. See `PatchApplyContext` for details.
properties: HashMap::new(),
},
metadata: snapshot.metadata,
};
let id = tree.insert_instance(properties, parent_id);
let id = tree.insert_instance(parent_id, remaining_snapshot);
context.applied_patch_set.added.push(id);
context.added_instance_properties.insert(id, properties);
context
.added_instance_properties
.insert(id, snapshot.properties);
if let Some(snapshot_id) = snapshot_id {
if let Some(snapshot_id) = snapshot.snapshot_id {
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
}
for child in children {
apply_add_child(context, tree, id, child);
for child_snapshot in snapshot.children {
apply_add_child(context, tree, id, child_snapshot);
}
}
@@ -156,7 +167,7 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
Some(instance) => instance,
None => {
log::warn!(
"Patch misapplication: Instance {:?}, referred to by update patch, did not exist.",
"Patch misapplication: Instance {}, referred to by update patch, did not exist.",
patch.id
);
return;
@@ -178,24 +189,23 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
// Ref values need to be potentially rewritten from snapshot IDs to
// instance IDs if they referred to an instance that was created as
// part of this patch.
Some(Variant::Ref(referent)) => {
if referent.is_none() {
continue;
}
Some(RbxValue::Ref { value: Some(id) }) => {
// If our ID is not found in this map, then it either refers to
// an existing instance NOT added by this patch, or there was an
// error. See `PatchApplyContext::snapshot_id_to_instance_id`
// for more info.
let new_referent = context
let new_id = context
.snapshot_id_to_instance_id
.get(&referent)
.get(&id)
.copied()
.unwrap_or(referent);
.unwrap_or(id);
instance
.properties_mut()
.insert(key.clone(), Variant::Ref(new_referent));
instance.properties_mut().insert(
key.clone(),
RbxValue::Ref {
value: Some(new_id),
},
);
}
Some(ref value) => {
instance.properties_mut().insert(key.clone(), value.clone());
@@ -215,10 +225,10 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
mod test {
use super::*;
use std::borrow::Cow;
use std::{borrow::Cow, collections::HashMap};
use maplit::hashmap;
use rbx_dom_weak::types::Variant;
use rbx_dom_weak::RbxValue;
use super::super::PatchAdd;
@@ -226,7 +236,14 @@ mod test {
fn add_from_empty() {
let _ = env_logger::try_init();
let mut tree = RojoTree::new(InstanceSnapshot::new());
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "Folder".to_owned(),
class_name: "Folder".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
@@ -236,7 +253,7 @@ mod test {
name: Cow::Borrowed("Foo"),
class_name: Cow::Borrowed("Bar"),
properties: hashmap! {
"Baz".to_owned() => Variant::Int32(5),
"Baz".to_owned() => RbxValue::Int32 { value: 5 },
},
children: Vec::new(),
};
@@ -265,14 +282,18 @@ mod test {
fn update_existing() {
let _ = env_logger::try_init();
let mut tree = RojoTree::new(
InstanceSnapshot::new()
.class_name("OldClassName")
.name("OldName")
.property("Foo", 7i32)
.property("Bar", 3i32)
.property("Unchanged", -5i32),
);
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "OldName".to_owned(),
class_name: "OldClassName".to_owned(),
properties: hashmap! {
"Foo".to_owned() => RbxValue::Int32 { value: 7 },
"Bar".to_owned() => RbxValue::Int32 { value: 3 },
"Unchanged".to_owned() => RbxValue::Int32 { value: -5 },
},
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
@@ -282,13 +303,13 @@ mod test {
changed_class_name: Some("NewClassName".to_owned()),
changed_properties: hashmap! {
// The value of Foo has changed
"Foo".to_owned() => Some(Variant::Int32(8)),
"Foo".to_owned() => Some(RbxValue::Int32 { value: 8 }),
// Bar has been deleted
"Bar".to_owned() => None,
// Baz has been added
"Baz".to_owned() => Some(Variant::Int32(10)),
"Baz".to_owned() => Some(RbxValue::Int32 { value: 10 }),
},
changed_metadata: None,
};
@@ -301,9 +322,9 @@ mod test {
apply_patch_set(&mut tree, patch_set);
let expected_properties = hashmap! {
"Foo".to_owned() => Variant::Int32(8),
"Baz".to_owned() => Variant::Int32(10),
"Unchanged".to_owned() => Variant::Int32(-5),
"Foo".to_owned() => RbxValue::Int32 { value: 8 },
"Baz".to_owned() => RbxValue::Int32 { value: 10 },
"Unchanged".to_owned() => RbxValue::Int32 { value: -5 },
};
let root_instance = tree.get_instance(root_id).unwrap();

View File

@@ -3,14 +3,14 @@
use std::collections::{HashMap, HashSet};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use super::{
patch::{PatchAdd, PatchSet, PatchUpdate},
InstanceSnapshot, InstanceWithMeta, RojoTree,
};
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref) -> PatchSet {
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: RbxId) -> PatchSet {
let mut patch_set = PatchSet::new();
let mut context = ComputePatchContext::default();
@@ -26,15 +26,17 @@ pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref)
#[derive(Default)]
struct ComputePatchContext {
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
snapshot_id_to_instance_id: HashMap<RbxId, RbxId>,
}
fn rewrite_refs_in_updates(context: &ComputePatchContext, updates: &mut [PatchUpdate]) {
for update in updates {
for property_value in update.changed_properties.values_mut() {
if let Some(Variant::Ref(referent)) = property_value {
if let Some(&instance_ref) = context.snapshot_id_to_instance_id.get(referent) {
*property_value = Some(Variant::Ref(instance_ref));
if let Some(RbxValue::Ref { value: Some(id) }) = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(id) {
*property_value = Some(RbxValue::Ref {
value: Some(instance_id),
});
}
}
}
@@ -49,9 +51,11 @@ fn rewrite_refs_in_additions(context: &ComputePatchContext, additions: &mut [Pat
fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut InstanceSnapshot) {
for property_value in snapshot.properties.values_mut() {
if let Variant::Ref(referent) = property_value {
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(referent) {
*property_value = Variant::Ref(instance_referent);
if let RbxValue::Ref { value: Some(id) } = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(id) {
*property_value = RbxValue::Ref {
value: Some(instance_id),
};
}
}
}
@@ -65,7 +69,7 @@ fn compute_patch_set_internal(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
tree: &RojoTree,
id: Ref,
id: RbxId,
patch_set: &mut PatchSet,
) {
if let Some(snapshot_id) = snapshot.snapshot_id {
@@ -150,7 +154,7 @@ fn compute_children_patches(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
tree: &RojoTree,
id: Ref,
id: RbxId,
patch_set: &mut PatchSet,
) {
let instance = tree
@@ -220,6 +224,9 @@ mod test {
use std::borrow::Cow;
use maplit::hashmap;
use rbx_dom_weak::RbxInstanceProperties;
use super::super::InstancePropertiesWithMeta;
/// This test makes sure that rewriting refs in instance update patches to
/// instances that already exists works. We should be able to correlate the
@@ -227,17 +234,26 @@ mod test {
/// value before returning from compute_patch_set.
#[test]
fn rewrite_ref_existing_instance_update() {
let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo"));
let tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "foo".to_owned(),
class_name: "foo".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
// This snapshot should be identical to the existing tree except for the
// addition of a prop named Self, which is a self-referential Ref.
let snapshot_id = Ref::new();
let snapshot_id = RbxId::new();
let snapshot = InstanceSnapshot {
snapshot_id: Some(snapshot_id),
properties: hashmap! {
"Self".to_owned() => Variant::Ref(snapshot_id),
"Self".to_owned() => RbxValue::Ref {
value: Some(snapshot_id),
}
},
metadata: Default::default(),
@@ -254,7 +270,9 @@ mod test {
changed_name: None,
changed_class_name: None,
changed_properties: hashmap! {
"Self".to_owned() => Some(Variant::Ref(root_id)),
"Self".to_owned() => Some(RbxValue::Ref {
value: Some(root_id),
}),
},
changed_metadata: None,
}],
@@ -270,17 +288,26 @@ mod test {
/// one.
#[test]
fn rewrite_ref_existing_instance_addition() {
let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo"));
let tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "foo".to_owned(),
class_name: "foo".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let root_id = tree.get_root_id();
// This patch describes the existing instance with a new child added.
let snapshot_id = Ref::new();
let snapshot_id = RbxId::new();
let snapshot = InstanceSnapshot {
snapshot_id: Some(snapshot_id),
children: vec![InstanceSnapshot {
properties: hashmap! {
"Self".to_owned() => Variant::Ref(snapshot_id),
"Self".to_owned() => RbxValue::Ref {
value: Some(snapshot_id),
},
},
snapshot_id: None,
@@ -305,7 +332,9 @@ mod test {
snapshot_id: None,
metadata: Default::default(),
properties: hashmap! {
"Self".to_owned() => Variant::Ref(root_id),
"Self".to_owned() => RbxValue::Ref {
value: Some(root_id),
},
},
name: Cow::Borrowed("child"),
class_name: Cow::Borrowed("child"),

View File

@@ -1,10 +1,11 @@
use insta::assert_yaml_snapshot;
use maplit::hashmap;
use rbx_dom_weak::{RbxInstanceProperties, RbxValue};
use rojo_insta_ext::RedactionMap;
use crate::{
snapshot::{apply_patch_set, InstanceSnapshot, PatchSet, PatchUpdate, RojoTree},
snapshot::{apply_patch_set, InstancePropertiesWithMeta, PatchSet, PatchUpdate, RojoTree},
tree_view::{intern_tree, view_tree},
};
@@ -48,7 +49,9 @@ fn add_property() {
changed_name: None,
changed_class_name: None,
changed_properties: hashmap! {
"Foo".to_owned() => Some("Value of Foo".into()),
"Foo".to_owned() => Some(RbxValue::String {
value: "Value of Foo".to_owned(),
}),
},
changed_metadata: None,
}],
@@ -75,9 +78,12 @@ fn remove_property() {
let root_id = tree.get_root_id();
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
root_instance
.properties_mut()
.insert("Foo".to_owned(), "Should be removed".into());
root_instance.properties_mut().insert(
"Foo".to_owned(),
RbxValue::String {
value: "Should be removed".to_owned(),
},
);
}
let tree_view = view_tree(&tree, &mut redactions);
@@ -106,5 +112,12 @@ fn remove_property() {
}
fn empty_tree() -> RojoTree {
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "ROOT".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
})
}

View File

@@ -2,10 +2,11 @@ use std::borrow::Cow;
use insta::assert_yaml_snapshot;
use maplit::hashmap;
use rbx_dom_weak::{RbxInstanceProperties, RbxValue};
use rojo_insta_ext::RedactionMap;
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
use crate::snapshot::{compute_patch_set, InstancePropertiesWithMeta, InstanceSnapshot, RojoTree};
#[test]
fn set_name_and_class_name() {
@@ -42,7 +43,9 @@ fn set_property() {
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
properties: hashmap! {
"PropertyName".to_owned() => "Hello, world!".into(),
"PropertyName".to_owned() => RbxValue::String {
value: "Hello, world!".to_owned(),
},
},
children: Vec::new(),
};
@@ -65,7 +68,9 @@ fn remove_property() {
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
root_instance.properties_mut().insert(
"Foo".to_owned(),
"This should be removed by the patch.".into(),
RbxValue::String {
value: "This should be removed by the patch.".to_owned(),
},
);
}
@@ -123,8 +128,15 @@ fn remove_child() {
{
let root_id = tree.get_root_id();
let new_id = tree.insert_instance(
InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "Should not appear in snapshot".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
},
root_id,
InstanceSnapshot::new().name("Should not appear in snapshot"),
);
redactions.intern(new_id);
@@ -146,5 +158,12 @@ fn remove_child() {
}
fn empty_tree() -> RojoTree {
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "ROOT".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
})
}

View File

@@ -1,29 +1,26 @@
use std::{
collections::{HashMap, VecDeque},
collections::HashMap,
path::{Path, PathBuf},
};
use rbx_dom_weak::{
types::{Ref, Variant},
Instance, InstanceBuilder, WeakDom,
};
use rbx_dom_weak::{Descendants, RbxId, RbxInstance, RbxInstanceProperties, RbxTree, RbxValue};
use crate::multimap::MultiMap;
use super::{InstanceMetadata, InstanceSnapshot};
use super::InstanceMetadata;
/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
/// An expanded variant of rbx_dom_weak's `RbxTree` that tracks additional
/// metadata per instance that's Rojo-specific.
///
/// This tree is also optimized for doing fast incremental updates and patches.
#[derive(Debug)]
pub struct RojoTree {
/// Contains the instances without their Rojo-specific metadata.
inner: WeakDom,
inner: RbxTree,
/// Metadata associated with each instance that is kept up-to-date with the
/// set of actual instances.
metadata_map: HashMap<Ref, InstanceMetadata>,
metadata_map: HashMap<RbxId, InstanceMetadata>,
/// A multimap from source paths to all of the root instances that were
/// constructed from that path.
@@ -32,42 +29,31 @@ pub struct RojoTree {
/// value portion of the map is also a set in order to support the same path
/// appearing multiple times in the same Rojo project. This is sometimes
/// called "path aliasing" in various Rojo documentation.
path_to_ids: MultiMap<PathBuf, Ref>,
path_to_ids: MultiMap<PathBuf, RbxId>,
}
impl RojoTree {
pub fn new(snapshot: InstanceSnapshot) -> RojoTree {
let root_builder = InstanceBuilder::new(snapshot.class_name.to_owned())
.with_name(snapshot.name.to_owned())
.with_properties(snapshot.properties);
pub fn new(root: InstancePropertiesWithMeta) -> RojoTree {
let mut tree = RojoTree {
inner: WeakDom::new(root_builder),
inner: RbxTree::new(root.properties),
metadata_map: HashMap::new(),
path_to_ids: MultiMap::new(),
};
let root_ref = tree.inner.root_ref();
tree.insert_metadata(root_ref, snapshot.metadata);
for child in snapshot.children {
tree.insert_instance(root_ref, child);
}
tree.insert_metadata(tree.inner.get_root_id(), root.metadata);
tree
}
pub fn inner(&self) -> &WeakDom {
pub fn inner(&self) -> &RbxTree {
&self.inner
}
pub fn get_root_id(&self) -> Ref {
self.inner.root_ref()
pub fn get_root_id(&self) -> RbxId {
self.inner.get_root_id()
}
pub fn get_instance(&self, id: Ref) -> Option<InstanceWithMeta> {
if let Some(instance) = self.inner.get_by_ref(id) {
pub fn get_instance(&self, id: RbxId) -> Option<InstanceWithMeta> {
if let Some(instance) = self.inner.get_instance(id) {
let metadata = self.metadata_map.get(&id).unwrap();
Some(InstanceWithMeta { instance, metadata })
@@ -76,8 +62,8 @@ impl RojoTree {
}
}
pub fn get_instance_mut(&mut self, id: Ref) -> Option<InstanceWithMetaMut> {
if let Some(instance) = self.inner.get_by_ref_mut(id) {
pub fn get_instance_mut(&mut self, id: RbxId) -> Option<InstanceWithMetaMut> {
if let Some(instance) = self.inner.get_instance_mut(id) {
let metadata = self.metadata_map.get_mut(&id).unwrap();
Some(InstanceWithMetaMut { instance, metadata })
@@ -86,38 +72,38 @@ impl RojoTree {
}
}
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
let builder = InstanceBuilder::new(snapshot.class_name.to_owned())
.with_name(snapshot.name.to_owned())
.with_properties(snapshot.properties);
let referent = self.inner.insert(parent_ref, builder);
self.insert_metadata(referent, snapshot.metadata);
for child in snapshot.children {
self.insert_instance(referent, child);
}
referent
pub fn insert_instance(
&mut self,
properties: InstancePropertiesWithMeta,
parent_id: RbxId,
) -> RbxId {
let id = self.inner.insert_instance(properties.properties, parent_id);
self.insert_metadata(id, properties.metadata);
id
}
pub fn remove(&mut self, id: Ref) {
let mut to_move = VecDeque::new();
to_move.push_back(id);
pub fn remove_instance(&mut self, id: RbxId) -> Option<RojoTree> {
if let Some(inner) = self.inner.remove_instance(id) {
let mut metadata_map = HashMap::new();
let mut path_to_ids = MultiMap::new();
while let Some(id) = to_move.pop_front() {
self.remove_metadata(id);
if let Some(instance) = self.inner.get_by_ref(id) {
to_move.extend(instance.children().iter().copied());
self.move_metadata(id, &mut metadata_map, &mut path_to_ids);
for instance in inner.descendants(id) {
self.move_metadata(instance.get_id(), &mut metadata_map, &mut path_to_ids);
}
}
self.inner.destroy(id);
Some(RojoTree {
inner,
metadata_map,
path_to_ids,
})
} else {
None
}
}
/// Replaces the metadata associated with the given instance ID.
pub fn update_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
pub fn update_metadata(&mut self, id: RbxId, metadata: InstanceMetadata) {
use std::collections::hash_map::Entry;
match self.metadata_map.entry(id) {
@@ -145,22 +131,22 @@ impl RojoTree {
}
}
pub fn descendants(&self, id: Ref) -> RojoDescendants<'_> {
let mut queue = VecDeque::new();
queue.push_back(id);
RojoDescendants { queue, tree: self }
pub fn descendants(&self, id: RbxId) -> RojoDescendants<'_> {
RojoDescendants {
inner: self.inner.descendants(id),
tree: self,
}
}
pub fn get_ids_at_path(&self, path: &Path) -> &[Ref] {
pub fn get_ids_at_path(&self, path: &Path) -> &[RbxId] {
self.path_to_ids.get(path)
}
pub fn get_metadata(&self, id: Ref) -> Option<&InstanceMetadata> {
pub fn get_metadata(&self, id: RbxId) -> Option<&InstanceMetadata> {
self.metadata_map.get(&id)
}
fn insert_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
fn insert_metadata(&mut self, id: RbxId, metadata: InstanceMetadata) {
for path in &metadata.relevant_paths {
self.path_to_ids.insert(path.clone(), id);
}
@@ -170,17 +156,25 @@ impl RojoTree {
/// Moves the Rojo metadata from the instance with the given ID from this
/// tree into some loose maps.
fn remove_metadata(&mut self, id: Ref) {
fn move_metadata(
&mut self,
id: RbxId,
metadata_map: &mut HashMap<RbxId, InstanceMetadata>,
path_to_ids: &mut MultiMap<PathBuf, RbxId>,
) {
let metadata = self.metadata_map.remove(&id).unwrap();
for path in &metadata.relevant_paths {
self.path_to_ids.remove(path, id);
path_to_ids.insert(path.clone(), id);
}
metadata_map.insert(id, metadata);
}
}
pub struct RojoDescendants<'a> {
queue: VecDeque<Ref>,
inner: Descendants<'a>,
tree: &'a RojoTree,
}
@@ -188,43 +182,50 @@ impl<'a> Iterator for RojoDescendants<'a> {
type Item = InstanceWithMeta<'a>;
fn next(&mut self) -> Option<Self::Item> {
let id = self.queue.pop_front()?;
let instance = self
.tree
.inner
.get_by_ref(id)
.expect("Instance did not exist");
let instance = self.inner.next()?;
let metadata = self
.tree
.get_metadata(instance.referent())
.get_metadata(instance.get_id())
.expect("Metadata did not exist for instance");
self.queue.extend(instance.children().iter().copied());
Some(InstanceWithMeta { instance, metadata })
}
}
/// RojoTree's equivalent of `&'a Instance`.
/// RojoTree's equivalent of `RbxInstanceProperties`.
#[derive(Debug, Clone)]
pub struct InstancePropertiesWithMeta {
pub properties: RbxInstanceProperties,
pub metadata: InstanceMetadata,
}
impl InstancePropertiesWithMeta {
pub fn new(properties: RbxInstanceProperties, metadata: InstanceMetadata) -> Self {
InstancePropertiesWithMeta {
properties,
metadata,
}
}
}
/// RojoTree's equivalent of `&'a RbxInstance`.
///
/// This has to be a value type for RojoTree because the instance and metadata
/// are stored in different places. The mutable equivalent is
/// `InstanceWithMetaMut`.
#[derive(Debug, Clone, Copy)]
pub struct InstanceWithMeta<'a> {
instance: &'a Instance,
instance: &'a RbxInstance,
metadata: &'a InstanceMetadata,
}
impl<'a> InstanceWithMeta<'a> {
pub fn id(&self) -> Ref {
self.instance.referent()
pub fn id(&self) -> RbxId {
self.instance.get_id()
}
pub fn parent(&self) -> Ref {
self.instance.parent()
pub fn parent(&self) -> Option<RbxId> {
self.instance.get_parent_id()
}
pub fn name(&self) -> &'a str {
@@ -232,15 +233,15 @@ impl<'a> InstanceWithMeta<'a> {
}
pub fn class_name(&self) -> &'a str {
&self.instance.class
&self.instance.class_name
}
pub fn properties(&self) -> &'a HashMap<String, Variant> {
pub fn properties(&self) -> &'a HashMap<String, RbxValue> {
&self.instance.properties
}
pub fn children(&self) -> &'a [Ref] {
self.instance.children()
pub fn children(&self) -> &'a [RbxId] {
self.instance.get_children_ids()
}
pub fn metadata(&self) -> &'a InstanceMetadata {
@@ -248,20 +249,20 @@ impl<'a> InstanceWithMeta<'a> {
}
}
/// RojoTree's equivalent of `&'a mut Instance`.
/// RojoTree's equivalent of `&'a mut RbxInstance`.
///
/// This has to be a value type for RojoTree because the instance and metadata
/// are stored in different places. The immutable equivalent is
/// `InstanceWithMeta`.
#[derive(Debug)]
pub struct InstanceWithMetaMut<'a> {
instance: &'a mut Instance,
instance: &'a mut RbxInstance,
metadata: &'a mut InstanceMetadata,
}
impl InstanceWithMetaMut<'_> {
pub fn id(&self) -> Ref {
self.instance.referent()
pub fn id(&self) -> RbxId {
self.instance.get_id()
}
pub fn name(&self) -> &str {
@@ -273,23 +274,23 @@ impl InstanceWithMetaMut<'_> {
}
pub fn class_name(&self) -> &str {
&self.instance.class
&self.instance.class_name
}
pub fn class_name_mut(&mut self) -> &mut String {
&mut self.instance.class
&mut self.instance.class_name
}
pub fn properties(&self) -> &HashMap<String, Variant> {
pub fn properties(&self) -> &HashMap<String, RbxValue> {
&self.instance.properties
}
pub fn properties_mut(&mut self) -> &mut HashMap<String, Variant> {
pub fn properties_mut(&mut self) -> &mut HashMap<String, RbxValue> {
&mut self.instance.properties
}
pub fn children(&self) -> &[Ref] {
self.instance.children()
pub fn children(&self) -> &[RbxId] {
self.instance.get_children_ids()
}
pub fn metadata(&self) -> &InstanceMetadata {

View File

@@ -3,6 +3,7 @@ use std::{collections::BTreeMap, path::Path};
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use serde::Serialize;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -29,7 +30,9 @@ pub fn snapshot_csv(
.name(instance_name)
.class_name("LocalizationTable")
.properties(hashmap! {
"Contents".to_owned() => table_contents.into(),
"Contents".to_owned() => RbxValue::String {
value: table_contents,
},
})
.metadata(
InstanceMetadata::new()
@@ -38,8 +41,8 @@ pub fn snapshot_csv(
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
Ok(Some(snapshot))

View File

@@ -60,8 +60,8 @@ pub fn snapshot_dir(context: &InstanceContext, vfs: &Vfs, path: &Path) -> Snapsh
);
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)?;
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
Ok(Some(snapshot))

View File

@@ -3,6 +3,7 @@ use std::path::Path;
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use crate::{
lua_ast::{Expression, Statement},
@@ -25,7 +26,9 @@ pub fn snapshot_json(
let as_lua = json_to_lua(value).to_string();
let properties = hashmap! {
"Source".to_owned() => as_lua.into(),
"Source".to_owned() => RbxValue::String {
value: as_lua,
},
};
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
@@ -42,8 +45,8 @@ pub fn snapshot_json(
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
Ok(Some(snapshot))

View File

@@ -2,12 +2,11 @@ use std::{borrow::Cow, collections::HashMap, path::Path};
use anyhow::Context;
use memofs::Vfs;
use rbx_dom_weak::UnresolvedRbxValue;
use rbx_reflection::try_resolve_value;
use serde::Deserialize;
use crate::{
resolution::UnresolvedValue,
snapshot::{InstanceContext, InstanceSnapshot},
};
use crate::snapshot::{InstanceContext, InstanceSnapshot};
use super::middleware::SnapshotInstanceResult;
@@ -21,10 +20,7 @@ pub fn snapshot_json_model(
let instance: JsonModel = serde_json::from_slice(&contents)
.with_context(|| format!("File is not a valid JSON model: {}", path.display()))?;
let mut snapshot = instance
.core
.into_snapshot(instance_name.to_owned())
.with_context(|| format!("Could not load JSON model: {}", path.display()))?;
let mut snapshot = instance.core.into_snapshot(instance_name.to_owned());
snapshot.metadata = snapshot
.metadata
@@ -62,32 +58,36 @@ struct JsonModelCore {
children: Vec<JsonModelInstance>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
properties: HashMap<String, UnresolvedValue>,
properties: HashMap<String, UnresolvedRbxValue>,
}
impl JsonModelCore {
fn into_snapshot(self, name: String) -> anyhow::Result<InstanceSnapshot> {
fn into_snapshot(self, name: String) -> InstanceSnapshot {
let class_name = self.class_name;
let mut children = Vec::with_capacity(self.children.len());
for child in self.children {
children.push(child.core.into_snapshot(child.name)?);
}
let children = self
.children
.into_iter()
.map(|child| child.core.into_snapshot(child.name))
.collect();
let mut properties = HashMap::with_capacity(self.properties.len());
for (key, unresolved) in self.properties {
let value = unresolved.resolve(&class_name, &key)?;
properties.insert(key, value);
}
let properties = self
.properties
.into_iter()
.map(|(key, value)| {
try_resolve_value(&class_name, &key, &value).map(|resolved| (key, resolved))
})
.collect::<Result<HashMap<_, _>, _>>()
.expect("TODO: Handle rbx_reflection errors");
Ok(InstanceSnapshot {
InstanceSnapshot {
snapshot_id: None,
metadata: Default::default(),
name: Cow::Owned(name),
class_name: Cow::Owned(class_name),
properties,
children,
})
}
}
}

View File

@@ -3,6 +3,7 @@ use std::{path::Path, str};
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -37,7 +38,9 @@ pub fn snapshot_lua(context: &InstanceContext, vfs: &Vfs, path: &Path) -> Snapsh
.name(instance_name)
.class_name(class_name)
.properties(hashmap! {
"Source".to_owned() => contents_str.into(),
"Source".to_owned() => RbxValue::String {
value: contents_str,
},
})
.metadata(
InstanceMetadata::new()
@@ -47,8 +50,8 @@ pub fn snapshot_lua(context: &InstanceContext, vfs: &Vfs, path: &Path) -> Snapsh
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
Ok(Some(snapshot))

View File

@@ -1,9 +1,11 @@
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use std::{borrow::Cow, collections::HashMap, path::Path};
use anyhow::{format_err, Context};
use anyhow::Context;
use rbx_dom_weak::UnresolvedRbxValue;
use rbx_reflection::try_resolve_value;
use serde::{Deserialize, Serialize};
use crate::{resolution::UnresolvedValue, snapshot::InstanceSnapshot};
use crate::snapshot::InstanceSnapshot;
/// Represents metadata in a sibling file with the same basename.
///
@@ -16,23 +18,17 @@ pub struct AdjacentMetadata {
pub ignore_unknown_instances: Option<bool>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<String, UnresolvedValue>,
#[serde(skip)]
pub path: PathBuf,
pub properties: HashMap<String, UnresolvedRbxValue>,
}
impl AdjacentMetadata {
pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result<Self> {
let mut meta: Self = serde_json::from_slice(slice).with_context(|| {
pub fn from_slice(slice: &[u8], path: &Path) -> anyhow::Result<Self> {
serde_json::from_slice(slice).with_context(|| {
format!(
"File contained malformed .meta.json data: {}",
path.display()
)
})?;
meta.path = path;
Ok(meta)
})
}
pub fn apply_ignore_unknown_instances(&mut self, snapshot: &mut InstanceSnapshot) {
@@ -41,24 +37,23 @@ impl AdjacentMetadata {
}
}
pub fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
let path = &self.path;
pub fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) {
let class_name = &snapshot.class_name;
for (key, unresolved) in self.properties.drain() {
let value = unresolved
.resolve(&snapshot.class_name, &key)
.with_context(|| format!("error applying meta file {}", path.display()))?;
let source_properties = self.properties.drain().map(|(key, value)| {
try_resolve_value(class_name, &key, &value)
.map(|resolved| (key, resolved))
.expect("TODO: Handle rbx_reflection errors")
});
for (key, value) in source_properties {
snapshot.properties.insert(key, value);
}
Ok(())
}
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) {
self.apply_ignore_unknown_instances(snapshot);
self.apply_properties(snapshot)?;
Ok(())
self.apply_properties(snapshot);
}
// TODO: Add method to allow selectively applying parts of metadata and
@@ -76,50 +71,37 @@ pub struct DirectoryMetadata {
pub ignore_unknown_instances: Option<bool>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<String, UnresolvedValue>,
pub properties: HashMap<String, UnresolvedRbxValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
#[serde(skip)]
pub path: PathBuf,
}
impl DirectoryMetadata {
pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result<Self> {
let mut meta: Self = serde_json::from_slice(slice).with_context(|| {
pub fn from_slice(slice: &[u8], path: &Path) -> anyhow::Result<Self> {
serde_json::from_slice(slice).with_context(|| {
format!(
"File contained malformed init.meta.json data: {}",
path.display()
)
})?;
meta.path = path;
Ok(meta)
})
}
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) {
self.apply_ignore_unknown_instances(snapshot);
self.apply_class_name(snapshot)?;
self.apply_properties(snapshot)?;
Ok(())
self.apply_class_name(snapshot);
self.apply_properties(snapshot);
}
fn apply_class_name(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
fn apply_class_name(&mut self, snapshot: &mut InstanceSnapshot) {
if let Some(class_name) = self.class_name.take() {
if snapshot.class_name != "Folder" {
// TODO: Turn into error type
return Err(format_err!(
"className in init.meta.json can only be specified if the \
affected directory would turn into a Folder instance."
));
panic!("className in init.meta.json can only be specified if the affected directory would turn into a Folder instance.");
}
snapshot.class_name = Cow::Owned(class_name);
}
Ok(())
}
fn apply_ignore_unknown_instances(&mut self, snapshot: &mut InstanceSnapshot) {
@@ -128,17 +110,17 @@ impl DirectoryMetadata {
}
}
fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
let path = &self.path;
fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) {
let class_name = &snapshot.class_name;
for (key, unresolved) in self.properties.drain() {
let value = unresolved
.resolve(&snapshot.class_name, &key)
.with_context(|| format!("error applying meta file {}", path.display()))?;
let source_properties = self.properties.drain().map(|(key, value)| {
try_resolve_value(class_name, &key, &value)
.map(|resolved| (key, resolved))
.expect("TODO: Handle rbx_reflection errors")
});
for (key, value) in source_properties {
snapshot.properties.insert(key, value);
}
Ok(())
}
}

View File

@@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path};
use anyhow::Context;
use memofs::Vfs;
use rbx_reflection::ClassTag;
use rbx_reflection::{get_class_descriptor, try_resolve_value};
use crate::{
project::{Project, ProjectNode},
@@ -140,9 +140,9 @@ pub fn snapshot_project_node(
// Members of DataModel with names that match known services are
// probably supposed to be those services.
let descriptor = rbx_reflection_database::get().classes.get(&name)?;
let descriptor = get_class_descriptor(&name)?;
if descriptor.tags.contains(&ClassTag::Service) {
if descriptor.is_service() {
return Some(name.clone());
}
} else if parent_class == "StarterPlayer" {
@@ -171,18 +171,11 @@ pub fn snapshot_project_node(
}
}
for (key, unresolved) in &node.properties {
let value = unresolved
.clone()
.resolve(&class_name, key)
.with_context(|| {
format!(
"Unresolvable property in project at path {}",
project_path.display()
)
})?;
for (key, value) in &node.properties {
let resolved_value = try_resolve_value(&class_name, key, value)
.expect("TODO: Properly handle value resolution errors");
properties.insert(key.clone(), value);
properties.insert(key.clone(), resolved_value);
}
// If the user specified $ignoreUnknownInstances, overwrite the existing

View File

@@ -1,7 +1,8 @@
use std::path::Path;
use std::{collections::HashMap, path::Path};
use anyhow::Context;
use memofs::Vfs;
use rbx_dom_weak::{RbxInstanceProperties, RbxTree};
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -13,11 +14,18 @@ pub fn snapshot_rbxm(
path: &Path,
instance_name: &str,
) -> SnapshotInstanceResult {
let temp_tree = rbx_binary::from_reader_default(vfs.read(path)?.as_slice())
let mut temp_tree = RbxTree::new(RbxInstanceProperties {
name: "DataModel".to_owned(),
class_name: "DataModel".to_owned(),
properties: HashMap::new(),
});
let root_id = temp_tree.get_root_id();
rbx_binary::decode(&mut temp_tree, root_id, vfs.read(path)?.as_slice())
.with_context(|| format!("Malformed rbxm file: {}", path.display()))?;
let root_instance = temp_tree.root();
let children = root_instance.children();
let root_instance = temp_tree.get_instance(root_id).unwrap();
let children = root_instance.get_children_ids();
if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])

View File

@@ -19,8 +19,8 @@ pub fn snapshot_rbxmx(
let temp_tree = rbx_xml::from_reader(vfs.read(path)?.as_slice(), options)
.with_context(|| format!("Malformed rbxm file: {}", path.display()))?;
let root_instance = temp_tree.root();
let children = root_instance.children();
let root_instance = temp_tree.get_instance(temp_tree.get_root_id()).unwrap();
let children = root_instance.get_children_ids();
if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])

View File

@@ -1,6 +1,7 @@
---
source: src/snapshot_middleware/json_model.rs
expression: instance_snapshot
---
snapshot_id: ~
metadata:
@@ -26,3 +27,4 @@ children:
class_name: StringValue
properties: {}
children: []

View File

@@ -3,6 +3,7 @@ use std::{path::Path, str};
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -20,7 +21,9 @@ pub fn snapshot_txt(
.to_owned();
let properties = hashmap! {
"Value".to_owned() => contents_str.into(),
"Value".to_owned() => RbxValue::String {
value: contents_str,
},
};
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
@@ -37,8 +40,8 @@ pub fn snapshot_txt(
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
}
Ok(Some(snapshot))

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use rojo_insta_ext::RedactionMap;
use serde::Serialize;
@@ -29,15 +29,15 @@ pub fn intern_tree(tree: &RojoTree, redactions: &mut RedactionMap) {
/// Copy of data from RojoTree in the right shape to have useful snapshots.
#[derive(Debug, Serialize)]
struct InstanceView {
id: Ref,
id: RbxId,
name: String,
class_name: String,
properties: HashMap<String, Variant>,
properties: HashMap<String, RbxValue>,
metadata: InstanceMetadata,
children: Vec<InstanceView>,
}
fn extract_instance_view(tree: &RojoTree, id: Ref) -> InstanceView {
fn extract_instance_view(tree: &RojoTree, id: RbxId) -> InstanceView {
let instance = tree.get_instance(id).unwrap();
InstanceView {

View File

@@ -1,12 +1,12 @@
//! Defines Rojo's HTTP API, all under /api. These endpoints generally return
//! JSON.
use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc};
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use futures::{Future, Stream};
use hyper::{service::Service, Body, Method, Request, StatusCode};
use rbx_dom_weak::types::Ref;
use rbx_dom_weak::RbxId;
use crate::{
serve_session::ServeSession,
@@ -200,11 +200,11 @@ impl ApiService {
fn handle_api_read(&self, request: Request<Body>) -> <Self as Service>::Future {
let argument = &request.uri().path()["/api/read/".len()..];
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
let requested_ids: Option<Vec<RbxId>> = argument.split(',').map(RbxId::parse_str).collect();
let requested_ids = match requested_ids {
Ok(ids) => ids,
Err(_) => {
Some(ids) => ids,
None => {
return json(
ErrorResponse::bad_request("Malformed ID list"),
StatusCode::BAD_REQUEST,
@@ -239,9 +239,9 @@ impl ApiService {
/// Open a script with the given ID in the user's default text editor.
fn handle_api_open(&self, request: Request<Body>) -> <Self as Service>::Future {
let argument = &request.uri().path()["/api/open/".len()..];
let requested_id = match Ref::from_str(argument) {
Ok(id) => id,
Err(_) => {
let requested_id = match RbxId::parse_str(argument) {
Some(id) => id,
None => {
return json(
ErrorResponse::bad_request("Invalid instance ID"),
StatusCode::BAD_REQUEST,

View File

@@ -5,7 +5,7 @@ use std::{
collections::{HashMap, HashSet},
};
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use serde::{Deserialize, Serialize};
use crate::{
@@ -17,28 +17,28 @@ use crate::{
pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Current protocol version, which is required to match.
pub const PROTOCOL_VERSION: u64 = 4;
pub const PROTOCOL_VERSION: u64 = 3;
/// Message returned by Rojo API when a change has occurred.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscribeMessage<'a> {
pub removed: Vec<Ref>,
pub added: HashMap<Ref, Instance<'a>>,
pub removed: Vec<RbxId>,
pub added: HashMap<RbxId, Instance<'a>>,
pub updated: Vec<InstanceUpdate>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstanceUpdate {
pub id: Ref,
pub id: RbxId,
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
// TODO: Transform from HashMap<String, Option<_>> to something else, since
// null will get lost when decoding from JSON in some languages.
#[serde(default)]
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_properties: HashMap<String, Option<RbxValue>>,
pub changed_metadata: Option<InstanceMetadata>,
}
@@ -59,36 +59,23 @@ impl InstanceMetadata {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Instance<'a> {
pub id: Ref,
pub parent: Ref,
pub id: RbxId,
pub parent: Option<RbxId>,
pub name: Cow<'a, str>,
pub class_name: Cow<'a, str>,
pub properties: HashMap<String, Cow<'a, Variant>>,
pub children: Cow<'a, [Ref]>,
pub properties: Cow<'a, HashMap<String, RbxValue>>,
pub children: Cow<'a, [RbxId]>,
pub metadata: Option<InstanceMetadata>,
}
impl<'a> Instance<'a> {
pub(crate) fn from_rojo_instance(source: InstanceWithMeta<'_>) -> Instance<'_> {
let properties = source
.properties()
.iter()
.filter_map(|(key, value)| {
// SharedString values can't be serialized via Serde
if matches!(value, Variant::SharedString(_)) {
return None;
}
Some((key.clone(), Cow::Borrowed(value)))
})
.collect();
Instance {
id: source.id(),
parent: source.parent(),
name: Cow::Borrowed(source.name()),
class_name: Cow::Borrowed(source.class_name()),
properties,
properties: Cow::Borrowed(source.properties()),
children: Cow::Borrowed(source.children()),
metadata: Some(InstanceMetadata::from_rojo_metadata(source.metadata())),
}
@@ -104,7 +91,7 @@ pub struct ServerInfoResponse {
pub protocol_version: u64,
pub project_name: String,
pub expected_place_ids: Option<HashSet<u64>>,
pub root_instance_id: Ref,
pub root_instance_id: RbxId,
}
/// Response body from /api/read/{id}
@@ -113,17 +100,17 @@ pub struct ServerInfoResponse {
pub struct ReadResponse<'a> {
pub session_id: SessionId,
pub message_cursor: u32,
pub instances: HashMap<Ref, Instance<'a>>,
pub instances: HashMap<RbxId, Instance<'a>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WriteRequest {
pub session_id: SessionId,
pub removed: Vec<Ref>,
pub removed: Vec<RbxId>,
#[serde(default)]
pub added: HashMap<Ref, ()>,
pub added: HashMap<RbxId, ()>,
pub updated: Vec<InstanceUpdate>,
}

View File

@@ -4,7 +4,7 @@ pub mod interface;
mod ui;
mod util;
use std::sync::Arc;
use std::{net::SocketAddr, sync::Arc};
use futures::{
future::{self, FutureResult},
@@ -57,9 +57,7 @@ impl LiveServer {
LiveServer { serve_session }
}
pub fn start(self, port: u16) {
let address = ([127, 0, 0, 1], port).into();
pub fn start(self, address: SocketAddr) {
let server = Server::bind(&address)
.serve(move || {
let service: FutureResult<_, hyper::Error> =

View File

@@ -5,7 +5,7 @@ use std::{borrow::Cow, sync::Arc, time::Duration};
use futures::{future, Future};
use hyper::{header, service::Service, Body, Method, Request, Response, StatusCode};
use maplit::hashmap;
use rbx_dom_weak::types::{Ref, Variant};
use rbx_dom_weak::{RbxId, RbxValue};
use ritz::{html, Fragment, HtmlContent, HtmlSelfClosingTag};
use crate::{
@@ -93,7 +93,7 @@ impl UiService {
.unwrap()
}
fn instance(tree: &RojoTree, id: Ref) -> HtmlContent<'_> {
fn instance(tree: &RojoTree, id: RbxId) -> HtmlContent<'_> {
let instance = tree.get_instance(id).unwrap();
let children_list: Vec<_> = instance
.children()
@@ -126,7 +126,7 @@ impl UiService {
.map(|(key, value)| {
html! {
<div class="instance-property" title={ Self::display_value(value) }>
{ key.clone() } ": " { format!("{:?}", value.ty()) }
{ key.clone() } ": " { format!("{:?}", value.get_type()) }
</div>
}
})
@@ -198,7 +198,7 @@ impl UiService {
html! {
<div class="instance">
<label class="instance-title" for={ format!("instance-{:?}", id) }>
<label class="instance-title" for={ format!("instance-{}", id) }>
{ instance.name().to_owned() }
{ class_name_specifier }
</label>
@@ -209,10 +209,10 @@ impl UiService {
}
}
fn display_value(value: &Variant) -> String {
fn display_value(value: &RbxValue) -> String {
match value {
Variant::String(value) => value.clone(),
Variant::Bool(value) => value.to_string(),
RbxValue::String { value } => value.clone(),
RbxValue::Bool { value } => value.to_string(),
_ => format!("{:?}", value),
}
}
@@ -288,14 +288,14 @@ impl UiService {
struct ExpandableSection<'a> {
title: &'a str,
class_name: &'a str,
id: Ref,
id: RbxId,
expanded: bool,
content: HtmlContent<'a>,
}
impl<'a> ExpandableSection<'a> {
fn render(self) -> HtmlContent<'a> {
let input_id = format!("{}-{:?}", self.class_name, self.id);
let input_id = format!("{}-{}", self.class_name, self.id);
// We need to specify this input manually because Ritz doesn't have
// support for conditional attributes like `checked`.

View File

@@ -1,12 +0,0 @@
{
"name": "enums",
"tree": {
"$className": "DataModel",
"Lighting": {
"$properties": {
"Technology": "Voxel"
}
}
}
}

View File

@@ -1,12 +1,6 @@
{
"name": "unions",
"tree": {
"$className": "DataModel",
"Workspace": {
"TwoParts": {
"$path": "src"
}
}
"$path": "src"
}
}

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use rbx_dom_weak::types::Ref;
use rbx_dom_weak::RbxId;
use serde::Serialize;
use librojo::web_api::{Instance, InstanceUpdate, ReadResponse, SubscribeResponse};
@@ -31,8 +31,8 @@ pub trait Internable<T> {
fn intern(&self, redactions: &mut RedactionMap, extra: T);
}
impl Internable<Ref> for ReadResponse<'_> {
fn intern(&self, redactions: &mut RedactionMap, root_id: Ref) {
impl Internable<RbxId> for ReadResponse<'_> {
fn intern(&self, redactions: &mut RedactionMap, root_id: RbxId) {
redactions.intern(root_id);
let root_instance = self.instances.get(&root_id).unwrap();
@@ -43,8 +43,12 @@ impl Internable<Ref> for ReadResponse<'_> {
}
}
impl<'a> Internable<&'a HashMap<Ref, Instance<'_>>> for Instance<'a> {
fn intern(&self, redactions: &mut RedactionMap, other_instances: &HashMap<Ref, Instance<'_>>) {
impl<'a> Internable<&'a HashMap<RbxId, Instance<'_>>> for Instance<'a> {
fn intern(
&self,
redactions: &mut RedactionMap,
other_instances: &HashMap<RbxId, Instance<'_>>,
) {
redactions.intern(self.id);
for child_id in self.children.iter() {
@@ -71,7 +75,7 @@ fn intern_instance_updates(redactions: &mut RedactionMap, updates: &[InstanceUpd
fn intern_instance_additions(
redactions: &mut RedactionMap,
additions: &HashMap<Ref, Instance<'_>>,
additions: &HashMap<RbxId, Instance<'_>>,
) {
// This method redacts in a deterministic order from a HashMap by collecting
// all of the instances that are direct children of instances we've already
@@ -79,7 +83,7 @@ fn intern_instance_additions(
let mut added_roots = Vec::new();
for (id, added) in additions {
let parent_id = added.parent;
let parent_id = added.parent.unwrap();
let parent_redacted = redactions.get_redacted_value(parent_id);
// Here, we assume that instances are only added to other instances that

View File

@@ -7,7 +7,7 @@ use std::{
time::Duration,
};
use rbx_dom_weak::types::Ref;
use rbx_dom_weak::RbxId;
use tempfile::{tempdir, TempDir};
@@ -146,7 +146,7 @@ impl TestServeSession {
Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
}
pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse, reqwest::Error> {
pub fn get_api_read(&self, id: RbxId) -> Result<ReadResponse, reqwest::Error> {
let url = format!("http://localhost:{}/api/read/{}", self.port, id);
let body = reqwest::get(&url)?.text()?;

View File

@@ -49,7 +49,6 @@ gen_build_tests! {
server_init,
txt,
txt_in_folder,
unresolved_values,
}
fn run_build_test(test_name: &str) {