add gs
This commit is contained in:
@@ -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
|
||||
+47
@@ -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
|
||||
+56
@@ -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
|
||||
+100
@@ -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
|
||||
+59
@@ -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
|
||||
+53
@@ -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
|
||||
+30
@@ -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
|
||||
+72
@@ -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
|
||||
+40
@@ -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
|
||||
+97
@@ -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
|
||||
+71
@@ -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
|
||||
+40
@@ -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
|
||||
Reference in New Issue
Block a user