Merge branch 'master' of https://github.com/boatbomber/rojo into headless-api

This commit is contained in:
boatbomber 2025-05-22 10:57:29 -07:00
commit 5f4c2a7a3f
82 changed files with 10139 additions and 2429 deletions

View file

@ -1 +1 @@
7.5.0-prealpha
7.5.1

View file

@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
elseif valueType == "table" then
local valueMeta = getmetatable(value)
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
-- This type implement's the metamethod we made up to line up with
-- Rust's 'Debug' trait.
@ -242,4 +242,4 @@ return {
debugOutputBuffer = debugOutputBuffer,
fmt = fmt,
debugify = debugify,
}
}

View file

@ -31,4 +31,4 @@ function Response:json()
return HttpService:JSONDecode(self.body)
end
return Response
return Response

View file

@ -2,4 +2,4 @@ return function()
it("should load", function()
require(script.Parent)
end)
end
end

View file

@ -57,4 +57,4 @@ function Log.error(template, ...)
error(Fmt.fmt(template, ...))
end
return Log
return Log

View file

@ -2,4 +2,4 @@ return function()
it("should load", function()
require(script.Parent)
end)
end
end

View file

@ -188,6 +188,38 @@ types = {
},
Content = {
fromPod = function(pod): Content
if type(pod) == "string" then
if pod == "None" then
return Content.none
else
error(`unexpected Content value '{pod}'`)
end
else
local ty, value = next(pod)
if ty == "Uri" then
return Content.fromUri(value)
elseif ty == "Object" then
error("Object deserializing is not currently implemented")
else
error(`Unknown Content type '{ty}' (could not deserialize)`)
end
end
end,
toPod = function(roblox: Content)
if roblox.SourceType == Enum.ContentSourceType.None then
return "None"
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
return { Uri = roblox.Uri }
elseif roblox.SourceType == Enum.ContentSourceType.Object then
error("Object serializing is not currently implemented")
else
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
end
end,
},
ContentId = {
fromPod = identity,
toPod = identity,
},
@ -205,6 +237,19 @@ types = {
end,
},
EnumItem = {
fromPod = function(pod)
return Enum[pod.type]:FromValue(pod.value)
end,
toPod = function(roblox)
return {
type = tostring(roblox.EnumType),
value = roblox.Value,
}
end,
},
Faces = {
fromPod = function(pod)
local faces = {}
@ -300,7 +345,12 @@ types = {
local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
-- TODO: Add a test for NaN or Infinity values and envelopes
-- Right now it isn't possible because it'd fail the roundtrip.
-- It's more important that it works right now, though.
local value = keypoint.value or 0
local envelope = keypoint.envelope or 0
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
end
return NumberSequence.new(keypoints)

View file

@ -5,6 +5,7 @@ Error.Kind = {
UnknownProperty = "UnknownProperty",
PropertyNotReadable = "PropertyNotReadable",
PropertyNotWritable = "PropertyNotWritable",
CannotParseBinaryString = "CannotParseBinaryString",
Roblox = "Roblox",
}

View file

@ -15,6 +15,12 @@
0.0
]
},
"TestEnumItem": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"TestNumber": {
"Float64": 1337.0
},
@ -170,9 +176,23 @@
},
"ty": "ColorSequence"
},
"Content": {
"ContentId": {
"value": {
"Content": "rbxassetid://12345"
"ContentId": "rbxassetid://12345"
},
"ty": "ContentId"
},
"Content_None": {
"value": {
"Content": "None"
},
"ty": "Content"
},
"Content_Uri": {
"value": {
"Content": {
"Uri": "rbxasset://abc/123.rojo"
}
},
"ty": "Content"
},
@ -182,6 +202,15 @@
},
"ty": "Enum"
},
"EnumItem": {
"value": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"ty": "EnumItem"
},
"Faces": {
"value": {
"Faces": [

View file

@ -1,6 +1,8 @@
local CollectionService = game:GetService("CollectionService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local Error = require(script.Parent.Error)
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
local TERRAIN_MATERIAL_COLORS = {
Enum.Material.Grass,
@ -51,6 +53,10 @@ return {
return true, instance:GetAttributes()
end,
write = function(instance, _, value)
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
local existing = instance:GetAttributes()
local didAllWritesSucceed = true
@ -160,9 +166,14 @@ return {
return true, colors
end,
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
for material, color in value do
instance:SetMaterialColor(material, color)
end
return true
end,
},

File diff suppressed because it is too large Load diff

View file

@ -45,14 +45,7 @@ end
local function rejectWrongPlaceId(infoResponseBody)
if infoResponseBody.expectedPlaceIds ~= nil then
local foundId = false
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
if id == game.PlaceId then
foundId = true
break
end
end
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId)
if not foundId then
local idList = {}
@ -62,10 +55,30 @@ local function rejectWrongPlaceId(infoResponseBody)
local message = (
"Found a Rojo server, but its project is set to only be used with a specific list of places."
.. "\nYour place ID is %s, but needs to be one of these:"
.. "\nYour place ID is %u, but needs to be one of these:"
.. "\n%s"
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
):format(tostring(game.PlaceId), table.concat(idList, "\n"))
):format(game.PlaceId, table.concat(idList, "\n"))
return Promise.reject(message)
end
end
if infoResponseBody.unexpectedPlaceIds ~= nil then
local foundId = table.find(infoResponseBody.unexpectedPlaceIds, game.PlaceId)
if foundId then
local idList = {}
for _, id in ipairs(infoResponseBody.unexpectedPlaceIds) do
table.insert(idList, "- " .. tostring(id))
end
local message = (
"Found a Rojo server, but its project is set to not be used with a specific list of places."
.. "\nYour place ID is %u, but needs to not be one of these:"
.. "\n%s"
.. "\n\nTo change this list, edit 'blockedPlaceIds' in your .project.json file."
):format(game.PlaceId, table.concat(idList, "\n"))
return Promise.reject(message)
end

View file

@ -16,6 +16,7 @@ local invariant = require(script.Parent.Parent.invariant)
local decodeValue = require(script.Parent.decodeValue)
local reify = require(script.Parent.reify)
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
local setProperty = require(script.Parent.setProperty)
local function applyPatch(instanceMap, patch)
@ -29,6 +30,11 @@ local function applyPatch(instanceMap, patch)
-- Tracks any portions of the patch that could not be applied to the DOM.
local unappliedPatch = PatchSet.newEmpty()
-- Contains a list of all of the ref properties that we'll need to assign.
-- It is imperative that refs are assigned after all instances are created
-- to ensure that referents can be mapped to instances correctly.
local deferredRefs = {}
for _, removedIdOrInstance in ipairs(patch.removed) do
local removeInstanceSuccess = pcall(function()
if Types.RbxId(removedIdOrInstance) then
@ -78,7 +84,7 @@ local function applyPatch(instanceMap, patch)
)
end
local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
local failedToReify = reifyInstance(deferredRefs, instanceMap, patch.added, id, parentInstance)
if not PatchSet.isEmpty(failedToReify) then
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
@ -143,7 +149,7 @@ local function applyPatch(instanceMap, patch)
[update.id] = mockVirtualInstance,
}
local failedToReify = reify(instanceMap, mockAdded, update.id, instance.Parent)
local failedToReify = reifyInstance(deferredRefs, instanceMap, mockAdded, update.id, instance.Parent)
local newInstance = instanceMap.fromIds[update.id]
@ -206,6 +212,18 @@ local function applyPatch(instanceMap, patch)
if update.changedProperties ~= nil then
for propertyName, propertyValue in pairs(update.changedProperties) do
-- Because refs may refer to instances that we haven't constructed yet,
-- we defer applying any ref properties until all instances are created.
if next(propertyValue) == "Ref" then
table.insert(deferredRefs, {
id = update.id,
instance = instance,
propertyName = propertyName,
virtualValue = propertyValue,
})
continue
end
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
if not decodeSuccess then
unappliedUpdate.changedProperties[propertyName] = propertyValue
@ -230,6 +248,8 @@ local function applyPatch(instanceMap, patch)
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
end
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
return unappliedPatch
end

View file

@ -151,7 +151,24 @@ local function diff(instanceMap, virtualInstances, rootId)
if getProperySuccess then
local existingValue = existingValueOrErr
local decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
local decodeSuccess, decodedValue
-- If `virtualValue` is a ref then instead of decoding it to an instance,
-- we change `existingValue` to be a ref. This is because `virtualValue`
-- may point to an Instance which doesn't exist yet and therefore
-- decoding it may throw an error.
if next(virtualValue) == "Ref" then
decodeSuccess, decodedValue = true, virtualValue
if existingValue and typeof(existingValue) == "Instance" then
local existingValueRef = instanceMap.fromInstances[existingValue]
if existingValueRef then
existingValue = { Ref = existingValueRef }
end
end
else
decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
end
if decodeSuccess then
if not trueEquals(existingValue, decodedValue) then

View file

@ -7,26 +7,6 @@ local PatchSet = require(script.Parent.Parent.PatchSet)
local setProperty = require(script.Parent.setProperty)
local decodeValue = require(script.Parent.decodeValue)
local reifyInner, applyDeferredRefs
local function reify(instanceMap, virtualInstances, rootId, parentInstance)
-- Create an empty patch that will be populated with any parts of this reify
-- that could not happen, like instances that couldn't be created and
-- properties that could not be assigned.
local unappliedPatch = PatchSet.newEmpty()
-- Contains a list of all of the ref properties that we'll need to assign
-- after all instances are created. We apply refs in a second pass, after
-- we create as many instances as we can, so that we ensure that referents
-- can be mapped to instances correctly.
local deferredRefs = {}
reifyInner(instanceMap, virtualInstances, rootId, parentInstance, unappliedPatch, deferredRefs)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
return unappliedPatch
end
--[[
Add the given ID and all of its descendants in virtualInstances to the given
PatchSet, marked for addition.
@ -40,10 +20,21 @@ local function addAllToPatch(patchSet, virtualInstances, id)
end
end
function reifyInstance(deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
-- Create an empty patch that will be populated with any parts of this reify
-- that could not happen, like instances that couldn't be created and
-- properties that could not be assigned.
local unappliedPatch = PatchSet.newEmpty()
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
return unappliedPatch
end
--[[
Inner function that defines the core routine.
]]
function reifyInner(instanceMap, virtualInstances, id, parentInstance, unappliedPatch, deferredRefs)
function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, id, parentInstance)
local virtualInstance = virtualInstances[id]
if virtualInstance == nil then
@ -102,7 +93,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
end
for _, childId in ipairs(virtualInstance.Children) do
reifyInner(instanceMap, virtualInstances, childId, instance, unappliedPatch, deferredRefs)
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance)
end
instance.Parent = parentInstance
@ -143,6 +134,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end
local targetInstance = instanceMap.fromIds[refId]
if targetInstance == nil then
markFailed(entry.id, entry.propertyName, entry.virtualValue)
continue
@ -155,4 +147,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end
end
return reify
return {
reifyInstance = reifyInstance,
applyDeferredRefs = applyDeferredRefs,
}

View file

@ -1,5 +1,6 @@
return function()
local reify = require(script.Parent.reify)
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
local PatchSet = require(script.Parent.Parent.PatchSet)
local InstanceMap = require(script.Parent.Parent.InstanceMap)
@ -20,7 +21,11 @@ return function()
it("should throw when given a bogus ID", function()
expect(function()
reify(InstanceMap.new(), {}, "Hi, mom!", game)
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, {}, "Hi, mom!", game)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end).to.throw()
end)
@ -34,8 +39,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT", nil)
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT", nil)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(instanceMap:size() == 0, "expected instanceMap to be empty")
@ -60,8 +68,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -90,8 +101,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -122,8 +136,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
expect(size(unappliedPatch.added)).to.equal(1)
expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
@ -153,8 +170,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
local instance = instanceMap.fromIds["ROOT"]
expect(instance.ClassName).to.equal("StringValue")
@ -196,8 +216,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -223,13 +246,16 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local existing = Instance.new("Folder")
existing.Name = "Existing"
instanceMap:insert("EXISTING", existing)
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -268,8 +294,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -307,8 +336,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@ -332,8 +364,11 @@ return function()
},
}
local deferredRefs = {}
local instanceMap = InstanceMap.new()
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(not PatchSet.hasRemoves(unappliedPatch), "expected no removes")
assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions")