From 42568b9709f3b5e2effd962e9e2733fa945d6d8e Mon Sep 17 00:00:00 2001 From: Micah Date: Thu, 27 Nov 2025 12:10:57 -0800 Subject: [PATCH 1/7] Release Rojo v7.7.0-rc.1 (#1174) --- CHANGELOG.md | 6 +- Cargo.lock | 26 +- Cargo.toml | 16 +- plugin/Version.txt | 2 +- plugin/rbx_dom_lua/base64.lua | 143 +----- plugin/rbx_dom_lua/customProperties.lua | 26 + plugin/rbx_dom_lua/database.json | 648 ++++++++++++++++++++---- 7 files changed, 600 insertions(+), 267 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f385fc..410a7cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +## [7.7.0-rc.1] (November 27th, 2025) + * Fixed a bug where passing `--skip-git` to `rojo init` would still create a file named `gitignore.txt` ([#1172]) * A new command `rojo syncback` has been added. It can be used as `rojo syncback [path to project] --input [path to file]`. ([#937]) This command takes a Roblox file and pulls Instances out of it and places them in the correct position in the provided project. @@ -65,13 +67,11 @@ Making a new release? Simply add the new header with the version and date undern - `syncCurrentCamera` is a toggle for whether to sync back the Workspace's CurrentCamera. Defaults to `false`. - `syncUnscriptable` is a toggle for whether to sync back properties that cannot be set by the Roblox Studio plugin. Defaults to `true`. - If you are used to the `UpliftGames` version of this feature, there are a few notable differences: - - `syncUnscriptable` defaults to `true` instead of `false` - - `ignoreTrees` doesn't require the root of the project's name in it. * Fixed bugs and improved performance & UX for the script diff viewer ([#994]) * Rebuilt the internal communication between the server and plugin to use [websockets](https://devforum.roblox.com/t/websockets-support-in-studio-is-now-available/4021932/1) instead of [long polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling) ([#1142]) * Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159]) +[7.7.0-rc.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.7.0-rc.1 [#937]: https://github.com/rojo-rbx/rojo/pull/937 [#994]: https://github.com/rojo-rbx/rojo/pull/994 [#1142]: https://github.com/rojo-rbx/rojo/pull/1142 diff --git a/Cargo.lock b/Cargo.lock index 000115ac..dd4237bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1792,9 +1792,9 @@ dependencies = [ [[package]] name = "rbx_binary" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d419f67c8012bf83569086e1208c541478b3b8e4f523deaa0b80d723fb5ef22" +checksum = "95e2b4a187679aa3d169ed50ed5eedbf26383459fec83bf1232c2934b35b24de" dependencies = [ "ahash", "log", @@ -1810,9 +1810,9 @@ dependencies = [ [[package]] name = "rbx_dom_weak" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74878a4a801afc8014b14ede4b38015a13de5d29ab0095d5ed284a744253f6" +checksum = "a7a5c48c2605913fbb1986bceb3e18ef9f12eadedb7edd62bf9fb03447b57c46" dependencies = [ "ahash", "rbx_types", @@ -1822,9 +1822,9 @@ dependencies = [ [[package]] name = "rbx_reflection" -version = "6.0.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565dd3430991f35443fa6d23cc239fade2110c5089deb6bae5de77c400df4fd2" +checksum = "84f635e79d5d710c82e9049faa57d32945e76a6b041280dc6274f732c0dd78dc" dependencies = [ "rbx_types", "serde", @@ -1833,9 +1833,9 @@ dependencies = [ [[package]] name = "rbx_reflection_database" -version = "2.0.1+roblox-697" +version = "2.0.2+roblox-700" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69035a14b103c5a9b8bc6a61d30f4ee6f2608afdee137dae09b26037dba5dc8" +checksum = "de2753b896d08d74316d8b89fbeb2470ebd3986404ebba82fa85fcc0330955cf" dependencies = [ "dirs 5.0.1", "log", @@ -1846,9 +1846,9 @@ dependencies = [ [[package]] name = "rbx_types" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03220ffce2bd06ad04f77a003cb807f2e5b2a18e97623066a5ac735a978398af" +checksum = "de3b89eefdd71f5e2a25543b1ead9f6ea2ed3fdfba8b397cbdb0de053eb2463e" dependencies = [ "base64 0.13.1", "bitflags 1.3.2", @@ -1861,9 +1861,9 @@ dependencies = [ [[package]] name = "rbx_xml" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6c302cefe9c92ed09bcbb075cd24379271de135b0af331409a64c2ea3646ee" +checksum = "e0cbaf53b44c9cc0fad1e5dc8ac63fb32fa0ecaa26d32b269cebe4dca8b7b4de" dependencies = [ "ahash", "base64 0.13.1", @@ -2037,7 +2037,7 @@ dependencies = [ [[package]] name = "rojo" -version = "7.7.0-prealpha" +version = "7.7.0-rc.1" dependencies = [ "anyhow", "backtrace", diff --git a/Cargo.toml b/Cargo.toml index 9fcea18d..c5b62e16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rojo" -version = "7.7.0-prealpha" +version = "7.7.0-rc.1" rust-version = "1.88" authors = [ "Lucien Greathouse ", @@ -49,17 +49,19 @@ harness = false memofs = { version = "0.3.0", path = "crates/memofs" } # These dependencies can be uncommented when working on rbx-dom simultaneously -# rbx_binary = { path = "../rbx-dom/rbx_binary" } +# rbx_binary = { path = "../rbx-dom/rbx_binary", features = [ +# "unstable_text_format", +# ] } # 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 = { version = "2.0.0", features = ["unstable_text_format"] } -rbx_dom_weak = "4.0.0" -rbx_reflection = "6.0.0" -rbx_reflection_database = "2.0.1" -rbx_xml = "2.0.0" +rbx_binary = { version = "2.0.1", features = ["unstable_text_format"] } +rbx_dom_weak = "4.1.0" +rbx_reflection = "6.1.0" +rbx_reflection_database = "2.0.2" +rbx_xml = "2.0.1" anyhow = "1.0.80" backtrace = "0.3.69" diff --git a/plugin/Version.txt b/plugin/Version.txt index 6bb62d27..baee60f4 100644 --- a/plugin/Version.txt +++ b/plugin/Version.txt @@ -1 +1 @@ -7.7.0-prealpha \ No newline at end of file +7.7.0-rc.1 \ No newline at end of file diff --git a/plugin/rbx_dom_lua/base64.lua b/plugin/rbx_dom_lua/base64.lua index 0ea19d39..0c6f2abc 100644 --- a/plugin/rbx_dom_lua/base64.lua +++ b/plugin/rbx_dom_lua/base64.lua @@ -1,139 +1,10 @@ --- Thanks to Tiffany352 for this base64 implementation! - -local floor = math.floor -local char = string.char - -local function encodeBase64(str) - local out = {} - local nOut = 0 - local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - local strLen = #str - - -- 3 octets become 4 hextets - for i = 1, strLen - 2, 3 do - local b1, b2, b3 = str:byte(i, i + 3) - local word = b3 + b2 * 256 + b1 * 256 * 256 - - local h4 = word % 64 + 1 - word = floor(word / 64) - local h3 = word % 64 + 1 - word = floor(word / 64) - local h2 = word % 64 + 1 - word = floor(word / 64) - local h1 = word % 64 + 1 - - out[nOut + 1] = alphabet:sub(h1, h1) - out[nOut + 2] = alphabet:sub(h2, h2) - out[nOut + 3] = alphabet:sub(h3, h3) - out[nOut + 4] = alphabet:sub(h4, h4) - nOut = nOut + 4 - end - - local remainder = strLen % 3 - - if remainder == 2 then - -- 16 input bits -> 3 hextets (2 full, 1 partial) - local b1, b2 = str:byte(-2, -1) - -- partial is 4 bits long, leaving 2 bits of zero padding -> - -- offset = 4 - local word = b2 * 4 + b1 * 4 * 256 - - local h3 = word % 64 + 1 - word = floor(word / 64) - local h2 = word % 64 + 1 - word = floor(word / 64) - local h1 = word % 64 + 1 - - out[nOut + 1] = alphabet:sub(h1, h1) - out[nOut + 2] = alphabet:sub(h2, h2) - out[nOut + 3] = alphabet:sub(h3, h3) - out[nOut + 4] = "=" - elseif remainder == 1 then - -- 8 input bits -> 2 hextets (2 full, 1 partial) - local b1 = str:byte(-1, -1) - -- partial is 2 bits long, leaving 4 bits of zero padding -> - -- offset = 16 - local word = b1 * 16 - - local h2 = word % 64 + 1 - word = floor(word / 64) - local h1 = word % 64 + 1 - - out[nOut + 1] = alphabet:sub(h1, h1) - out[nOut + 2] = alphabet:sub(h2, h2) - out[nOut + 3] = "=" - out[nOut + 4] = "=" - end - -- if the remainder is 0, then no work is needed - - return table.concat(out, "") -end - -local function decodeBase64(str) - local out = {} - local nOut = 0 - local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - local strLen = #str - local acc = 0 - local nAcc = 0 - - local alphabetLut = {} - for i = 1, #alphabet do - alphabetLut[alphabet:sub(i, i)] = i - 1 - end - - -- 4 hextets become 3 octets - for i = 1, strLen do - local ch = str:sub(i, i) - local byte = alphabetLut[ch] - if byte then - acc = acc * 64 + byte - nAcc = nAcc + 1 - end - - if nAcc == 4 then - local b3 = acc % 256 - acc = floor(acc / 256) - local b2 = acc % 256 - acc = floor(acc / 256) - local b1 = acc % 256 - - out[nOut + 1] = char(b1) - out[nOut + 2] = char(b2) - out[nOut + 3] = char(b3) - nOut = nOut + 3 - nAcc = 0 - acc = 0 - end - end - - if nAcc == 3 then - -- 3 hextets -> 16 bit output - acc = acc * 64 - acc = floor(acc / 256) - local b2 = acc % 256 - acc = floor(acc / 256) - local b1 = acc % 256 - - out[nOut + 1] = char(b1) - out[nOut + 2] = char(b2) - elseif nAcc == 2 then - -- 2 hextets -> 8 bit output - acc = acc * 64 - acc = floor(acc / 256) - acc = acc * 64 - acc = floor(acc / 256) - local b1 = acc % 256 - - out[nOut + 1] = char(b1) - elseif nAcc == 1 then - error("Base64 has invalid length") - end - - return table.concat(out, "") -end +local EncodingService = game:GetService("EncodingService") return { - decode = decodeBase64, - encode = encodeBase64, + decode = function(input: string) + return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input))) + end, + encode = function(input: string) + return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input))) + end, } diff --git a/plugin/rbx_dom_lua/customProperties.lua b/plugin/rbx_dom_lua/customProperties.lua index dc41a2b8..a5b96f99 100644 --- a/plugin/rbx_dom_lua/customProperties.lua +++ b/plugin/rbx_dom_lua/customProperties.lua @@ -208,4 +208,30 @@ return { end, }, }, + StyleRule = { + PropertiesSerialize = { + read = function(instance: StyleRule) + return true, instance:GetProperties() + end, + write = function(instance: StyleRule, _, value: { [any]: any }) + if typeof(value) ~= "table" then + return false, Error.new(Error.Kind.CannotParseBinaryString) + end + + local existing = instance:GetProperties() + + for itemName, itemValue in pairs(value) do + instance:SetProperty(itemName, itemValue) + end + + for existingItemName in pairs(existing) do + if value[existingItemName] == nil then + instance:SetProperty(existingItemName, nil) + end + end + + return true + end, + }, + }, } diff --git a/plugin/rbx_dom_lua/database.json b/plugin/rbx_dom_lua/database.json index 842e3848..1856f932 100644 --- a/plugin/rbx_dom_lua/database.json +++ b/plugin/rbx_dom_lua/database.json @@ -1,9 +1,9 @@ { "Version": [ 0, - 697, + 700, 0, - 6970925 + 7000935 ], "Classes": { "Accessory": { @@ -2098,7 +2098,7 @@ "UniqueId": "00000000000000000000000000000000" }, "InputPinData": { - "BinaryString": "" + "BinaryString": "AQAAAAAAAAA=" }, "NodeType": { "Enum": 0 @@ -5807,6 +5807,19 @@ } } }, + "AutoPlay": { + "Name": "AutoPlay", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "IsMutedForCapture": { "Name": "IsMutedForCapture", "Scriptability": "None", @@ -5957,6 +5970,9 @@ "AutoLoad": { "Bool": true }, + "AutoPlay": { + "Bool": false + }, "Capabilities": { "SecurityCapabilities": 0 }, @@ -13098,6 +13114,19 @@ } } }, + "HeadShape": { + "Name": "HeadShape", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "String" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "Instance": { "Name": "Instance", "Scriptability": "ReadWrite", @@ -13135,6 +13164,9 @@ 0.0 ] }, + "HeadShape": { + "String": "" + }, "HistoryId": { "UniqueId": "00000000000000000000000000000000" }, @@ -23424,7 +23456,7 @@ "Orientation": { "Vector3": [ -0.0, - 180.0, + 179.99998, 90.0 ] }, @@ -24514,6 +24546,22 @@ "Serialization": "DoesNotSerialize" } } + }, + "SelectionMode": { + "Name": "SelectionMode", + "Scriptability": "None", + "DataType": { + "Enum": "ExperienceStateCaptureSelectionMode" + }, + "Tags": [ + "Hidden", + "NotReplicated" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } } }, "DefaultProperties": { @@ -27719,25 +27767,12 @@ "DataType": { "Value": "Bool" }, - "Tags": [], - "Kind": { - "Canonical": { - "Serialization": "Serializes" - } - } - }, - "VideoRecording": { - "Name": "VideoRecording", - "Scriptability": "None", - "DataType": { - "Value": "Bool" - }, "Tags": [ - "NotReplicated" + "Deprecated" ], "Kind": { "Canonical": { - "Serialization": "DoesNotSerialize" + "Serialization": "Serializes" } } } @@ -39425,6 +39460,9 @@ "SmoothingAngle": { "Float32": 0.0 }, + "SolidMeshHolder": { + "NetAssetRef": "" + }, "SourceAssetId": { "Int64": -1 }, @@ -46393,6 +46431,9 @@ "SmoothingAngle": { "Float32": 0.0 }, + "SolidMeshHolder": { + "NetAssetRef": "" + }, "SourceAssetId": { "Int64": -1 }, @@ -47207,6 +47248,7 @@ "OpenCloudApiV1": { "Name": "OpenCloudApiV1", "Tags": [ + "Deprecated", "NotCreatable", "NotReplicated" ], @@ -47227,6 +47269,7 @@ "OpenCloudService": { "Name": "OpenCloudService", "Tags": [ + "Deprecated", "NotCreatable", "NotReplicated", "Service" @@ -48456,23 +48499,6 @@ } } }, - "SerializedCSGTree": { - "Name": "SerializedCSGTree", - "Scriptability": "None", - "DataType": { - "Value": "SharedString" - }, - "Tags": [ - "Hidden", - "NotReplicated", - "NotScriptable" - ], - "Kind": { - "Canonical": { - "Serialization": "DoesNotSerialize" - } - } - }, "SerializedOperationGraph": { "Name": "SerializedOperationGraph", "Scriptability": "None", @@ -48798,6 +48824,9 @@ "SmoothingAngle": { "Float32": 0.0 }, + "SolidMeshHolder": { + "NetAssetRef": "" + }, "SourceAssetId": { "Int64": -1 }, @@ -52716,6 +52745,22 @@ } } }, + "InputBindingMappingsRaw": { + "Name": "InputBindingMappingsRaw", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, "ScreenOrientation": { "Name": "ScreenOrientation", "Scriptability": "ReadWrite", @@ -54707,6 +54752,158 @@ } } }, + "PyramidHandleAdornment": { + "Name": "PyramidHandleAdornment", + "Tags": [], + "Superclass": "HandleAdornment", + "Properties": { + "Height": { + "Name": "Height", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Float32" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "Shading": { + "Name": "Shading", + "Scriptability": "ReadWrite", + "DataType": { + "Enum": "AdornShading" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "Sides": { + "Name": "Sides", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Int32" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "Size": { + "Name": "Size", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Float32" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "AdornCullingMode": { + "Enum": 0 + }, + "AlwaysOnTop": { + "Bool": false + }, + "Archivable": { + "Bool": true + }, + "Attributes": { + "Attributes": {} + }, + "CFrame": { + "CFrame": { + "position": [ + 0.0, + 0.0, + 0.0 + ], + "orientation": [ + [ + 1.0, + 0.0, + 0.0 + ], + [ + 0.0, + 1.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0 + ] + ] + } + }, + "Capabilities": { + "SecurityCapabilities": 0 + }, + "Color3": { + "Color3": [ + 0.050980393, + 0.4117647, + 0.6745098 + ] + }, + "Height": { + "Float32": 2.0 + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "Sandboxed": { + "Bool": false + }, + "Shading": { + "Enum": 0 + }, + "Sides": { + "Int32": 4 + }, + "Size": { + "Float32": 1.0 + }, + "SizeRelativeOffset": { + "Vector3": [ + 0.0, + 0.0, + 0.0 + ] + }, + "SourceAssetId": { + "Int64": -1 + }, + "Tags": { + "Tags": [] + }, + "Transparency": { + "Float32": 0.0 + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + }, + "Visible": { + "Bool": true + }, + "ZIndex": { + "Int32": -1 + } + } + }, "QWidgetPluginGui": { "Name": "QWidgetPluginGui", "Tags": [ @@ -58222,22 +58419,6 @@ } } }, - "PredictionState": { - "Name": "PredictionState", - "Scriptability": "Read", - "DataType": { - "Enum": "PredictionState" - }, - "Tags": [ - "NotReplicated", - "ReadOnly" - ], - "Kind": { - "Canonical": { - "Serialization": "DoesNotSerialize" - } - } - }, "RunState": { "Name": "RunState", "Scriptability": "ReadWrite", @@ -63152,19 +63333,6 @@ } } }, - "AudioPlayerVolumeFix": { - "Name": "AudioPlayerVolumeFix", - "Scriptability": "None", - "DataType": { - "Enum": "RolloutState" - }, - "Tags": [], - "Kind": { - "Canonical": { - "Serialization": "Serializes" - } - } - }, "CharacterSoundsUseNewApi": { "Name": "CharacterSoundsUseNewApi", "Scriptability": "ReadWrite", @@ -63290,9 +63458,6 @@ "AudioApiByDefault": { "Enum": 0 }, - "AudioPlayerVolumeFix": { - "Enum": 0 - }, "Capabilities": { "SecurityCapabilities": 0 }, @@ -64952,6 +65117,19 @@ } } }, + "CharacterBreakJointsOnDeath": { + "Name": "CharacterBreakJointsOnDeath", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "CharacterJumpHeight": { "Name": "CharacterJumpHeight", "Scriptability": "ReadWrite", @@ -65032,6 +65210,21 @@ } } }, + "CreateDefaultPlayerModule": { + "Name": "CreateDefaultPlayerModule", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [ + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "DevCameraOcclusionMode": { "Name": "DevCameraOcclusionMode", "Scriptability": "ReadWrite", @@ -65460,9 +65653,7 @@ "DataType": { "Enum": "CharacterControlMode" }, - "Tags": [ - "NotBrowsable" - ], + "Tags": [], "Kind": { "Canonical": { "Serialization": "Serializes" @@ -65482,21 +65673,6 @@ } } }, - "RagdollDeath": { - "Name": "RagdollDeath", - "Scriptability": "ReadWrite", - "DataType": { - "Value": "Bool" - }, - "Tags": [ - "NotBrowsable" - ], - "Kind": { - "Canonical": { - "Serialization": "Serializes" - } - } - }, "UserEmotesEnabled": { "Name": "UserEmotesEnabled", "Scriptability": "ReadWrite", @@ -65539,6 +65715,9 @@ "Capabilities": { "SecurityCapabilities": 0 }, + "CharacterBreakJointsOnDeath": { + "Bool": true + }, "CharacterJumpHeight": { "Float32": 7.2 }, @@ -65557,6 +65736,9 @@ "ClassicDeath": { "Bool": true }, + "CreateDefaultPlayerModule": { + "Bool": true + }, "DevCameraOcclusionMode": { "Enum": 0 }, @@ -65662,9 +65844,6 @@ "NameDisplayDistance": { "Float32": 100.0 }, - "RagdollDeath": { - "Bool": true - }, "Sandboxed": { "Bool": false }, @@ -67295,6 +67474,19 @@ } } }, + "EnableFindOnType": { + "Name": "EnableFindOnType", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "EnableIndentationRulers": { "Name": "EnableIndentationRulers", "Scriptability": "None", @@ -68836,6 +69028,19 @@ "Serialization": "Serializes" } } + }, + "LoggingEnabled": { + "Name": "LoggingEnabled", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } } }, "DefaultProperties": { @@ -69335,6 +69540,27 @@ } } }, + "StudioTestService": { + "Name": "StudioTestService", + "Tags": [ + "NotCreatable", + "NotReplicated", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "StudioTheme": { "Name": "StudioTheme", "Tags": [ @@ -69598,6 +69824,22 @@ } } }, + "ConditionsSerialize": { + "Name": "ConditionsSerialize", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "IsActive": { "Name": "IsActive", "Scriptability": "Read", @@ -69689,9 +69931,9 @@ }, "PropertiesSerialize": { "Name": "PropertiesSerialize", - "Scriptability": "None", + "Scriptability": "Custom", "DataType": { - "Value": "BinaryString" + "Value": "Attributes" }, "Tags": [ "Hidden", @@ -69699,7 +69941,9 @@ ], "Kind": { "Canonical": { - "Serialization": "Serializes" + "Serialization": { + "SerializesAs": "PropertiesSerialize" + } } } }, @@ -69750,7 +69994,7 @@ "Int32": 0 }, "PropertiesSerialize": { - "BinaryString": "AAAAAA==" + "Attributes": {} }, "Sandboxed": { "Bool": false @@ -74889,7 +75133,7 @@ "SecurityCapabilities": 0 }, "ChatTranslationFTUXShown": { - "Bool": false + "Bool": true }, "ChatTranslationToggleEnabled": { "Bool": false @@ -76041,6 +76285,38 @@ "Serialization": "DoesNotSerialize" } } + }, + "HasActiveUser": { + "Name": "HasActiveUser", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [ + "NotReplicated", + "ReadOnly" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "VoiceChatRestrictionStatus": { + "Name": "VoiceChatRestrictionStatus", + "Scriptability": "None", + "DataType": { + "Enum": "ChatRestrictionStatus" + }, + "Tags": [ + "NotReplicated", + "ReadOnly" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } } }, "DefaultProperties": { @@ -77457,6 +77733,7 @@ }, "Tags": [ "Hidden", + "NotReplicated", "NotScriptable" ], "Kind": { @@ -80464,6 +80741,9 @@ "SmoothingAngle": { "Float32": 0.0 }, + "SolidMeshHolder": { + "NetAssetRef": "" + }, "SourceAssetId": { "Int64": -1 }, @@ -84580,6 +84860,83 @@ } } }, + "VideoSampler": { + "Name": "VideoSampler", + "Tags": [ + "NotCreatable" + ], + "Superclass": "Object", + "Properties": { + "TimeLength": { + "Name": "TimeLength", + "Scriptability": "Read", + "DataType": { + "Value": "Float64" + }, + "Tags": [ + "NotReplicated", + "ReadOnly" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "VideoContent": { + "Name": "VideoContent", + "Scriptability": "Read", + "DataType": { + "Value": "Content" + }, + "Tags": [ + "NotReplicated", + "ReadOnly" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + } + }, + "DefaultProperties": {} + }, + "VideoScreenCaptureService": { + "Name": "VideoScreenCaptureService", + "Tags": [ + "NotCreatable", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "Attributes": { + "Attributes": {} + }, + "Capabilities": { + "SecurityCapabilities": 0 + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "Sandboxed": { + "Bool": false + }, + "SourceAssetId": { + "Int64": -1 + }, + "Tags": { + "Tags": [] + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "VideoService": { "Name": "VideoService", "Tags": [ @@ -87257,7 +87614,7 @@ "UniqueId": "00000000000000000000000000000000" }, "UseNewLuauTypeSolver": { - "Enum": 0 + "Enum": 2 }, "WorldPivotData": { "OptionalCFrame": null @@ -88428,6 +88785,23 @@ "Timeout": 10 } }, + "AnimationNodePlayMode": { + "name": "AnimationNodePlayMode", + "items": { + "Loop": 0, + "OnceAndHold": 2, + "OnceAndReset": 3, + "PingPong": 1 + } + }, + "AnimationNodeTransitionType": { + "name": "AnimationNodeTransitionType", + "items": { + "CrossFade": 0, + "DeadBlend": 2, + "InertialBlend": 1 + } + }, "AnimationNodeType": { "name": "AnimationNodeType", "items": { @@ -88607,10 +88981,12 @@ "EarAccessory": 57, "EmoteAnimation": 61, "EyeAccessory": 58, + "EyeMakeup": 90, "EyebrowAccessory": 76, "EyelashAccessory": 77, "Face": 18, "FaceAccessory": 42, + "FaceMakeup": 88, "FallAnimation": 50, "FontFamily": 73, "FrontAccessory": 45, @@ -88626,6 +89002,7 @@ "LeftArm": 29, "LeftLeg": 30, "LeftShoeAccessory": 70, + "LipMakeup": 89, "Lua": 5, "Mesh": 4, "MeshPart": 40, @@ -88759,10 +89136,12 @@ "DressSkirtAccessory": 72, "DynamicHead": 79, "EmoteAnimation": 61, + "EyeMakeup": 90, "EyebrowAccessory": 76, "EyelashAccessory": 77, "Face": 18, "FaceAccessory": 42, + "FaceMakeup": 88, "FallAnimation": 50, "FrontAccessory": 45, "Gear": 19, @@ -88775,6 +89154,7 @@ "LeftArm": 29, "LeftLeg": 30, "LeftShoeAccessory": 70, + "LipMakeup": 89, "MoodAnimation": 78, "NeckAccessory": 43, "Pants": 12, @@ -89501,6 +89881,7 @@ "DisconnectSecurityKeyMismatch": 258, "DisconnectSendPacketError": 262, "DisconnectTimeout": 266, + "DisconnectVerboselyModeratedGame": 309, "DisconnectWrongVersion": 280, "DisconnectionNotification": 308, "IPRecentlyConnected": 8, @@ -89657,9 +90038,17 @@ "GetVersionAsync": 7, "ListAsync": 6, "OnUpdate": 5, + "OrderedList": 15, + "OrderedRead": 13, + "OrderedRemove": 16, + "OrderedWrite": 14, "RemoveVersionAsync": 8, "SetIncrementAsync": 1, "SetIncrementSortedAsync": 4, + "StandardList": 11, + "StandardRead": 9, + "StandardRemove": 12, + "StandardWrite": 10, "UpdateAsync": 2 } }, @@ -90001,6 +90390,13 @@ "Unpublished": 3 } }, + "ExperienceStateCaptureSelectionMode": { + "name": "ExperienceStateCaptureSelectionMode", + "items": { + "Default": 0, + "SafetyHighlightMode": 1 + } + }, "ExperienceStateRecordingLoadMode": { "name": "ExperienceStateRecordingLoadMode", "items": { @@ -90427,6 +90823,15 @@ "Quality": 2 } }, + "GroupMembershipStatus": { + "name": "GroupMembershipStatus", + "items": { + "AlreadyMember": 3, + "JoinRequestPending": 2, + "Joined": 1, + "None": 0 + } + }, "GuiState": { "name": "GuiState", "items": { @@ -91191,9 +91596,10 @@ "LuauTypeCheckMode": { "name": "LuauTypeCheckMode", "items": { - "NoCheck": 0, - "Nonstrict": 1, - "Strict": 2 + "Default": 0, + "NoCheck": 1, + "Nonstrict": 2, + "Strict": 3 } }, "MarketplaceBulkPurchasePromptStatus": { @@ -91865,13 +92271,12 @@ "On": 1 } }, - "PredictionState": { - "name": "PredictionState", + "PredictionStatus": { + "name": "PredictionStatus", "items": { - "Idle": 0, - "Resimulating": 3, - "RollingBack": 2, - "Simulating": 1 + "Authoritative": 0, + "None": 2, + "Predicted": 1 } }, "PreferredInput": { @@ -91976,6 +92381,13 @@ "UploadFailed": 4 } }, + "PromptExperienceDetailsResult": { + "name": "PromptExperienceDetailsResult", + "items": { + "PromptClosed": 0, + "TeleportAttempted": 1 + } + }, "PromptLinkSharingResult": { "name": "PromptLinkSharingResult", "items": { @@ -92264,8 +92676,9 @@ "RigType": { "name": "RigType", "items": { - "Custom": 1, - "None": 2, + "Custom": 2, + "CustomHumanoid": 1, + "None": 3, "R15": 0 } }, @@ -92357,7 +92770,8 @@ "items": { "All": 1, "Collectibles": 2, - "Premium": 3 + "Premium": 3, + "TimedOptions": 4 } }, "SandboxedInstanceMode": { @@ -92661,6 +93075,17 @@ "Poison": 0 } }, + "StepFrequency": { + "name": "StepFrequency", + "items": { + "Hz1": 5, + "Hz10": 3, + "Hz15": 2, + "Hz30": 1, + "Hz5": 4, + "Hz60": 0 + } + }, "StreamOutBehavior": { "name": "StreamOutBehavior", "items": { @@ -93735,6 +94160,15 @@ "Unsupported": 9 } }, + "VideoSampleSize": { + "name": "VideoSampleSize", + "items": { + "Full": 3, + "Large": 2, + "Medium": 1, + "Small": 0 + } + }, "ViewMode": { "name": "ViewMode", "items": { From b89cc7f398971dab0582d6877339e9551b23544d Mon Sep 17 00:00:00 2001 From: Micah Date: Thu, 27 Nov 2025 12:32:57 -0800 Subject: [PATCH 2/7] Release memofs v0.3.1 (#1175) --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/memofs/CHANGELOG.md | 2 ++ crates/memofs/Cargo.toml | 2 +- crates/memofs/src/noop_backend.rs | 10 ++-------- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd4237bc..7f9a58a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,7 +1313,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memofs" -version = "0.3.0" +version = "0.3.1" dependencies = [ "crossbeam-channel", "fs-err", diff --git a/Cargo.toml b/Cargo.toml index c5b62e16..d8f34311 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ name = "build" harness = false [dependencies] -memofs = { version = "0.3.0", path = "crates/memofs" } +memofs = { version = "0.3.1", path = "crates/memofs" } # These dependencies can be uncommented when working on rbx-dom simultaneously # rbx_binary = { path = "../rbx-dom/rbx_binary", features = [ diff --git a/crates/memofs/CHANGELOG.md b/crates/memofs/CHANGELOG.md index 85b51523..b83bfeaf 100644 --- a/crates/memofs/CHANGELOG.md +++ b/crates/memofs/CHANGELOG.md @@ -1,6 +1,8 @@ # memofs Changelog ## Unreleased Changes + +## 0.3.1 (2025-11-27) * Added `Vfs::exists`. [#1169] * Added `create_dir` and `create_dir_all` to allow creating directories. [#937] diff --git a/crates/memofs/Cargo.toml b/crates/memofs/Cargo.toml index 8c2e8875..ad0b2637 100644 --- a/crates/memofs/Cargo.toml +++ b/crates/memofs/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "memofs" description = "Virtual filesystem with configurable backends." -version = "0.3.0" +version = "0.3.1" authors = [ "Lucien Greathouse ", "Micah Reid ", diff --git a/crates/memofs/src/noop_backend.rs b/crates/memofs/src/noop_backend.rs index dd10d452..98b86d1a 100644 --- a/crates/memofs/src/noop_backend.rs +++ b/crates/memofs/src/noop_backend.rs @@ -31,17 +31,11 @@ impl VfsBackend for NoopBackend { } fn create_dir(&mut self, _path: &Path) -> io::Result<()> { - Err(io::Error::new( - io::ErrorKind::Other, - "NoopBackend doesn't do anything", - )) + Err(io::Error::other("NoopBackend doesn't do anything")) } fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> { - Err(io::Error::new( - io::ErrorKind::Other, - "NoopBackend doesn't do anything", - )) + Err(io::Error::other("NoopBackend doesn't do anything")) } fn remove_file(&mut self, _path: &Path) -> io::Result<()> { From d08780fc14a17ea5ce887b35889a8d06bd7423bf Mon Sep 17 00:00:00 2001 From: Micah Date: Sat, 29 Nov 2025 21:21:48 -0800 Subject: [PATCH 3/7] Ensure that pruned Instances aren't treated as existing in syncback (#1179) Closes #1178. --- CHANGELOG.md | 3 +++ ...ck_util__ref_properties_pruned-stdout.snap | 7 +++++ ...rties_pruned-src__Pointer1.model.json.snap | 7 +++++ ...rties_pruned-src__Pointer2.model.json.snap | 7 +++++ ...rties_pruned-src__Pointer3.model.json.snap | 7 +++++ .../input-project/default.project.json | 9 +++++++ .../input-project/src/.gitkeep | 0 .../ref_properties_pruned/input.rbxl | Bin 0 -> 48562 bytes src/syncback/ref_properties.rs | 25 ++++++++++++++---- tests/tests/syncback.rs | 6 ++++- 10 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_pruned-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer1.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer2.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer3.model.json.snap create mode 100644 rojo-test/syncback-tests/ref_properties_pruned/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties_pruned/input-project/src/.gitkeep create mode 100644 rojo-test/syncback-tests/ref_properties_pruned/input.rbxl diff --git a/CHANGELOG.md b/CHANGELOG.md index 410a7cb0..7843cb44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ Making a new release? Simply add the new header with the version and date undern --> ## Unreleased +* Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179]) + +[#1179]: https://github.com/rojo-rbx/rojo/pull/1179 ## [7.7.0-rc.1] (November 27th, 2025) diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_pruned-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_pruned-stdout.snap new file mode 100644 index 00000000..3a9c2502 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_pruned-stdout.snap @@ -0,0 +1,7 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/Pointer1.model.json +Writing src/Pointer2.model.json +Writing src/Pointer3.model.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer1.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer1.model.json.snap new file mode 100644 index 00000000..aeee8256 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer1.model.json.snap @@ -0,0 +1,7 @@ +--- +source: tests/tests/syncback.rs +expression: src/Pointer1.model.json +--- +{ + "className": "ObjectValue" +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer2.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer2.model.json.snap new file mode 100644 index 00000000..87da5680 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer2.model.json.snap @@ -0,0 +1,7 @@ +--- +source: tests/tests/syncback.rs +expression: src/Pointer2.model.json +--- +{ + "className": "ObjectValue" +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer3.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer3.model.json.snap new file mode 100644 index 00000000..9c5e3b94 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_pruned-src__Pointer3.model.json.snap @@ -0,0 +1,7 @@ +--- +source: tests/tests/syncback.rs +expression: src/Pointer3.model.json +--- +{ + "className": "ObjectValue" +} diff --git a/rojo-test/syncback-tests/ref_properties_pruned/input-project/default.project.json b/rojo-test/syncback-tests/ref_properties_pruned/input-project/default.project.json new file mode 100644 index 00000000..9656479a --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_pruned/input-project/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "ref_properties_pruned", + "tree": { + "$className": "DataModel", + "ServerScriptService": { + "$path": "src" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_pruned/input-project/src/.gitkeep b/rojo-test/syncback-tests/ref_properties_pruned/input-project/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/ref_properties_pruned/input.rbxl b/rojo-test/syncback-tests/ref_properties_pruned/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..1513ff1197476f9dc2c31f31549e928373078930 GIT binary patch literal 48562 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 RefLinks { continue; } + let target = match dom.get_by_ref(*prop_value) { + Some(inst) => inst, + None => { + // Properties that are Some but point to nothing may as + // well be `nil`. Roblox and us never produce these values + // but syncback prunes trees without adjusting ref + // properties for performance reasons. + log::warn!( + "Property {}.{} will be `nil` on the disk because the actual value is not being included in syncback", + inst_path(dom, inst_ref), + prop_name + ); + continue; + } + }; + links.insert( inst_ref, RefLink { @@ -58,10 +77,6 @@ pub fn collect_referents(dom: &WeakDom) -> RefLinks { }, ); - let target = dom - .get_by_ref(*prop_value) - .expect("Refs in DOM should point to valid Instances"); - // 1. Check if target has an ID if let Some(id) = get_existing_id(target) { // If it does, we need to check whether that ID is a duplicate diff --git a/tests/tests/syncback.rs b/tests/tests/syncback.rs index 851b4e4d..31553496 100644 --- a/tests/tests/syncback.rs +++ b/tests/tests/syncback.rs @@ -71,8 +71,13 @@ syncback_tests! { ref_properties_conflict => ["src/Pointer_2.model.json", "src/Target_2.model.json"], // Ensures that having multiple pointers that are aimed at the same target doesn't trigger ref rewrites. ref_properties_duplicate => [], + // Ensures that ref properties that point to nothing after the prune both + // do not leave any trace of themselves + ref_properties_pruned => ["src/Pointer1.model.json", "src/Pointer2.model.json", "src/Pointer3.model.json"], // Ensures that the old middleware is respected during syncback respect_old_middleware => ["default.project.json", "src/model_json.model.json", "src/rbxm.rbxm", "src/rbxmx.rbxmx"], + // Ensures that the `$schema` field roundtrips with syncback + schema_roundtrip => ["default.project.json", "src/model.model.json", "src/init/init.meta.json", "src/adjacent.meta.json"], // Ensures that StringValues inside project files are written to the // project file, but only if they don't have `$path` set string_value_project => ["default.project.json"], @@ -81,5 +86,4 @@ syncback_tests! { sync_rules => ["src/module.modulescript", "src/text.text"], // Ensures that the `syncUnscriptable` setting works unscriptable_properties => ["default.project.json"], - schema_roundtrip => ["default.project.json", "src/model.model.json", "src/init/init.meta.json", "src/adjacent.meta.json"] } From 02b41133f8fa5adcf250a44f9bebac96225d6164 Mon Sep 17 00:00:00 2001 From: Ken Loeffler Date: Mon, 19 Jan 2026 14:44:42 -0800 Subject: [PATCH 4/7] Use post for ref patch and serialize (#1192) --- CHANGELOG.md | 2 ++ plugin/src/ApiContext.lua | 40 ++++++++++++--------- src/web/api.rs | 65 +++++++++++++++++++---------------- src/web/interface.rs | 14 ++++++++ tests/rojo_test/serve_util.rs | 30 +++++++++------- tests/tests/serve.rs | 6 ++-- 6 files changed, 98 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7843cb44..7e6aacdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,10 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased * Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179]) +* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192]) [#1179]: https://github.com/rojo-rbx/rojo/pull/1179 +[#1192]: https://github.com/rojo-rbx/rojo/pull/1192 ## [7.7.0-rc.1] (November 27th, 2025) diff --git a/plugin/src/ApiContext.lua b/plugin/src/ApiContext.lua index cbbffcc1..2d2459ec 100644 --- a/plugin/src/ApiContext.lua +++ b/plugin/src/ApiContext.lua @@ -290,31 +290,39 @@ function ApiContext:open(id) end function ApiContext:serialize(ids: { string }) - local url = ("%s/api/serialize/%s"):format(self.__baseUrl, table.concat(ids, ",")) + local url = ("%s/api/serialize"):format(self.__baseUrl) + local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids }) - return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) - if body.sessionId ~= self.__sessionId then - return Promise.reject("Server changed ID") - end + return Http.post(url, request_body) + :andThen(rejectFailedRequests) + :andThen(Http.Response.json) + :andThen(function(response_body) + if response_body.sessionId ~= self.__sessionId then + return Promise.reject("Server changed ID") + end - assert(validateApiSerialize(body)) + assert(validateApiSerialize(response_body)) - return body - end) + return response_body + end) end function ApiContext:refPatch(ids: { string }) - local url = ("%s/api/ref-patch/%s"):format(self.__baseUrl, table.concat(ids, ",")) + local url = ("%s/api/ref-patch"):format(self.__baseUrl) + local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids }) - return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) - if body.sessionId ~= self.__sessionId then - return Promise.reject("Server changed ID") - end + return Http.post(url, request_body) + :andThen(rejectFailedRequests) + :andThen(Http.Response.json) + :andThen(function(response_body) + if response_body.sessionId ~= self.__sessionId then + return Promise.reject("Server changed ID") + end - assert(validateApiRefPatch(body)) + assert(validateApiRefPatch(response_body)) - return body - end) + return response_body + end) end return ApiContext diff --git a/src/web/api.rs b/src/web/api.rs index 8e7ce72f..5fbd7b6d 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -1,13 +1,7 @@ //! Defines Rojo's HTTP API, all under /api. These endpoints generally return //! JSON. -use std::{ - collections::{HashMap, HashSet}, - fs, - path::PathBuf, - str::FromStr, - sync::Arc, -}; +use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc}; use futures::{sink::SinkExt, stream::StreamExt}; use hyper::{body, Body, Method, Request, Response, StatusCode}; @@ -30,7 +24,10 @@ use crate::{ }, util::{json, json_ok}, }, - web_api::{BufferEncode, InstanceUpdate, RefPatchResponse, SerializeResponse}, + web_api::{ + BufferEncode, InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest, + SerializeResponse, + }, }; pub async fn call(serve_session: Arc, mut request: Request) -> Response { @@ -53,12 +50,8 @@ pub async fn call(serve_session: Arc, mut request: Request) ) } } - (&Method::GET, path) if path.starts_with("/api/serialize/") => { - service.handle_api_serialize(request).await - } - (&Method::GET, path) if path.starts_with("/api/ref-patch/") => { - service.handle_api_ref_patch(request).await - } + (&Method::POST, "/api/serialize") => service.handle_api_serialize(request).await, + (&Method::POST, "/api/ref-patch") => service.handle_api_ref_patch(request).await, (&Method::POST, path) if path.starts_with("/api/open/") => { service.handle_api_open(request).await @@ -229,22 +222,30 @@ impl ApiService { /// that correspond to the requested Instances. These values have their /// `Value` property set to point to the requested Instance. async fn handle_api_serialize(&self, request: Request) -> Response { - let argument = &request.uri().path()["/api/serialize/".len()..]; - let requested_ids: Result, _> = argument.split(',').map(Ref::from_str).collect(); + let session_id = self.serve_session.session_id(); + let body = body::to_bytes(request.into_body()).await.unwrap(); - let requested_ids = match requested_ids { - Ok(ids) => ids, - Err(_) => { + let request: SerializeRequest = match json::from_slice(&body) { + Ok(request) => request, + Err(err) => { return json( - ErrorResponse::bad_request("Malformed ID list"), + ErrorResponse::bad_request(format!("Invalid body: {}", err)), StatusCode::BAD_REQUEST, ); } }; + + if request.session_id != session_id { + return json( + ErrorResponse::bad_request("Wrong session ID"), + StatusCode::BAD_REQUEST, + ); + } + let mut response_dom = WeakDom::new(InstanceBuilder::new("Folder")); let tree = self.serve_session.tree(); - for id in &requested_ids { + for id in &request.ids { if let Some(instance) = tree.get_instance(*id) { let clone = response_dom.insert( Ref::none(), @@ -290,20 +291,26 @@ impl ApiService { /// and referent properties need to be updated after the serialize /// endpoint is used. async fn handle_api_ref_patch(self, request: Request) -> Response { - let argument = &request.uri().path()["/api/ref-patch/".len()..]; - let requested_ids: Result, _> = - argument.split(',').map(Ref::from_str).collect(); + let session_id = self.serve_session.session_id(); + let body = body::to_bytes(request.into_body()).await.unwrap(); - let requested_ids = match requested_ids { - Ok(ids) => ids, - Err(_) => { + let request: RefPatchRequest = match json::from_slice(&body) { + Ok(request) => request, + Err(err) => { return json( - ErrorResponse::bad_request("Malformed ID list"), + ErrorResponse::bad_request(format!("Invalid body: {}", err)), StatusCode::BAD_REQUEST, ); } }; + if request.session_id != session_id { + return json( + ErrorResponse::bad_request("Wrong session ID"), + StatusCode::BAD_REQUEST, + ); + } + let mut instance_updates: HashMap = HashMap::new(); let tree = self.serve_session.tree(); @@ -312,7 +319,7 @@ impl ApiService { let Variant::Ref(prop_value) = prop_value else { continue; }; - if let Some(target_id) = requested_ids.get(prop_value) { + if let Some(target_id) = request.ids.get(prop_value) { let instance_id = instance.id(); let update = instance_updates diff --git a/src/web/interface.rs b/src/web/interface.rs index 4871ec97..430f13ec 100644 --- a/src/web/interface.rs +++ b/src/web/interface.rs @@ -238,6 +238,13 @@ pub struct OpenResponse { pub session_id: SessionId, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SerializeRequest { + pub session_id: SessionId, + pub ids: Vec, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SerializeResponse { @@ -269,6 +276,13 @@ impl BufferEncode { } } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RefPatchRequest { + pub session_id: SessionId, + pub ids: HashSet, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RefPatchResponse<'a> { diff --git a/tests/rojo_test/serve_util.rs b/tests/rojo_test/serve_util.rs index 9036316f..a5decdbb 100644 --- a/tests/rojo_test/serve_util.rs +++ b/tests/rojo_test/serve_util.rs @@ -1,5 +1,4 @@ use std::{ - fmt::Write as _, fs, path::{Path, PathBuf}, process::Command, @@ -13,8 +12,12 @@ use rbx_dom_weak::types::Ref; use tempfile::{tempdir, TempDir}; -use librojo::web_api::{ - ReadResponse, SerializeResponse, ServerInfoResponse, SocketPacket, SocketPacketType, +use librojo::{ + web_api::{ + ReadResponse, SerializeRequest, SerializeResponse, ServerInfoResponse, SocketPacket, + SocketPacketType, + }, + SessionId, }; use rojo_insta_ext::RedactionMap; @@ -226,16 +229,19 @@ impl TestServeSession { } } - pub fn get_api_serialize(&self, ids: &[Ref]) -> Result { - let mut id_list = String::with_capacity(ids.len() * 33); - for id in ids { - write!(id_list, "{id},").unwrap(); - } - id_list.pop(); + pub fn get_api_serialize( + &self, + ids: &[Ref], + session_id: SessionId, + ) -> Result { + let client = reqwest::blocking::Client::new(); + let url = format!("http://localhost:{}/api/serialize", self.port); + let body = serde_json::to_string(&SerializeRequest { + session_id, + ids: ids.to_vec(), + }); - let url = format!("http://localhost:{}/api/serialize/{}", self.port, id_list); - - reqwest::blocking::get(url)?.json() + client.post(url).body((body).unwrap()).send()?.json() } } diff --git a/tests/tests/serve.rs b/tests/tests/serve.rs index ee0528e1..748f1687 100644 --- a/tests/tests/serve.rs +++ b/tests/tests/serve.rs @@ -646,7 +646,7 @@ fn meshpart_with_id() { .unwrap(); let serialize_response = session - .get_api_serialize(&[*meshpart, *objectvalue]) + .get_api_serialize(&[*meshpart, *objectvalue], info.session_id) .unwrap(); // We don't assert a snapshot on the SerializeResponse because the model includes the @@ -673,7 +673,9 @@ fn forced_parent() { read_response.intern_and_redact(&mut redactions, root_id) ); - let serialize_response = session.get_api_serialize(&[root_id]).unwrap(); + let serialize_response = session + .get_api_serialize(&[root_id], info.session_id) + .unwrap(); assert_eq!(serialize_response.session_id, info.session_id); From 2a1102fc55a8d8afc1c9f8218d74b7b932ac2a1e Mon Sep 17 00:00:00 2001 From: Ivan Matthew <76107136+Ivanmatthew@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:04:59 +0100 Subject: [PATCH 5/7] Implement VFS Path normalization for improved cross-platform tree synchronization (#1201) --- .gitignore | 3 + CHANGELOG.md | 2 + Cargo.lock | 1 + crates/memofs/CHANGELOG.md | 1 + crates/memofs/Cargo.toml | 3 + crates/memofs/src/in_memory_fs.rs | 27 ++++++++ crates/memofs/src/lib.rs | 88 +++++++++++++++++++++++- crates/memofs/src/noop_backend.rs | 6 +- crates/memofs/src/std_backend.rs | 4 ++ src/change_processor.rs | 98 ++++++++++++++++----------- src/snapshot_middleware/csv.rs | 2 +- src/snapshot_middleware/dir.rs | 17 ++--- src/snapshot_middleware/json.rs | 2 +- src/snapshot_middleware/json_model.rs | 2 +- src/snapshot_middleware/lua.rs | 2 +- src/snapshot_middleware/rbxm.rs | 2 +- src/snapshot_middleware/rbxmx.rs | 2 +- src/snapshot_middleware/toml.rs | 2 +- src/snapshot_middleware/txt.rs | 2 +- src/snapshot_middleware/yaml.rs | 2 +- 20 files changed, 209 insertions(+), 59 deletions(-) diff --git a/.gitignore b/.gitignore index 61bdf0b2..a660919d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ # Macos file system junk ._* .DS_STORE + +# JetBrains IDEs +/.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6aacdf..d437eb6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,9 +32,11 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased * Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179]) * Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192]) +* Fixed a bug where MacOS paths weren't being handled correctly. ([#1201]) [#1179]: https://github.com/rojo-rbx/rojo/pull/1179 [#1192]: https://github.com/rojo-rbx/rojo/pull/1192 +[#1201]: https://github.com/rojo-rbx/rojo/pull/1201 ## [7.7.0-rc.1] (November 27th, 2025) diff --git a/Cargo.lock b/Cargo.lock index 7f9a58a7..8ac0676b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,6 +1319,7 @@ dependencies = [ "fs-err", "notify", "serde", + "tempfile", ] [[package]] diff --git a/crates/memofs/CHANGELOG.md b/crates/memofs/CHANGELOG.md index b83bfeaf..c35b5399 100644 --- a/crates/memofs/CHANGELOG.md +++ b/crates/memofs/CHANGELOG.md @@ -1,6 +1,7 @@ # memofs Changelog ## Unreleased Changes +* Added `Vfs::canonicalize`. [#1201] ## 0.3.1 (2025-11-27) * Added `Vfs::exists`. [#1169] diff --git a/crates/memofs/Cargo.toml b/crates/memofs/Cargo.toml index ad0b2637..d1d8cc5e 100644 --- a/crates/memofs/Cargo.toml +++ b/crates/memofs/Cargo.toml @@ -19,3 +19,6 @@ crossbeam-channel = "0.5.12" fs-err = "2.11.0" notify = "4.0.17" serde = { version = "1.0.197", features = ["derive"] } + +[dev-dependencies] +tempfile = "3.10.1" diff --git a/crates/memofs/src/in_memory_fs.rs b/crates/memofs/src/in_memory_fs.rs index 3c825575..41006f1c 100644 --- a/crates/memofs/src/in_memory_fs.rs +++ b/crates/memofs/src/in_memory_fs.rs @@ -232,6 +232,33 @@ impl VfsBackend for InMemoryFs { } } + // TODO: We rely on Rojo to prepend cwd to any relative path before storing paths + // in MemoFS. The current implementation will error if no prepended absolute path + // is found. It really only normalizes paths within the provided path's context. + // Example: "/Users/username/project/../other/file.txt" -> + // "/Users/username/other/file.txt" + // Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt" + // This is not very robust. We should implement proper path normalization here or otherwise + // warn if we are missing context and can not fully canonicalize the path correctly. + fn canonicalize(&mut self, path: &Path) -> io::Result { + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + std::path::Component::ParentDir => { + normalized.pop(); + } + std::path::Component::CurDir => {} + _ => normalized.push(component), + } + } + + let inner = self.inner.lock().unwrap(); + match inner.entries.get(&normalized) { + Some(_) => Ok(normalized), + None => not_found(&normalized), + } + } + fn event_receiver(&self) -> crossbeam_channel::Receiver { let inner = self.inner.lock().unwrap(); diff --git a/crates/memofs/src/lib.rs b/crates/memofs/src/lib.rs index bc7e329e..41d21100 100644 --- a/crates/memofs/src/lib.rs +++ b/crates/memofs/src/lib.rs @@ -77,6 +77,7 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static { fn metadata(&mut self, path: &Path) -> io::Result; fn remove_file(&mut self, path: &Path) -> io::Result<()>; fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>; + fn canonicalize(&mut self, path: &Path) -> io::Result; fn event_receiver(&self) -> crossbeam_channel::Receiver; fn watch(&mut self, path: &Path) -> io::Result<()>; @@ -225,6 +226,11 @@ impl VfsInner { self.backend.metadata(path) } + fn canonicalize>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + self.backend.canonicalize(path) + } + fn event_receiver(&self) -> crossbeam_channel::Receiver { self.backend.event_receiver() } @@ -413,6 +419,19 @@ impl Vfs { self.inner.lock().unwrap().metadata(path) } + /// Normalize a path via the underlying backend. + /// + /// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are + /// resolved against the backend's current working directory (if applicable) and errors are + /// surfaced directly from the backend. + /// + /// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html + #[inline] + pub fn canonicalize>(&self, path: P) -> io::Result { + let path = path.as_ref(); + self.inner.lock().unwrap().canonicalize(path) + } + /// Retrieve a handle to the event receiver for this `Vfs`. #[inline] pub fn event_receiver(&self) -> crossbeam_channel::Receiver { @@ -540,6 +559,13 @@ impl VfsLock<'_> { self.inner.metadata(path) } + /// Normalize a path via the underlying backend. + #[inline] + pub fn normalize>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + self.inner.canonicalize(path) + } + /// Retrieve a handle to the event receiver for this `Vfs`. #[inline] pub fn event_receiver(&self) -> crossbeam_channel::Receiver { @@ -555,7 +581,9 @@ impl VfsLock<'_> { #[cfg(test)] mod test { - use crate::{InMemoryFs, Vfs, VfsSnapshot}; + use crate::{InMemoryFs, StdBackend, Vfs, VfsSnapshot}; + use std::io; + use std::path::PathBuf; /// https://github.com/rojo-rbx/rojo/issues/899 #[test] @@ -571,4 +599,62 @@ mod test { "bar\nfoo\n\n" ); } + + /// https://github.com/rojo-rbx/rojo/issues/1200 + #[test] + fn canonicalize_in_memory_success() { + let mut imfs = InMemoryFs::new(); + let contents = "Lorem ipsum dolor sit amet.".to_string(); + + imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string())) + .unwrap(); + + let vfs = Vfs::new(imfs); + + assert_eq!( + vfs.canonicalize("/test/nested/../file.txt").unwrap(), + PathBuf::from("/test/file.txt") + ); + assert_eq!( + vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap()) + .unwrap() + .to_string(), + contents.to_string() + ); + } + + #[test] + fn canonicalize_in_memory_missing_errors() { + let imfs = InMemoryFs::new(); + let vfs = Vfs::new(imfs); + + let err = vfs.canonicalize("test").unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::NotFound); + } + + #[test] + fn canonicalize_std_backend_success() { + let contents = "Lorem ipsum dolor sit amet.".to_string(); + let dir = tempfile::tempdir().unwrap(); + let file_path = dir.path().join("file.txt"); + fs_err::write(&file_path, contents.to_string()).unwrap(); + + let vfs = Vfs::new(StdBackend::new()); + let canonicalized = vfs.canonicalize(&file_path).unwrap(); + assert_eq!(canonicalized, file_path.canonicalize().unwrap()); + assert_eq!( + vfs.read_to_string(&canonicalized).unwrap().to_string(), + contents.to_string() + ); + } + + #[test] + fn canonicalize_std_backend_missing_errors() { + let dir = tempfile::tempdir().unwrap(); + let file_path = dir.path().join("test"); + + let vfs = Vfs::new(StdBackend::new()); + let err = vfs.canonicalize(&file_path).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::NotFound); + } } diff --git a/crates/memofs/src/noop_backend.rs b/crates/memofs/src/noop_backend.rs index 98b86d1a..b1ee301d 100644 --- a/crates/memofs/src/noop_backend.rs +++ b/crates/memofs/src/noop_backend.rs @@ -1,5 +1,5 @@ use std::io; -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::{Metadata, ReadDir, VfsBackend, VfsEvent}; @@ -50,6 +50,10 @@ impl VfsBackend for NoopBackend { Err(io::Error::other("NoopBackend doesn't do anything")) } + fn canonicalize(&mut self, _path: &Path) -> io::Result { + Err(io::Error::other("NoopBackend doesn't do anything")) + } + fn event_receiver(&self) -> crossbeam_channel::Receiver { crossbeam_channel::never() } diff --git a/crates/memofs/src/std_backend.rs b/crates/memofs/src/std_backend.rs index f354d806..a25fa39d 100644 --- a/crates/memofs/src/std_backend.rs +++ b/crates/memofs/src/std_backend.rs @@ -106,6 +106,10 @@ impl VfsBackend for StdBackend { }) } + fn canonicalize(&mut self, path: &Path) -> io::Result { + fs_err::canonicalize(path) + } + fn event_receiver(&self) -> crossbeam_channel::Receiver { self.watcher_receiver.clone() } diff --git a/src/change_processor.rs b/src/change_processor.rs index 78d45acc..6c5c8224 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -1,12 +1,12 @@ -use std::{ - fs, - sync::{Arc, Mutex}, -}; - use crossbeam_channel::{select, Receiver, RecvError, Sender}; use jod_thread::JoinHandle; use memofs::{IoResultExt, Vfs, VfsEvent}; use rbx_dom_weak::types::{Ref, Variant}; +use std::path::PathBuf; +use std::{ + fs, + sync::{Arc, Mutex}, +}; use crate::{ message_queue::MessageQueue, @@ -114,6 +114,49 @@ struct JobThreadContext { } impl JobThreadContext { + /// Computes and applies patches to the DOM for a given file path. + /// + /// This function finds the nearest ancestor to the given path that has associated instances + /// in the tree. + /// It then computes and applies changes for each affected instance ID and + /// returns a vector of applied patch sets. + fn apply_patches(&self, path: PathBuf) -> Vec { + let mut tree = self.tree.lock().unwrap(); + let mut applied_patches = Vec::new(); + + // Find the nearest ancestor to this path that has + // associated instances in the tree. This helps make sure + // that we handle additions correctly, especially if we + // receive events for descendants of a large tree being + // created all at once. + let mut current_path = path.as_path(); + let affected_ids = loop { + let ids = tree.get_ids_at_path(current_path); + + log::trace!("Path {} affects IDs {:?}", current_path.display(), ids); + + if !ids.is_empty() { + break ids.to_vec(); + } + + log::trace!("Trying parent path..."); + match current_path.parent() { + Some(parent) => current_path = parent, + None => break Vec::new(), + } + }; + + for id in affected_ids { + if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) { + if !patch.is_empty() { + applied_patches.push(patch); + } + } + } + + applied_patches + } + fn handle_vfs_event(&self, event: VfsEvent) { log::trace!("Vfs event: {:?}", event); @@ -125,41 +168,16 @@ impl JobThreadContext { // For a given VFS event, we might have many changes to different parts // of the tree. Calculate and apply all of these changes. let applied_patches = match event { - VfsEvent::Create(path) | VfsEvent::Remove(path) | VfsEvent::Write(path) => { - let mut tree = self.tree.lock().unwrap(); - let mut applied_patches = Vec::new(); - - // Find the nearest ancestor to this path that has - // associated instances in the tree. This helps make sure - // that we handle additions correctly, especially if we - // receive events for descendants of a large tree being - // created all at once. - let mut current_path = path.as_path(); - let affected_ids = loop { - let ids = tree.get_ids_at_path(current_path); - - log::trace!("Path {} affects IDs {:?}", current_path.display(), ids); - - if !ids.is_empty() { - break ids.to_vec(); - } - - log::trace!("Trying parent path..."); - match current_path.parent() { - Some(parent) => current_path = parent, - None => break Vec::new(), - } - }; - - for id in affected_ids { - if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) { - if !patch.is_empty() { - applied_patches.push(patch); - } - } - } - - applied_patches + VfsEvent::Create(path) | VfsEvent::Write(path) => { + self.apply_patches(self.vfs.canonicalize(&path).unwrap()) + } + VfsEvent::Remove(path) => { + // MemoFS does not track parent removals yet, so we can canonicalize + // the parent path safely and then append the removed path's file name. + let parent = path.parent().unwrap(); + let file_name = path.file_name().unwrap(); + let parent_normalized = self.vfs.canonicalize(parent).unwrap(); + self.apply_patches(parent_normalized.join(file_name)) } _ => { log::warn!("Unhandled VFS event: {:?}", event); diff --git a/src/snapshot_middleware/csv.rs b/src/snapshot_middleware/csv.rs index 75a043d1..18fa1424 100644 --- a/src/snapshot_middleware/csv.rs +++ b/src/snapshot_middleware/csv.rs @@ -42,7 +42,7 @@ pub fn snapshot_csv( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]), + .relevant_paths(vec![vfs.canonicalize(path)?]), ); AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?; diff --git a/src/snapshot_middleware/dir.rs b/src/snapshot_middleware/dir.rs index b825542d..fb444c37 100644 --- a/src/snapshot_middleware/dir.rs +++ b/src/snapshot_middleware/dir.rs @@ -62,18 +62,19 @@ pub fn snapshot_dir_no_meta( } } + let normalized_path = vfs.canonicalize(path)?; let relevant_paths = vec![ - path.to_path_buf(), + normalized_path.clone(), // TODO: We shouldn't need to know about Lua existing in this // middleware. Should we figure out a way for that function to add // relevant paths to this middleware? - path.join("init.lua"), - path.join("init.luau"), - path.join("init.server.lua"), - path.join("init.server.luau"), - path.join("init.client.lua"), - path.join("init.client.luau"), - path.join("init.csv"), + normalized_path.join("init.lua"), + normalized_path.join("init.luau"), + normalized_path.join("init.server.lua"), + normalized_path.join("init.server.luau"), + normalized_path.join("init.client.lua"), + normalized_path.join("init.client.luau"), + normalized_path.join("init.csv"), ]; let snapshot = InstanceSnapshot::new() diff --git a/src/snapshot_middleware/json.rs b/src/snapshot_middleware/json.rs index 8cd029f5..1a9537ad 100644 --- a/src/snapshot_middleware/json.rs +++ b/src/snapshot_middleware/json.rs @@ -32,7 +32,7 @@ pub fn snapshot_json( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]) + .relevant_paths(vec![vfs.canonicalize(path)?]) .context(context), ); diff --git a/src/snapshot_middleware/json_model.rs b/src/snapshot_middleware/json_model.rs index d19792cb..fdc2069e 100644 --- a/src/snapshot_middleware/json_model.rs +++ b/src/snapshot_middleware/json_model.rs @@ -59,7 +59,7 @@ pub fn snapshot_json_model( snapshot.metadata = snapshot .metadata .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]) + .relevant_paths(vec![vfs.canonicalize(path)?]) .context(context) .specified_id(id) .schema(schema); diff --git a/src/snapshot_middleware/lua.rs b/src/snapshot_middleware/lua.rs index eaa90952..a0058555 100644 --- a/src/snapshot_middleware/lua.rs +++ b/src/snapshot_middleware/lua.rs @@ -88,7 +88,7 @@ pub fn snapshot_lua( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]) + .relevant_paths(vec![vfs.canonicalize(path)?]) .context(context), ); diff --git a/src/snapshot_middleware/rbxm.rs b/src/snapshot_middleware/rbxm.rs index f3693d52..6518be6a 100644 --- a/src/snapshot_middleware/rbxm.rs +++ b/src/snapshot_middleware/rbxm.rs @@ -28,7 +28,7 @@ pub fn snapshot_rbxm( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]) + .relevant_paths(vec![vfs.canonicalize(path)?]) .context(context), ); diff --git a/src/snapshot_middleware/rbxmx.rs b/src/snapshot_middleware/rbxmx.rs index 59aff679..8f122324 100644 --- a/src/snapshot_middleware/rbxmx.rs +++ b/src/snapshot_middleware/rbxmx.rs @@ -31,7 +31,7 @@ pub fn snapshot_rbxmx( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]) + .relevant_paths(vec![vfs.canonicalize(path)?]) .context(context), ); diff --git a/src/snapshot_middleware/toml.rs b/src/snapshot_middleware/toml.rs index 7262cfa9..3c42de6c 100644 --- a/src/snapshot_middleware/toml.rs +++ b/src/snapshot_middleware/toml.rs @@ -31,7 +31,7 @@ pub fn snapshot_toml( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]) + .relevant_paths(vec![vfs.canonicalize(path)?]) .context(context), ); diff --git a/src/snapshot_middleware/txt.rs b/src/snapshot_middleware/txt.rs index 715354e0..c56aacdd 100644 --- a/src/snapshot_middleware/txt.rs +++ b/src/snapshot_middleware/txt.rs @@ -28,7 +28,7 @@ pub fn snapshot_txt( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]) + .relevant_paths(vec![vfs.canonicalize(path)?]) .context(context), ); diff --git a/src/snapshot_middleware/yaml.rs b/src/snapshot_middleware/yaml.rs index c8132d49..10f744bc 100644 --- a/src/snapshot_middleware/yaml.rs +++ b/src/snapshot_middleware/yaml.rs @@ -37,7 +37,7 @@ pub fn snapshot_yaml( .metadata( InstanceMetadata::new() .instigating_source(path) - .relevant_paths(vec![path.to_path_buf()]) + .relevant_paths(vec![vfs.canonicalize(path)?]) .context(context), ); From 68eab3479a9ec6be11f470d83849933b6230e915 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 19 Jan 2026 16:35:19 -0800 Subject: [PATCH 6/7] Fix notification unmount thread cancel bug (#1211) --- CHANGELOG.md | 3 +++ .../Components/Notifications/FullscreenNotification.lua | 8 +++++++- plugin/src/App/Components/Notifications/Notification.lua | 8 +++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d437eb6d..95b95859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,13 +30,16 @@ Making a new release? Simply add the new header with the version and date undern --> ## Unreleased + * Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179]) * Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192]) * Fixed a bug where MacOS paths weren't being handled correctly. ([#1201]) +* Fixed a bug where the notification timeout thread would fail to cancel on unmount ([#1211]) [#1179]: https://github.com/rojo-rbx/rojo/pull/1179 [#1192]: https://github.com/rojo-rbx/rojo/pull/1192 [#1201]: https://github.com/rojo-rbx/rojo/pull/1201 +[#1211]: https://github.com/rojo-rbx/rojo/pull/1211 ## [7.7.0-rc.1] (November 27th, 2025) diff --git a/plugin/src/App/Components/Notifications/FullscreenNotification.lua b/plugin/src/App/Components/Notifications/FullscreenNotification.lua index 215398b2..47d0ae6f 100644 --- a/plugin/src/App/Components/Notifications/FullscreenNotification.lua +++ b/plugin/src/App/Components/Notifications/FullscreenNotification.lua @@ -19,9 +19,15 @@ local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotifi function FullscreenNotification:init() self.transparency, self.setTransparency = Roact.createBinding(0) self.lifetime = self.props.timeout + self.dismissed = false end function FullscreenNotification:dismiss() + if self.dismissed then + return + end + self.dismissed = true + if self.props.onClose then self.props.onClose() end @@ -59,7 +65,7 @@ function FullscreenNotification:didMount() end function FullscreenNotification:willUnmount() - if self.timeout and coroutine.status(self.timeout) ~= "dead" then + if self.timeout and coroutine.status(self.timeout) == "suspended" then task.cancel(self.timeout) end end diff --git a/plugin/src/App/Components/Notifications/Notification.lua b/plugin/src/App/Components/Notifications/Notification.lua index 47fb0d08..1d9464c9 100644 --- a/plugin/src/App/Components/Notifications/Notification.lua +++ b/plugin/src/App/Components/Notifications/Notification.lua @@ -25,6 +25,7 @@ function Notification:init() self.binding = bindingUtil.fromMotor(self.motor) self.lifetime = self.props.timeout + self.dismissed = false self.motor:onStep(function(value) if value <= 0 and self.props.onClose then @@ -34,6 +35,11 @@ function Notification:init() end function Notification:dismiss() + if self.dismissed then + return + end + self.dismissed = true + self.motor:setGoal(Flipper.Spring.new(0, { frequency = 5, dampingRatio = 1, @@ -75,7 +81,7 @@ function Notification:didMount() end function Notification:willUnmount() - if self.timeout and coroutine.status(self.timeout) ~= "dead" then + if self.timeout and coroutine.status(self.timeout) == "suspended" then task.cancel(self.timeout) end end From 4965165ad5b751ba1ebdb57967a904df71f492a7 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 23 Jan 2026 13:15:34 -0800 Subject: [PATCH 7/7] Add option to forget prior info for place in reminder notif (#1215) --- CHANGELOG.md | 2 ++ plugin/src/App/init.lua | 76 +++++++++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b95859..a246ebf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,11 +35,13 @@ Making a new release? Simply add the new header with the version and date undern * Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192]) * Fixed a bug where MacOS paths weren't being handled correctly. ([#1201]) * Fixed a bug where the notification timeout thread would fail to cancel on unmount ([#1211]) +* Added a "Forget" option to the sync reminder notification to avoid being reminded for that place in the future ([#1215]) [#1179]: https://github.com/rojo-rbx/rojo/pull/1179 [#1192]: https://github.com/rojo-rbx/rojo/pull/1192 [#1201]: https://github.com/rojo-rbx/rojo/pull/1201 [#1211]: https://github.com/rojo-rbx/rojo/pull/1211 +[#1215]: https://github.com/rojo-rbx/rojo/pull/1215 ## [7.7.0-rc.1] (November 27th, 2025) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index c83d9d1c..e6e4deee 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -301,6 +301,19 @@ function App:setPriorSyncInfo(host: string, port: string, projectName: string) Settings:set("priorEndpoints", priorSyncInfos) end +function App:forgetPriorSyncInfo() + local priorSyncInfos = Settings:get("priorEndpoints") + if not priorSyncInfos then + priorSyncInfos = {} + end + + local id = tostring(game.PlaceId) + priorSyncInfos[id] = nil + Log.trace("Erased last used endpoint for {}", game.PlaceId) + + Settings:set("priorEndpoints", priorSyncInfos) +end + function App:getHostAndPort() local host = self.host:getValue() local port = self.port:getValue() @@ -435,7 +448,8 @@ function App:checkSyncReminder() self:findActiveServer() :andThen(function(serverInfo, host, port) self:sendSyncReminder( - `Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?` + `Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`, + { "Connect", "Dismiss" } ) end) :catch(function() @@ -446,7 +460,8 @@ function App:checkSyncReminder() local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp) self:sendSyncReminder( - `You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?` + `You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`, + { "Connect", "Forget", "Dismiss" } ) end end) @@ -486,12 +501,16 @@ function App:stopSyncReminderPolling() end end -function App:sendSyncReminder(message: string) +function App:sendSyncReminder(message: string, shownActions: { string }) local syncReminderMode = Settings:get("syncReminderMode") if syncReminderMode == "None" then return end + local connectIndex = table.find(shownActions, "Connect") + local forgetIndex = table.find(shownActions, "Forget") + local dismissIndex = table.find(shownActions, "Dismiss") + self.dismissSyncReminder = self:addNotification({ text = message, timeout = 120, @@ -500,24 +519,39 @@ function App:sendSyncReminder(message: string) self.dismissSyncReminder = nil end, actions = { - Connect = { - text = "Connect", - style = "Solid", - layoutOrder = 1, - onClick = function() - self:startSession() - end, - }, - Dismiss = { - text = "Dismiss", - style = "Bordered", - layoutOrder = 2, - onClick = function() - -- If the user dismisses the reminder, - -- then we don't need to remind them again - self:stopSyncReminderPolling() - end, - }, + Connect = if connectIndex + then { + text = "Connect", + style = "Solid", + layoutOrder = connectIndex, + onClick = function() + self:startSession() + end, + } + else nil, + Forget = if forgetIndex + then { + text = "Forget", + style = "Bordered", + layoutOrder = forgetIndex, + onClick = function() + -- The user doesn't want to be reminded again about this sync + self:forgetPriorSyncInfo() + end, + } + else nil, + Dismiss = if dismissIndex + then { + text = "Dismiss", + style = "Bordered", + layoutOrder = dismissIndex, + onClick = function() + -- If the user dismisses the reminder, + -- then we don't need to remind them again + self:stopSyncReminderPolling() + end, + } + else nil, }, }) end