This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,12 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"LocalShadow": "fatal",
"LocalUnused": "fatal",
"ImportUnused": "fatal",
"DeprecatedGlobal": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,247 @@
local CorePackages = game:GetService("CorePackages")
local Cryo = require(CorePackages.Cryo)
-- switch this to Cryo.List.toSet when available
local function convertArrayToTable(array)
local result = {}
for _, v in pairs(array) do
result[v] = true
end
return result
end
local Constants = {}
Constants.MAX_HAT_TRIANGLES = 4000
Constants.MAX_TEXTURE_SIZE = 256
Constants.MATERIAL_WHITELIST = convertArrayToTable({
Enum.Material.Plastic,
})
Constants.BANNED_CLASS_NAMES = {
"Script",
"LocalScript",
"ModuleScript",
"ParticleEmitter",
"Fire",
"Smoke",
"Sparkles",
}
Constants.R6_BODY_PARTS = {
"Torso",
"Left Leg",
"Right Leg",
"Left Arm",
"Right Arm",
}
Constants.R15_BODY_PARTS = {
"UpperTorso",
"LowerTorso",
"LeftUpperLeg",
"LeftLowerLeg",
"LeftFoot",
"RightUpperLeg",
"RightLowerLeg",
"RightFoot",
"LeftUpperArm",
"LeftLowerArm",
"LeftHand",
"RightUpperArm",
"RightLowerArm",
"RightHand",
}
Constants.EXTRA_BANNED_NAMES = {
"Head",
"HumanoidRootPart",
"Humanoid",
}
if game:GetFastFlag("UGCExtraBannedNames") then
local extraBannedNames = {
"Body Colors",
"Shirt Graphic",
"Shirt",
"Pants",
"Health",
"Animate",
}
for _, name in ipairs(extraBannedNames) do
table.insert(Constants.EXTRA_BANNED_NAMES, name)
end
end
Constants.BANNED_NAMES = convertArrayToTable(Cryo.Dictionary.join(
Constants.R6_BODY_PARTS,
Constants.R15_BODY_PARTS,
Constants.EXTRA_BANNED_NAMES
))
Constants.ASSET_STATUS = {
UNKNOWN = "Unknown",
REVIEW_PENDING = "ReviewPending",
MODERATED = "Moderated",
}
-- https://confluence.rbx.com/display/AVATAR/UGC+Accessory+Max+Sizes
-- Measurements are doubled to account full size
-- boundsOffset is used when measurements are non-symmetrical
-- i.e. WaistAccessory is 3 behind, 2.5 front
Constants.ASSET_TYPE_INFO = {}
Constants.ASSET_TYPE_INFO[Enum.AssetType.Hat] = {
attachmentNames = { "HatAttachment" },
bounds = {
HatAttachment = {
size = Vector3.new(3, 4, 3),
},
},
}
Constants.ASSET_TYPE_INFO[Enum.AssetType.HairAccessory] = {
attachmentNames = { "HairAttachment" },
bounds = {
HairAttachment = {
size = Vector3.new(3, 5, 3.5),
offset = Vector3.new(0, -0.5, 0.25),
},
},
}
local FACE_BOUNDS = { size = Vector3.new(3, 2, 2) }
Constants.ASSET_TYPE_INFO[Enum.AssetType.FaceAccessory] = {
attachmentNames = { "FaceFrontAttachment", "FaceCenterAttachment" },
bounds = {
FaceFrontAttachment = FACE_BOUNDS,
FaceCenterAttachment = FACE_BOUNDS,
},
}
Constants.ASSET_TYPE_INFO[Enum.AssetType.NeckAccessory] = {
attachmentNames = { "NeckAttachment" },
bounds = {
NeckAttachment = { size = Vector3.new(3, 3, 2) },
},
}
local SHOULDER_BOUNDS = { size = Vector3.new(3, 3, 3) }
Constants.ASSET_TYPE_INFO[Enum.AssetType.ShoulderAccessory] = {
attachmentNames = {
"NeckAttachment",
"LeftCollarAttachment",
"RightCollarAttachment",
"LeftShoulderAttachment",
"RightShoulderAttachment",
},
bounds = {
NeckAttachment = { size = Vector3.new(7, 3, 3) },
LeftCollarAttachment = SHOULDER_BOUNDS,
RightCollarAttachment = SHOULDER_BOUNDS,
LeftShoulderAttachment = SHOULDER_BOUNDS,
RightShoulderAttachment = SHOULDER_BOUNDS,
},
}
Constants.ASSET_TYPE_INFO[Enum.AssetType.FrontAccessory] = {
attachmentNames = { "BodyFrontAttachment" },
bounds = {
BodyFrontAttachment = { size = Vector3.new(3, 3, 3) },
},
}
Constants.ASSET_TYPE_INFO[Enum.AssetType.BackAccessory] = {
attachmentNames = { "BodyBackAttachment" },
bounds = {
BodyBackAttachment = {
size = Vector3.new(10, 7, 4.5),
offset = Vector3.new(0, 0, 0.75),
},
},
}
local WAIST_BOUNDS = {
size = Vector3.new(4, 3.5, 7),
offset = Vector3.new(0, -0.25, 0),
}
Constants.ASSET_TYPE_INFO[Enum.AssetType.WaistAccessory] = {
attachmentNames = {
"WaistBackAttachment",
"WaistFrontAttachment",
"WaistCenterAttachment",
},
bounds = {
WaistBackAttachment = WAIST_BOUNDS,
WaistFrontAttachment = WAIST_BOUNDS,
WaistCenterAttachment = WAIST_BOUNDS,
}
}
Constants.PROPERTIES = {
Instance = {
Archivable = true,
},
Attachment = {
Visible = false,
},
SpecialMesh = {
MeshType = Enum.MeshType.FileMesh,
Offset = Vector3.new(0, 0, 0),
VertexColor = Vector3.new(1, 1, 1),
},
BasePart = {
Anchored = false,
Color = BrickColor.new("Medium stone grey").Color, -- luacheck: ignore BrickColor
CollisionGroupId = 0, -- collision groups can change by place
CustomPhysicalProperties = Cryo.None, -- ensure CustomPhysicalProperties is _not_ defined
Elasticity = 0.5,
Friction = 0.3,
LocalTransparencyModifier = 0,
Massless = false, -- this is already done by accessories internally
Reflectance = 0,
RootPriority = 0,
RotVelocity = Vector3.new(0, 0, 0),
Transparency = 0,
Velocity = Vector3.new(0, 0, 0),
-- surface properties
BackParamA = -0.5,
BackParamB = 0.5,
BackSurfaceInput = Enum.InputType.NoInput,
BottomParamA = -0.5,
BottomParamB = 0.5,
BottomSurfaceInput = Enum.InputType.NoInput,
FrontParamA = -0.5,
FrontParamB = 0.5,
FrontSurfaceInput = Enum.InputType.NoInput,
LeftParamA = -0.5,
LeftParamB = 0.5,
LeftSurfaceInput = Enum.InputType.NoInput,
RightParamA = -0.5,
RightParamB = 0.5,
RightSurfaceInput = Enum.InputType.NoInput,
TopParamA = -0.5,
TopParamB = 0.5,
TopSurfaceInput = Enum.InputType.NoInput,
BackSurface = Enum.SurfaceType.Smooth,
BottomSurface = Enum.SurfaceType.Smooth,
FrontSurface = Enum.SurfaceType.Smooth,
LeftSurface = Enum.SurfaceType.Smooth,
RightSurface = Enum.SurfaceType.Smooth,
TopSurface = Enum.SurfaceType.Smooth,
},
Part = {
Shape = Enum.PartType.Block,
},
}
return Constants
@@ -0,0 +1,96 @@
game:DefineFastFlag("UGCValidateMeshBounds", false)
game:DefineFastFlag("UGCValidateHandleSize", false)
game:DefineFastFlag("UGCExtraBannedNames", false)
local root = script
local validateInstanceTree = require(root.validation.validateInstanceTree)
local validateMeshTriangles = require(root.validation.validateMeshTriangles)
local validateModeration = require(root.validation.validateModeration)
local validateMaterials = require(root.validation.validateMaterials)
local validateTags = require(root.validation.validateTags)
local validateMeshBounds = require(root.validation.validateMeshBounds)
local validateTextureSize = require(root.validation.validateTextureSize)
local validateHandleSize = require(root.validation.validateHandleSize)
local validateProperties = require(root.validation.validateProperties)
local function validateInternal(isAsync, instances, assetTypeEnum, noModeration)
-- validate that only one instance was selected
if #instances == 0 then
return false, { "No instances selected" }
elseif #instances > 1 then
return false, { "More than one instance selected" }
end
local instance = instances[1]
local success, reasons
success, reasons = validateInstanceTree(instance, assetTypeEnum)
if not success then
return false, reasons
end
success, reasons = validateMaterials(instance)
if not success then
return false, reasons
end
success, reasons = validateProperties(instance)
if not success then
return false, reasons
end
success, reasons = validateTags(instance)
if not success then
return false, reasons
end
if game:GetFastFlag("UGCValidateMeshBounds") then
success, reasons = validateMeshBounds(isAsync, instance, assetTypeEnum)
if not success then
return false, reasons
end
end
success, reasons = validateTextureSize(isAsync, instance)
if not success then
return false, reasons
end
if game:GetFastFlag("UGCValidateHandleSize") then
success, reasons = validateHandleSize(isAsync, instance)
if not success then
return false, reasons
end
end
success, reasons = validateMeshTriangles(isAsync, instance)
if not success then
return false, reasons
end
if not noModeration then
success, reasons = validateModeration(isAsync, instance)
if not success then
return false, reasons
end
end
return true
end
local UGCValidation = {}
function UGCValidation.validate(instances, assetTypeEnum, noModeration)
local success, reasons = validateInternal(--[[ isAsync = ]] false, instances, assetTypeEnum, noModeration)
return success, reasons
end
function UGCValidation.validateAsync(instances, assetTypeEnum, callback, noModeration)
coroutine.wrap(function()
callback(validateInternal(--[[ isAsync = ]] true, instances, assetTypeEnum, noModeration))
end)()
end
return UGCValidation
@@ -0,0 +1,47 @@
local function createAccessorySchema(attachmentName)
assert(attachmentName, "attachmentName cannot be nil")
return {
ClassName = "Accessory",
_children = {
{
Name = "ThumbnailConfiguration",
ClassName = "Configuration",
_optional = true,
_children = {
{
Name = "ThumbnailCameraTarget",
ClassName = "ObjectValue",
},
{
Name = "ThumbnailCameraValue",
ClassName = "CFrameValue",
},
},
},
{
Name = "Handle",
ClassName = "Part",
_children = {
{
Name = attachmentName,
ClassName = "Attachment",
},
{
ClassName = "SpecialMesh",
},
{
ClassName = "StringValue",
Name = "AvatarPartScaleType",
_optional = true,
},
{
ClassName = "TouchTransmitter",
_optional = true,
},
}
},
},
}
end
return createAccessorySchema
@@ -0,0 +1,56 @@
local HttpService = game:GetService("HttpService")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local ContentProvider = game:GetService("ContentProvider")
local function getBaseDomain()
local baseUrl = ContentProvider.BaseUrl
if string.sub(baseUrl, #baseUrl) ~= "/" then
baseUrl = baseUrl .. "/"
end
local _, schemeEnd = string.find(baseUrl, "://")
local _, prefixEnd = string.find(baseUrl, "%.", schemeEnd + 1)
return string.sub(baseUrl, prefixEnd + 1)
end
local MAX_RETRIES = 5
local function requestAndRetry(apiUrl, data, attempt)
if attempt == nil then
attempt = 0
end
local success, response = pcall(function()
return HttpRbxApiService:PostAsyncFullUrl(apiUrl, data)
end)
if success then
return true, response
elseif attempt >= MAX_RETRIES then
return false, response
else
local timeToWait = 2^(attempt - 1)
wait(timeToWait)
return requestAndRetry(apiUrl, data, attempt + 1)
end
end
local BASE_DOMAIN = getBaseDomain()
local ITEM_CONFIGURATION_URL = string.format("https://itemconfiguration.%s", BASE_DOMAIN)
local GET_ASSET_CREATION_DETAILS_URL = ITEM_CONFIGURATION_URL .. "v1/creations/get-asset-details"
local function getAssetCreationDetails(isAsync, assetIds)
local success, response = requestAndRetry(
GET_ASSET_CREATION_DETAILS_URL,
HttpService:JSONEncode({ assetIds = assetIds })
)
-- TODO: isAsync
if success then
return true, HttpService:JSONDecode(response)
else
return false, response
end
end
return getAssetCreationDetails
@@ -0,0 +1,100 @@
local function checkName(nameList, instanceName)
if type(nameList) == "table" then
for _, name in pairs(nameList) do
if name == instanceName then
return true
end
end
elseif type(nameList) == "string" then
return nameList == instanceName
end
return false
end
local function getReadableName(nameList)
if type(nameList) == "table" then
return table.concat(nameList, " or ")
elseif type(nameList) == "string" then
return nameList
end
return "*"
end
local function validateWithSchemaHelper(schema, instance, authorizedSet)
-- validate
if instance.ClassName ~= schema.ClassName or (schema.Name ~= nil and not checkName(schema.Name, instance.Name)) then
return { success = false }
end
-- validate children
if schema._children then
for _, childSchema in pairs(schema._children) do
local found = false
local mostRecentFailure
for _, child in pairs(instance:GetChildren()) do
local result = validateWithSchemaHelper(childSchema, child, authorizedSet)
if result.success then
found = true
break
elseif result.message then
mostRecentFailure = result
end
end
if not found and not childSchema._optional then
if mostRecentFailure then
return mostRecentFailure
else
return {
success = false,
message = "Could not find a "
.. childSchema.ClassName
.. " called "
.. getReadableName(childSchema.Name)
.. " inside "
.. instance.Name
}
end
end
end
end
authorizedSet[instance] = true
return { success = true }
end
local function validateWithSchema(schema, instance)
if instance.ClassName ~= schema.ClassName or (schema.Name ~= nil and schema.Name ~= instance.Name) then
return {
success = false,
message = "Expected top-level instance to be a " .. schema.ClassName,
}
end
local authorizedSet = {}
local result = validateWithSchemaHelper(schema, instance, authorizedSet)
if not result.success then
return result
end
-- check for extra descendants
local unauthorizedDescendantPaths = {}
for _, descendant in pairs(instance:GetDescendants()) do
if authorizedSet[descendant] == nil then
unauthorizedDescendantPaths[#unauthorizedDescendantPaths + 1] = descendant:GetFullName()
end
end
if #unauthorizedDescendantPaths > 0 then
return {
success = false,
message = "Unexpected Descendants:\n" .. table.concat(unauthorizedDescendantPaths, "\n")
}
end
return { success = true }
end
return validateWithSchema
@@ -0,0 +1,33 @@
local CorePackages = game:GetService("CorePackages")
local Cryo = require(CorePackages.Cryo)
local function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
local function valueToString(propValue)
local valueType = typeof(propValue)
if propValue == Cryo.None then
return "not defined"
elseif valueType == "Vector3" then
return string.format(
"%d, %d, %d",
round(propValue.X, 2),
round(propValue.Y, 2),
round(propValue.Z, 2)
)
elseif valueType == "Color3" then
return string.format(
"%d, %d, %d",
math.floor(propValue.r * 255),
math.floor(propValue.g * 255),
math.floor(propValue.b * 255)
)
else
return tostring(propValue)
end
end
return valueToString
@@ -0,0 +1,59 @@
local UGCValidationService = game:GetService("UGCValidationService")
local root = script.Parent.Parent
local valueToString = require(root.util.valueToString)
local MARGIN_OF_ERROR = 0.1
local function validateHandleSize(isAsync, instance)
-- these are guaranteed to exist thanks to validateInstanceTree being called beforehand
local handle = instance.Handle
local mesh = handle:FindFirstChildOfClass("SpecialMesh")
local success, verts = pcall(function()
if isAsync then
return UGCValidationService:GetMeshVerts(mesh.MeshId)
else
return UGCValidationService:GetMeshVertsSync(mesh.MeshId)
end
end)
if not success then
return false, { "Failed to read mesh" }
end
local minX, maxX = math.huge, 0
local minY, maxY = math.huge, 0
local minZ, maxZ = math.huge, 0
for i = 1, #verts do
local vert = verts[i] * mesh.Scale
minX = math.min(minX, vert.X)
minY = math.min(minY, vert.Y)
minZ = math.min(minZ, vert.Z)
maxX = math.max(maxX, vert.X)
maxY = math.max(maxY, vert.Y)
maxZ = math.max(maxZ, vert.Z)
end
local meshSize = Vector3.new(
maxX - minX,
maxY - minY,
maxZ - minZ
)
-- allow handle.Size to be within MARGIN_OF_ERROR of meshSize or larger
-- this is necessary since we're comparing floats
-- the size only needs to be a rough equivalent for thumbnailing
if handle.Size.X + MARGIN_OF_ERROR < meshSize.X
or handle.Size.Y + MARGIN_OF_ERROR < meshSize.Y
or handle.Size.Z + MARGIN_OF_ERROR < meshSize.Z then
return false, {
string.format("Accessory Handle size should be at least the size of the mesh ( %s )", valueToString(meshSize))
}
end
return true
end
return validateHandleSize
@@ -0,0 +1,53 @@
local root = script.Parent.Parent
local Constants = require(root.Constants)
local createAccessorySchema = require(root.util.createAccessorySchema)
local validateWithSchema = require(root.util.validateWithSchema)
-- validates a given instance based on a schema
local function validateInstanceTree(instance, assetTypeEnum)
local assetInfo = Constants.ASSET_TYPE_INFO[assetTypeEnum]
if not assetInfo then
return false, { "Could not validate" }
end
local schema = createAccessorySchema(assetInfo.attachmentNames)
-- validate using hat schema
local validationResult = validateWithSchema(schema, instance)
if validationResult.success == false then
return false, { validationResult.message }
end
-- fallback case for if validateWithSchema breaks
local invalidDescendantsReasons = {}
if Constants.BANNED_NAMES[instance.Name] then
local reason = string.format("%s has an invalid name", instance:GetFullName())
invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason
end
for _, descendant in pairs(instance:GetDescendants()) do
for _, className in pairs(Constants.BANNED_CLASS_NAMES) do
if descendant:IsA(className) then
local reason = string.format(
"%s is of type %s which is not allowed",
descendant:GetFullName(),
className
)
invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason
end
end
if Constants.BANNED_NAMES[descendant.Name] then
local reason = string.format("%s has an invalid name", descendant:GetFullName())
invalidDescendantsReasons[#invalidDescendantsReasons + 1] = reason
end
end
if #invalidDescendantsReasons > 0 then
return false, invalidDescendantsReasons
end
return true
end
return validateInstanceTree
@@ -0,0 +1,30 @@
local root = script.Parent.Parent
local Constants = require(root.Constants)
-- ensures no descendant of instance has a material that does not exist in Constants.MATERIAL_WHITELIST
local function validateMaterials(instance)
local materialFailures = {}
for _, descendant in pairs(instance:GetDescendants()) do
if descendant:IsA("BasePart") and not Constants.MATERIAL_WHITELIST[descendant.Material] then
materialFailures[#materialFailures + 1] = descendant:GetFullName()
end
end
if #materialFailures > 0 then
local reasons = {}
local acceptedMaterialNames = {}
for material in pairs(Constants.MATERIAL_WHITELIST) do
acceptedMaterialNames[#acceptedMaterialNames + 1] = material.Name
end
reasons[#reasons + 1] = "Invalid materials for"
for _, name in pairs(materialFailures) do
reasons[#reasons + 1] = name
end
reasons[#reasons + 1] = "Accepted materials are " .. table.concat(acceptedMaterialNames, ", ")
return false, reasons
end
return true
end
return validateMaterials
@@ -0,0 +1,72 @@
local UGCValidationService = game:GetService("UGCValidationService")
local root = script.Parent.Parent
local Constants = require(root.Constants)
local DEFAULT_OFFSET = Vector3.new(0, 0, 0)
local function pointInBounds(worldPos, boundsCF, boundsSize)
local objectPos = boundsCF:pointToObjectSpace(worldPos)
return objectPos.X >= -boundsSize.X/2
and objectPos.X <= boundsSize.X/2
and objectPos.Y >= -boundsSize.Y/2
and objectPos.Y <= boundsSize.Y/2
and objectPos.Z >= -boundsSize.Z/2
and objectPos.Z <= boundsSize.Z/2
end
local function getAttachment(parent, names)
for _, name in pairs(names) do
local result = parent:FindFirstChild(name)
if result then
return result
end
end
return nil
end
local function validateMeshBounds(isAsync, instance, assetTypeEnum)
local assetInfo = Constants.ASSET_TYPE_INFO[assetTypeEnum]
-- these are guaranteed to exist thanks to validateInstanceTree being called beforehand
local handle = instance.Handle
local mesh = handle:FindFirstChildOfClass("SpecialMesh")
local attachment = getAttachment(handle, assetInfo.attachmentNames)
if mesh.MeshId == "" then
return false, { "Mesh must contain valid MeshId" }
end
local success, verts = pcall(function()
if isAsync then
return UGCValidationService:GetMeshVerts(mesh.MeshId)
else
return UGCValidationService:GetMeshVertsSync(mesh.MeshId)
end
end)
if not success then
return false, { "Failed to read mesh" }
end
local boundsInfo = assert(assetInfo.bounds[attachment.Name], "Could not find bounds for " .. attachment.Name)
local boundsSize = boundsInfo.size
local boundsOffset = boundsInfo.offset or DEFAULT_OFFSET
local boundsCF = handle.CFrame * attachment.CFrame * CFrame.new(boundsOffset)
for _, vertPos in pairs(verts) do
local worldPos = handle.CFrame:pointToWorldSpace(vertPos * mesh.Scale)
if not pointInBounds(worldPos, boundsCF, boundsSize) then
return false, {
"Mesh is too large!",
string.format("Max size for type %s is ( %s )", assetTypeEnum.Name, tostring(boundsSize)),
"Use SpecialMesh.Scale if needed"
}
end
end
return true
end
return validateMeshBounds
@@ -0,0 +1,40 @@
local UGCValidationService = game:GetService("UGCValidationService")
local root = script.Parent.Parent
local Constants = require(root.Constants)
-- ensures accessory mesh does not have more triangles than Constants.MAX_HAT_TRIANGLES
local function validateMeshTriangles(isAsync, instance)
-- check mesh triangles
-- this is guaranteed to exist thanks to validateInstanceTree being called beforehand
local mesh = instance.Handle:FindFirstChildOfClass("SpecialMesh")
if mesh.MeshId == "" then
return false, { "Mesh must contain valid MeshId" }
end
local success, triangles = pcall(function()
if isAsync then
return UGCValidationService:GetMeshTriCount(mesh.MeshId)
else
return UGCValidationService:GetMeshTriCountSync(mesh.MeshId)
end
end)
if not success then
return false, { "Failed to load mesh data" }
elseif triangles > Constants.MAX_HAT_TRIANGLES then
return false, {
string.format(
"Mesh has %d triangles, but the limit is %d",
triangles,
Constants.MAX_HAT_TRIANGLES
)
}
end
return true
end
return validateMeshTriangles
@@ -0,0 +1,97 @@
local root = script.Parent.Parent
local Constants = require(root.Constants)
local getAssetCreationDetails = require(root.util.getAssetCreationDetails)
local function parseContentId(contentIds, contentIdMap, object, fieldName)
local contentId = object[fieldName]
-- map to ending digits
-- rbxassetid://1234 -> 1234
-- http://www.roblox.com/asset/?id=1234 -> 1234
local id = tonumber(string.match(contentId, "%d+$"))
if id == nil then
return false, {
"Could not parse ContentId",
contentId,
}
end
contentIdMap[id] = {
fieldName = fieldName,
instance = object,
}
table.insert(contentIds, id)
return true
end
local function parseDescendantContentIds(contentIds, contentIdMap, object)
for _, descendant in pairs(object:GetDescendants()) do
if descendant:IsA("SpecialMesh") then
local success, reasons
success, reasons = parseContentId(contentIds, contentIdMap, descendant, "MeshId")
if not success then
return false, reasons
end
success, reasons = parseContentId(contentIds, contentIdMap, descendant, "TextureId")
if not success then
return false, reasons
end
end
end
return true
end
-- ensures accessory content ids have all passed moderation review
local function validateModeration(isAsync, instance)
local contentIdMap = {}
local contentIds = {}
local parseSuccess, parseReasons = parseDescendantContentIds(contentIds, contentIdMap, instance)
if not parseSuccess then
return false, parseReasons
end
local moderatedIds = {}
local success, response = getAssetCreationDetails(isAsync, contentIds)
if not success or #response ~= #contentIds then
return false, { "Could not fetch details for assets" }
end
for _, details in pairs(response) do
if details.status == Constants.ASSET_STATUS.UNKNOWN
or details.status == Constants.ASSET_STATUS.REVIEW_PENDING
or details.status == Constants.ASSET_STATUS.MODERATED
then
table.insert(moderatedIds, details.assetId)
end
end
if #moderatedIds > 0 then
local moderationMessages = {}
for idx, id in pairs(moderatedIds) do
local mapped = contentIdMap[id]
if mapped then
moderationMessages[idx] = string.format(
"%s.%s ( %s )",
mapped.instance:GetFullName(),
mapped.fieldName,
id
)
else
moderationMessages[idx] = id
end
end
return false, {
"The following asset IDs have not passed moderation:",
unpack(moderationMessages),
}
end
return true
end
return validateModeration
@@ -0,0 +1,71 @@
local CorePackages = game:GetService("CorePackages")
local Cryo = require(CorePackages.Cryo)
local root = script.Parent.Parent
local Constants = require(root.Constants)
local valueToString = require(root.util.valueToString)
local EPSILON = 1e-5
local function floatEq(a, b)
return math.abs(a - b) <= EPSILON
end
local function v3FloatEq(a, b)
return floatEq(a.X, b.X) and floatEq(a.Y, b.Y) and floatEq(a.Z, b.Z)
end
local function c3FloatEq(a, b)
return floatEq(a.r, b.r) and floatEq(a.g, b.g) and floatEq(a.b, b.b)
end
local function propEq(propValue, expectedValue)
local valueType = typeof(expectedValue)
if expectedValue == Cryo.None then
return propValue == nil
elseif valueType == "number" then
return floatEq(propValue, expectedValue)
elseif valueType == "Vector3" then
return v3FloatEq(propValue, expectedValue)
elseif valueType == "Color3" then
return c3FloatEq(propValue, expectedValue)
else
return propValue == expectedValue
end
end
local function validateProperties(instance)
-- full tree of instance + descendants
local objects = instance:GetDescendants()
table.insert(objects, instance)
for _, object in pairs(objects) do
for className, properties in pairs(Constants.PROPERTIES) do
if object:IsA(className) then
for propName, expectedValue in pairs(properties) do
-- ensure property exists first
local propExists, propValue = pcall(function() return object[propName] end)
if not propExists then
return false, {
string.format("Property %s does not exist on type %s", propName, object.ClassName)
}
end
if not propEq(propValue, expectedValue) then
return false, {
string.format("Expected %s.%s to be %s", object:GetFullName(), propName, valueToString(expectedValue))
}
end
end
end
end
end
return true
end
return validateProperties
@@ -0,0 +1,25 @@
local CollectionService = game:GetService("CollectionService")
local function validateTags(instance)
local objects = instance:GetDescendants()
table.insert(objects, instance)
local hasTags = {}
for _, obj in pairs(objects) do
if #CollectionService:GetTags(obj) > 0 then
table.insert(hasTags, obj)
end
end
if #hasTags > 0 then
local reasons = { "The following objects contain CollectionService tags:" }
for _, obj in pairs(hasTags) do
table.insert(reasons, obj:GetFullName())
end
return false, reasons
end
return true
end
return validateTags
@@ -0,0 +1,40 @@
local UGCValidationService = game:GetService("UGCValidationService")
local root = script.Parent.Parent
local Constants = require(root.Constants)
local function validateTextureSize(isAsync, instance)
-- this is guaranteed to exist thanks to validateInstanceTree being called beforehand
local mesh = instance.Handle:FindFirstChildOfClass("SpecialMesh")
if mesh.TextureId == "" then
return false, { "Mesh must contain valid TextureId" }
end
local success, imageSize = pcall(function()
if isAsync then
return UGCValidationService:GetTextureSize(mesh.TextureId)
else
return UGCValidationService:GetTextureSizeSync(mesh.TextureId)
end
end)
if not success then
return false, { "Failed to load texture data", imageSize }
elseif imageSize.X > Constants.MAX_TEXTURE_SIZE or imageSize.Y > Constants.MAX_TEXTURE_SIZE then
return false, {
string.format(
"Texture size is %dx%d px, but the limit is %dx%d px",
imageSize.X,
imageSize.Y,
Constants.MAX_TEXTURE_SIZE,
Constants.MAX_TEXTURE_SIZE
)
}
end
return true
end
return validateTextureSize