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,9 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalShadow": "fatal",
"FunctionUnused": "fatal",
"ImportUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,61 @@
--[[
Filename: CommonUtil.lua
Written by: dbanks
Description: Common work.
--]]
--[[ Classes ]]--
local CommonUtil = {}
-- Concatenate these two tables, return result.
function CommonUtil.TableConcat(t1,t2)
for i=1,#t2 do
t1[#t1+1] = t2[i]
end
return t1
end
-- Instances have a "Name" field. Sort
-- by that name,
function CommonUtil.SortByName(items)
local function compareInstanceNames(i1, i2)
return (i1.Name < i2.Name)
end
table.sort(items, compareInstanceNames)
return items
end
-- Provides a nice syntax for creating Roblox instances.
-- Example:
-- local newPart = Utility.Create("Part") {
-- Parent = workspace,
-- Anchored = true,
--
-- --Create a SpecialMesh as a child of this part too
-- Utility.Create("SpecialMesh") {
-- MeshId = "rbxassetid://...",
-- Scale = Vector3.new(0.5, 0.2, 10)
-- }
-- }
function CommonUtil.Create(instanceType)
return function(data)
local obj = Instance.new(instanceType)
local parent = nil
for k, v in pairs(data) do
if type(k) == 'number' then
v.Parent = obj
elseif k == 'Parent' then
parent = v
else
obj[k] = v
end
end
if parent then
obj.Parent = parent
end
return obj
end
end
return CommonUtil
@@ -0,0 +1,38 @@
-- universal design constants for in-game ui style
local Constants = {
COLORS = {
SLATE = Color3.fromRGB(35, 37, 39),
FLINT = Color3.fromRGB(57, 59, 61),
GRAPHITE = Color3.fromRGB(101, 102, 104),
PUMICE = Color3.fromRGB(189, 190, 190),
WHITE = Color3.fromRGB(255, 255, 255),
},
ERROR_PROMPT_HEIGHT = {
Default = 236,
XBox = 180,
},
ERROR_PROMPT_MIN_WIDTH = {
Default = 320,
XBox = 400,
},
ERROR_PROMPT_MAX_WIDTH = {
Default = 400,
XBox = 400,
},
ERROR_TITLE_FRAME_HEIGHT = {
Default = 50,
},
SPLIT_LINE_THICKNESS = 1,
BUTTON_CELL_PADDING = 10,
BUTTON_HEIGHT = 36,
SIDE_PADDING = 20,
LAYOUT_PADDING = 20,
SIDE_MARGIN = 20, -- When resizing according to screen size, reserve with side margins
PRIMARY_BUTTON_TEXTURE = "rbxasset://textures/ui/ErrorPrompt/PrimaryButton.png",
SECONDARY_BUTTON_TEXTURE = "rbxasset://textures/ui/ErrorPrompt/SecondaryButton.png",
SHIMMER_TEXTURE = "rbxasset://textures/ui/LuaApp/graphic/shimmer_darkTheme.png",
OVERLAY_TEXTURE = "rbxasset://textures/ui/ErrorPrompt/ShimmerOverlay.png",
}
return Constants
@@ -0,0 +1,67 @@
if not settings():GetFFlag("CoreScriptFasterCreate") then
return function(className, defaultParent)
return function(propertyList)
local object = Instance.new(className)
local parent = nil
for index, value in next, propertyList do
if typeof(index) == 'string' then
if index == 'Parent' then
parent = value
else
object[index] = value
end
else
local valueType = typeof(value)
if valueType == 'function' then
value(object)
elseif valueType == 'Instance' then
value.Parent = object
end
end
end
if parent then
object.Parent = parent
end
if object.Parent == nil then
object.Parent = defaultParent
end
return object
end
end
end
return function(className, defaultParent)
return function(propertyList)
local object = Instance.new(className)
local parent = nil
for index, value in next, propertyList do
if type(index) == 'string' then
if index == 'Parent' then
parent = value
else
object[index] = value
end
else
local valueType = typeof(value)
if valueType == 'function' then
value(object)
elseif valueType == 'Instance' then
value.Parent = object
end
end
end
if parent then
object.Parent = parent
elseif defaultParent then
object.Parent = defaultParent
end
return object
end
end
@@ -0,0 +1,53 @@
--[[
A component that establishes a connection to a Roblox event when it is rendered.
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local EventConnection = Roact.Component:extend("EventConnection")
function EventConnection:init()
self.connection = nil
end
--[[
Render the child component so that EventConnections can be nested like so:
Roact.createElement(EventConnection, {
event = UserInputService.InputBegan,
callback = inputBeganCallback,
}, {
Roact.createElement(EventConnection, {
event = UserInputService.InputChanged,
callback = inputChangedCallback,
})
})
]]
function EventConnection:render()
return Roact.oneChild(self.props[Roact.Children])
end
function EventConnection:didMount()
local event = self.props.event
local callback = self.props.callback
self.connection = event:Connect(callback)
end
function EventConnection:didUpdate(oldProps)
if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then
self.connection:Disconnect()
self.connection = self.props.event:Connect(self.props.callback)
end
end
function EventConnection:willUnmount()
self.connection:Disconnect()
self.connection = nil
end
return EventConnection
@@ -0,0 +1,5 @@
game:DefineFastFlag("FixDialogServerWait", false)
return function()
return game:GetFastFlag("FixDialogServerWait")
end
@@ -0,0 +1,12 @@
-- This Flag is a descendant of the Common folder instead of Modules/Flags
-- because it needs to be accessible by both Modules/InGameChat and
-- Modules/Server/ClientChat/ChatWindowInstaller.
--
-- Since the server only has access to the Common and Server folders, it's
-- placed here so both parts of the codebase can access it.
game:DefineFastFlag("RoactBubbleChat", false)
return function()
return game:GetFastFlag("RoactBubbleChat")
end
@@ -0,0 +1,12 @@
-- This Flag is a descendant of the Common folder instead of Modules/Flags
-- because it needs to be accessible by both Modules/Common/LegacyThumbnailUrls and
-- other places like Emotes, InspectAndBuy
--
-- Since the server only has access to the Common and Server folders, it's
-- placed here so both parts of the codebase can access it.
game:DefineFastFlag("UseThumbnailUrl", false)
return function()
return game:GetFastFlag("UseThumbnailUrl")
end
@@ -0,0 +1,78 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
local GAME_I18N_URL = string.format("https://gameinternationalization.%s", Url.DOMAIN)
local GameRequests = {}
--[[
This endpoint (gameinternationalization/v1/name-description/games/{gameId})
returns all of the available localized names + descriptions for a game.
Docs: https://gameinternationalization.roblox.com/docs#!/NameDescription/get_v1_name_description_games_gameId
{
"data": [
{
"name": "string",
"description": "string",
"languageCode": "string"
}
]
}
]]
function GameRequests.GetNamesAndDescriptions(requestImpl, gameId)
local url = string.format("%sv1/name-description/games/%s", GAME_I18N_URL, gameId)
return requestImpl(url, "GET")
end
--[[
This endpoint (gameinternationalization/v1/source-language/games/{gameId})
returns the games default language
Docs: https://gameinternationalization.roblox.com/docs#!/SourceLanguage/get_v1_source_language_games_gameId
{
"name": "English",
"nativeName": "English",
"languageCode": "en"
}
]]
function GameRequests.GetSourceLanguage(requestImpl, gameId)
local url = string.format("%sv1/source-language/games/%s", GAME_I18N_URL, gameId)
return requestImpl(url, "GET")
end
--[[
This endpoint (locale.roblox.com/v1/locales)
returns information about the supported languages on Roblox
Docs: https://locale.roblox.com/docs#!/Locale/get_v1_locales_user_locale
{
"data": [
{
"locale": {
"id": 0,
"locale": "string",
"name": "string",
"nativeName": "string",
"language": {
"id": 0,
"name": "string",
"nativeName": "string",
"languageCode": "string"
}
},
"isEnabledForFullExperience": true,
"isEnabledForSignupAndLogin": true,
"isEnabledForInGameUgc": true
}
]
}
]]
function GameRequests.GetSupportedLanguages(requestImpl)
local url = string.format("%sv1/locales", Url.LOCALE)
return requestImpl(url, "GET")
end
return GameRequests
@@ -0,0 +1,7 @@
--!nocheck
local UserInputService = game:GetService("UserInputService")
return function()
return Enum.Platform.XBoxOne == UserInputService:GetPlatform() and Enum.QualityLevel.Level21 or Enum.QualityLevel.Automatic
end
@@ -0,0 +1,173 @@
local CorePackages = game:GetService("CorePackages")
local Promise = require(CorePackages.Promise)
local GameRequests = require(script.Parent.GameRequests)
local SupportedLanguagesFetched = false
local StartedFetchingSupportedLanaguages = false
local FetchedSupportedLanguagesEvent = Instance.new("BindableEvent")
local CachedSupportedLanguages = nil
local FFlagFixGetGameNameAndDescription = game:DefineFastFlag("FixGetGameNameAndDescription", false)
local FALLBACK_LANGUAGE_CONSTANT = "FALLBACK"
local FALLBACK_SOURCE_LOCALE = "en-us"
local function GetSupportedLanguagesPromise(networkImpl)
if SupportedLanguagesFetched then
return Promise.resolve(CachedSupportedLanguages)
elseif StartedFetchingSupportedLanaguages then
return Promise.new(function(resolve, reject)
local success = FetchedSupportedLanguagesEvent.Event:Wait()
if success then
resolve(CachedSupportedLanguages)
else
reject()
end
end)
end
StartedFetchingSupportedLanaguages = true
return GameRequests.GetSupportedLanguages(networkImpl):andThen(function(result)
if FFlagFixGetGameNameAndDescription then
SupportedLanguagesFetched = true
end
CachedSupportedLanguages = result
FetchedSupportedLanguagesEvent:Fire(true)
return Promise.resolve(result)
end,
function()
StartedFetchingSupportedLanaguages = false
FetchedSupportedLanguagesEvent:Fire(false)
return Promise.reject()
end)
end
local function PerGameCachedRequestFactoryFunction(Request)
local cache = {}
return function(networkImpl, gameId)
if cache[gameId] then
if cache[gameId].Fetched then
return Promise.resolve(cache[gameId].Result)
else
local success = cache[gameId].FinishedEvent.Event:Wait()
if success then
return Promise.resolve(cache[gameId].Result)
else
return Promise.reject()
end
end
else
cache[gameId] = {
Fetched = false,
FinishedEvent = Instance.new("BindableEvent"),
Result = nil,
}
return Request(networkImpl, gameId):andThen(function(result)
cache[gameId].Fetched = true
cache[gameId].Result = result
cache[gameId].FinishedEvent:Fire(true)
return Promise.resolve(result)
end,
function()
cache[gameId].FinishedEvent:Fire(false)
return Promise.reject()
end)
end
end
end
local function ProcessNamesAndDescriptionsResult(result)
local data = result.responseBody.data
local gameNames = {}
local gameDescriptions = {}
for _, nameDescription in ipairs(data) do
local languageCode = nameDescription.languageCode or FALLBACK_LANGUAGE_CONSTANT
if gameNames[languageCode] == nil then
gameNames[languageCode] = nameDescription.name
end
if gameDescriptions[languageCode] == nil then
gameDescriptions[languageCode] = nameDescription.description
end
end
return gameNames, gameDescriptions
end
local function ProcessSupportedLanaguagesResult(result)
local data = result.responseBody.data
local languageCodeMap = {}
for _, localeInfo in ipairs(data) do
-- Locale being repeated here twice looks wrong but it is correct.
-- Web passes in the locales with _ as a seperator but we use - on the client
languageCodeMap[localeInfo.locale.language.languageCode] = localeInfo.locale.locale:gsub("_", "-")
end
return languageCodeMap
end
local function ProcessSourceLanguageResult(result)
local data = result.responseBody
return data.languageCode
end
local GetNamesAndDescriptionsPromise = PerGameCachedRequestFactoryFunction(GameRequests.GetNamesAndDescriptions)
local GetSourceLanguagePromise = PerGameCachedRequestFactoryFunction(GameRequests.GetSourceLanguage)
return function(networkImpl, gameId)
local namesAndDescriptionsPromise = GetNamesAndDescriptionsPromise(networkImpl, gameId)
local supportedLanguagesPromise = GetSupportedLanguagesPromise(networkImpl)
local sourceLanguagesPromise = GetSourceLanguagePromise(networkImpl, gameId)
return Promise.all(
namesAndDescriptionsPromise,
supportedLanguagesPromise,
sourceLanguagesPromise):andThen(function(results)
local namesAndDescriptionsResult = results[1]
local supportedLanaguesResult = results[2]
local sourceLanagueResult = results[3]
local gameNamesLanguageCodeMap, gameDescriptionsLanguageCodeMap = ProcessNamesAndDescriptionsResult(
namesAndDescriptionsResult)
local languageCodeMap = ProcessSupportedLanaguagesResult(supportedLanaguesResult)
local sourceLangaugeCode = ProcessSourceLanguageResult(sourceLanagueResult)
local sourceLocale = languageCodeMap[sourceLangaugeCode] or FALLBACK_SOURCE_LOCALE
local gameNameLocaleMap = {}
for languageCode, name in pairs(gameNamesLanguageCodeMap) do
local locale = languageCodeMap[languageCode]
if locale then
gameNameLocaleMap[locale] = name
end
end
if gameNameLocaleMap[sourceLocale] == nil then
gameNameLocaleMap[sourceLocale] = gameNamesLanguageCodeMap[FALLBACK_LANGUAGE_CONSTANT]
end
local gameDescriptionsLocaleMap = {}
for languageCode, description in pairs(gameDescriptionsLanguageCodeMap) do
local locale = languageCodeMap[languageCode]
if locale then
gameDescriptionsLocaleMap[locale] = description
end
end
if gameDescriptionsLocaleMap[sourceLocale] == nil then
gameDescriptionsLocaleMap[sourceLocale] = gameDescriptionsLanguageCodeMap[FALLBACK_LANGUAGE_CONSTANT]
end
return gameNameLocaleMap, gameDescriptionsLocaleMap, sourceLocale
end,
function()
return Promise.reject()
end)
end
@@ -0,0 +1,105 @@
local Players = game:GetService("Players")
-- wait for the first of the passed signals to fire
local function waitForFirst(...)
local shunt = Instance.new("BindableEvent")
local slots = {...}
local function fire(...)
for i = 1, #slots do
slots[i]:Disconnect()
end
return shunt:Fire(...)
end
for i = 1, #slots do
slots[i] = slots[i]:Connect(fire)
end
return shunt.Event:Wait()
end
local HumanoidReadyUtil = {}
-- registers a humanoidReady(player: Player, character: Model, humanoid: Humanoid) callback and
-- invokes it immediately for existing player character humanoids.
--
-- Unregistering is not currently supported.
--
-- This can't just be an event because we need to invoke it immediately for existing eligable player
-- character humanoids, but would not for any existing callbacks.
--
-- For now this is about sharing code, not sharing event connections. Supporting this would really
-- complicate the code and we don't currently have multiple systems that could benefit from this
-- sharing that would all be active on a single client.
function HumanoidReadyUtil.registerHumanoidReady(humanoidReady)
local function characterAdded(player, character)
-- Avoiding memory leaks in the face of Character/Humanoid/RootPart lifetime has a few complications:
-- * character deparenting is a Remove instead of a Destroy, so signals are not cleaned up automatically.
-- ** must use a waitForFirst on everything and listen for hierarchy changes.
-- * the character might not be in the dm by the time CharacterAdded fires
-- ** constantly check consistency with player.Character and abort if CharacterAdded is fired again
-- * Humanoid may not exist immediately, and by the time it's inserted the character might be deparented.
-- * RootPart probably won't exist immediately.
-- ** by the time RootPart is inserted and Humanoid.RootPart is set, the character or the humanoid might be deparented.
if not character.Parent then
waitForFirst(character.AncestryChanged, player.CharacterAdded)
end
if player.Character ~= character or not character.Parent then
return
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
while character:IsDescendantOf(game) and not humanoid do
waitForFirst(character.ChildAdded, character.AncestryChanged, player.CharacterAdded)
humanoid = character:FindFirstChildOfClass("Humanoid")
end
if player.Character ~= character or not character:IsDescendantOf(game) then
return
end
-- must rely on HumanoidRootPart naming because Humanoid.RootPart does not fire changed signals
local rootPart = character:FindFirstChild("HumanoidRootPart")
while character:IsDescendantOf(game) and not rootPart do
waitForFirst(character.ChildAdded, character.AncestryChanged, humanoid.AncestryChanged, player.CharacterAdded)
rootPart = character:FindFirstChild("HumanoidRootPart")
end
if rootPart and humanoid:IsDescendantOf(game) and character:IsDescendantOf(game) and player.Character == character then
humanoidReady(player, character, humanoid)
end
end
local function playerAdded(player)
local characterAddedConn = player.CharacterAdded:Connect(function(character)
characterAdded(player, character)
end)
-- Players are Removed, not Destroyed, by replication so we must clean up
local ancestryChangedConn
ancestryChangedConn = player.AncestryChanged:Connect(function(_child, parent)
if not game:IsAncestorOf(parent) then
ancestryChangedConn:Disconnect()
characterAddedConn:Disconnect()
end
end)
local character = player.Character
if character then
characterAdded(player, character)
end
end
-- Track all players (including local player on the client)
Players.PlayerAdded:Connect(playerAdded)
for _, player in pairs(Players:GetPlayers()) do
playerAdded(player)
end
end
return HumanoidReadyUtil
@@ -0,0 +1,28 @@
local ContentProvider = game:GetService("ContentProvider")
local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui")
local GetFFlagUseThumbnailUrl = require(RobloxGui.Modules.Common.Flags.GetFFlagUseThumbnailUrl)
if GetFFlagUseThumbnailUrl() then
local BaseUrl = ContentProvider.BaseUrl:lower()
BaseUrl = string.gsub(BaseUrl, "/m.", "/www.")
BaseUrl = string.gsub(BaseUrl, "/www.", "/thumbnails.")
BaseUrl = string.gsub(BaseUrl, "http:", "https:")
return {
Headshot = "rbxthumb://type=AvatarHeadShot&id=%d&w=%d&h=%d",
Bust = BaseUrl .. "v1/users/avatar-bust?userIds=%d&size=%dx%d&format=Png&isCircular=false",
Thumbnail = "rbxthumb://type=Avatar&id=%d&w=%d&h=%d",
}
else
--Prefer Rbxthumb urls for new work!
local BaseUrl = ContentProvider.BaseUrl:lower()
BaseUrl = string.gsub(BaseUrl, "/m.", "/www.")
BaseUrl = string.gsub(BaseUrl, "http:", "https:")
return {
Headshot = BaseUrl .. "headshot-thumbnail/image?width=%d&height=%d&format=png&userId=%d",
Bust = BaseUrl .. "bust-thumbnail/image?width=%d&height=%d&format=png&userId=%d",
Thumbnail = BaseUrl .. "avatar-thumbnail/image?width=%d&height=%d&format=png&userId=%d",
}
end
@@ -0,0 +1,50 @@
-- // FileName: ObjectPool.lua
-- // Written by: TheGamer101
-- // Description: An object pool class used to avoid unnecessarily instantiating Instances.
local module = {}
--////////////////////////////// Include
--//////////////////////////////////////
--////////////////////////////// Methods
--//////////////////////////////////////
local methods = {}
methods.__index = methods
function methods:GetInstance(className)
if self.InstancePoolsByClass[className] == nil then
self.InstancePoolsByClass[className] = {}
end
local availableInstances = #self.InstancePoolsByClass[className]
if availableInstances > 0 then
local instance = self.InstancePoolsByClass[className][availableInstances]
table.remove(self.InstancePoolsByClass[className])
return instance
end
return Instance.new(className)
end
function methods:ReturnInstance(instance)
if self.InstancePoolsByClass[instance.ClassName] == nil then
self.InstancePoolsByClass[instance.ClassName] = {}
end
if #self.InstancePoolsByClass[instance.ClassName] < self.PoolSizePerType then
table.insert(self.InstancePoolsByClass[instance.ClassName], instance)
else
instance:Destroy()
end
end
--///////////////////////// Constructors
--//////////////////////////////////////
function module.new(poolSizePerType)
local obj = setmetatable({}, methods)
obj.InstancePoolsByClass = {}
obj.Name = "ObjectPool"
obj.PoolSizePerType = poolSizePerType
return obj
end
return module
@@ -0,0 +1,54 @@
--[[
Filename: PolicyService.lua
Written by: ben
Description: Handles all policy service calls in lua for core scripts
--]]
local PlayersService = game:GetService('Players')
local isSubjectToChinaPolicies = true
local policyTable
local initialized = false
local initAsyncCalledOnce = false
local initializedEvent = Instance.new("BindableEvent")
--[[ Classes ]]--
local PolicyService = {}
function PolicyService:InitAsync()
if _G.__TESTEZ_RUNNING_TEST__ then
-- Return here in the case of unit tests
return
end
if initialized then return end
if initAsyncCalledOnce then
initializedEvent.Event:Wait()
return
end
initAsyncCalledOnce = true
local localPlayer = PlayersService.LocalPlayer
while not localPlayer do
PlayersService.PlayerAdded:wait()
localPlayer = PlayersService.LocalPlayer
end
pcall(function() policyTable = game:GetService("PolicyService"):GetPolicyInfoForPlayerAsync(localPlayer) end)
if policyTable then
isSubjectToChinaPolicies = policyTable["IsSubjectToChinaPolicies"]
end
initialized = true
initializedEvent:Fire()
end
function PolicyService:IsSubjectToChinaPolicies()
self:InitAsync()
return isSubjectToChinaPolicies
end
return PolicyService
@@ -0,0 +1,480 @@
local RunService = game:GetService("RunService")
local Rigging = {}
-- Gravity that joint friction values were tuned under.
local REFERENCE_GRAVITY = 196.2
-- ReferenceMass values from mass of child part. Used to normalized "stiffness" for differently
-- sized avatars (with different mass).
local DEFAULT_MAX_FRICTION_TORQUE = 500
local HEAD_LIMITS = {
UpperAngle = 45,
TwistLowerAngle = -40,
TwistUpperAngle = 40,
FrictionTorque = 400,
ReferenceMass = 1.0249234437943,
}
local WAIST_LIMITS = {
UpperAngle = 20,
TwistLowerAngle = -40,
TwistUpperAngle = 20,
FrictionTorque = 750,
ReferenceMass = 2.861558675766,
}
local ANKLE_LIMITS = {
UpperAngle = 10,
TwistLowerAngle = -10,
TwistUpperAngle = 10,
ReferenceMass = 0.43671694397926,
}
local ELBOW_LIMITS = {
-- Elbow is basically a hinge, but allow some twist for Supination and Pronation
UpperAngle = 20,
TwistLowerAngle = 5,
TwistUpperAngle = 120,
ReferenceMass = 0.70196455717087,
}
local WRIST_LIMITS = {
UpperAngle = 30,
TwistLowerAngle = -10,
TwistUpperAngle = 10,
ReferenceMass = 0.69132566452026,
}
local KNEE_LIMITS = {
UpperAngle = 5,
TwistLowerAngle = -120,
TwistUpperAngle = -5,
ReferenceMass = 0.65389388799667,
}
local SHOULDER_LIMITS = {
UpperAngle = 110,
TwistLowerAngle = -85,
TwistUpperAngle = 85,
FrictionTorque = 600,
ReferenceMass = 0.90918225049973,
}
local HIP_LIMITS = {
UpperAngle = 40,
TwistLowerAngle = -5,
TwistUpperAngle = 80,
FrictionTorque = 600,
ReferenceMass = 1.9175016880035,
}
local R6_HEAD_LIMITS = {
UpperAngle = 30,
TwistLowerAngle = -40,
TwistUpperAngle = 40,
}
local R6_SHOULDER_LIMITS = {
UpperAngle = 110,
TwistLowerAngle = -85,
TwistUpperAngle = 85,
}
local R6_HIP_LIMITS = {
UpperAngle = 40,
TwistLowerAngle = -5,
TwistUpperAngle = 80,
}
local V3_ZERO = Vector3.new()
local V3_UP = Vector3.new(0, 1, 0)
local V3_DOWN = Vector3.new(0, -1, 0)
local V3_RIGHT = Vector3.new(1, 0, 0)
local V3_LEFT = Vector3.new(-1, 0, 0)
-- To model shoulder cone and twist limits correctly we really need the primary axis of the UpperArm
-- to be going down the limb. the waist and neck joints attachments actually have the same problem
-- of non-ideal axis orientation, but it's not as noticable there since the limits for natural
-- motion are tighter for those joints anyway.
local R15_ADDITIONAL_ATTACHMENTS = {
{"UpperTorso", "RightShoulderRagdollAttachment", CFrame.fromMatrix(V3_ZERO, V3_RIGHT, V3_UP), "RightShoulderRigAttachment"},
{"RightUpperArm", "RightShoulderRagdollAttachment", CFrame.fromMatrix(V3_ZERO, V3_DOWN, V3_RIGHT), "RightShoulderRigAttachment"},
{"UpperTorso", "LeftShoulderRagdollAttachment", CFrame.fromMatrix(V3_ZERO, V3_LEFT, V3_UP), "LeftShoulderRigAttachment"},
{"LeftUpperArm", "LeftShoulderRagdollAttachment", CFrame.fromMatrix(V3_ZERO, V3_DOWN, V3_LEFT), "LeftShoulderRigAttachment"},
}
-- { { Part0 name (parent), Part1 name (child, parent of joint), attachmentName, limits }, ... }
local R15_RAGDOLL_RIG = {
{"UpperTorso", "Head", "NeckRigAttachment", HEAD_LIMITS},
{"LowerTorso", "UpperTorso", "WaistRigAttachment", WAIST_LIMITS},
{"UpperTorso", "LeftUpperArm", "LeftShoulderRagdollAttachment", SHOULDER_LIMITS},
{"LeftUpperArm", "LeftLowerArm", "LeftElbowRigAttachment", ELBOW_LIMITS},
{"LeftLowerArm", "LeftHand", "LeftWristRigAttachment", WRIST_LIMITS},
{"UpperTorso", "RightUpperArm", "RightShoulderRagdollAttachment", SHOULDER_LIMITS},
{"RightUpperArm", "RightLowerArm", "RightElbowRigAttachment", ELBOW_LIMITS},
{"RightLowerArm", "RightHand", "RightWristRigAttachment", WRIST_LIMITS},
{"LowerTorso", "LeftUpperLeg", "LeftHipRigAttachment", HIP_LIMITS},
{"LeftUpperLeg", "LeftLowerLeg", "LeftKneeRigAttachment", KNEE_LIMITS},
{"LeftLowerLeg", "LeftFoot", "LeftAnkleRigAttachment", ANKLE_LIMITS},
{"LowerTorso", "RightUpperLeg", "RightHipRigAttachment", HIP_LIMITS},
{"RightUpperLeg", "RightLowerLeg", "RightKneeRigAttachment", KNEE_LIMITS},
{"RightLowerLeg", "RightFoot", "RightAnkleRigAttachment", ANKLE_LIMITS},
}
-- { { Part0 name, Part1 name }, ... }
local R15_NO_COLLIDES = {
{"LowerTorso", "LeftUpperArm"},
{"LeftUpperArm", "LeftHand"},
{"LowerTorso", "RightUpperArm"},
{"RightUpperArm", "RightHand"},
{"LeftUpperLeg", "RightUpperLeg"},
{"UpperTorso", "RightUpperLeg"},
{"RightUpperLeg", "RightFoot"},
{"UpperTorso", "LeftUpperLeg"},
{"LeftUpperLeg", "LeftFoot"},
-- Support weird R15 rigs
{"UpperTorso", "LeftLowerLeg"},
{"UpperTorso", "RightLowerLeg"},
{"LowerTorso", "LeftLowerLeg"},
{"LowerTorso", "RightLowerLeg"},
{"UpperTorso", "LeftLowerArm"},
{"UpperTorso", "RightLowerArm"},
{"Head", "LeftUpperArm"},
{"Head", "RightUpperArm"},
}
-- { { Motor6D name, Part name }, ...}, must be in tree order, important for ApplyJointVelocities
local R15_MOTOR6DS = {
{"Waist", "UpperTorso"},
{"Neck", "Head"},
{"LeftShoulder", "LeftUpperArm"},
{"LeftElbow", "LeftLowerArm"},
{"LeftWrist", "LeftHand"},
{"RightShoulder", "RightUpperArm"},
{"RightElbow", "RightLowerArm"},
{"RightWrist", "RightHand"},
{"LeftHip", "LeftUpperLeg"},
{"LeftKnee", "LeftLowerLeg"},
{"LeftAnkle", "LeftFoot"},
{"RightHip", "RightUpperLeg"},
{"RightKnee", "RightLowerLeg"},
{"RightAnkle", "RightFoot"},
}
-- R6 has hard coded part sizes and does not have a full set of rig Attachments.
local R6_ADDITIONAL_ATTACHMENTS = {
{"Head", "NeckAttachment", CFrame.new(0, -0.5, 0)},
{"Torso", "RightShoulderRagdollAttachment", CFrame.fromMatrix(Vector3.new(1, 0.5, 0), V3_RIGHT, V3_UP)},
{"Right Arm", "RightShoulderRagdollAttachment", CFrame.fromMatrix(Vector3.new(-0.5, 0.5, 0), V3_DOWN, V3_RIGHT)},
{"Torso", "LeftShoulderRagdollAttachment", CFrame.fromMatrix(Vector3.new(-1, 0.5, 0), V3_LEFT, V3_UP)},
{"Left Arm", "LeftShoulderRagdollAttachment", CFrame.fromMatrix(Vector3.new(0.5, 0.5, 0), V3_DOWN, V3_LEFT)},
{"Torso", "RightHipAttachment", CFrame.new(0.5, -1, 0)},
{"Right Leg", "RightHipAttachment", CFrame.new(0, 1, 0)},
{"Torso", "LeftHipAttachment", CFrame.new(-0.5, -1, 0)},
{"Left Leg", "LeftHipAttachment", CFrame.new(0, 1, 0)},
}
-- R6 rig tables use the same table structures as R15.
local R6_RAGDOLL_RIG = {
{"Torso", "Head", "NeckAttachment", R6_HEAD_LIMITS},
{"Torso", "Left Leg", "LeftHipAttachment", R6_HIP_LIMITS},
{"Torso", "Right Leg", "RightHipAttachment", R6_HIP_LIMITS},
{"Torso", "Left Arm", "LeftShoulderRagdollAttachment", R6_SHOULDER_LIMITS},
{"Torso", "Right Arm", "RightShoulderRagdollAttachment", R6_SHOULDER_LIMITS},
}
local R6_NO_COLLIDES = {
{"Left Leg", "Right Leg"},
{"Head", "Right Arm"},
{"Head", "Left Arm"},
}
local R6_MOTOR6DS = {
{"Neck", "Torso"},
{"Left Shoulder", "Torso"},
{"Right Shoulder", "Torso"},
{"Left Hip", "Torso"},
{"Right Hip", "Torso"},
}
local BALL_SOCKET_NAME = "RagdollBallSocket"
local NO_COLLIDE_NAME = "RagdollNoCollision"
-- Index parts by name to save us from many O(n) FindFirstChild searches
local function indexParts(model)
local parts = {}
for _, child in ipairs(model:GetChildren()) do
if child:IsA("BasePart") then
local name = child.name
-- Index first, mimicing FindFirstChild
if not parts[name] then
parts[name] = child
end
end
end
return parts
end
local function createRigJoints(parts, rig)
for _, params in ipairs(rig) do
local part0Name, part1Name, attachmentName, limits = unpack(params)
local part0 = parts[part0Name]
local part1 = parts[part1Name]
if part0 and part1 then
local a0 = part0:FindFirstChild(attachmentName)
local a1 = part1:FindFirstChild(attachmentName)
if a0 and a1 and a0:IsA("Attachment") and a1:IsA("Attachment") then
-- Our rigs only have one joint per part (connecting each part to it's parent part), so
-- we can re-use it if we have to re-rig that part again.
local constraint = part1:FindFirstChild(BALL_SOCKET_NAME)
if not constraint then
constraint = Instance.new("BallSocketConstraint")
constraint.Name = BALL_SOCKET_NAME
end
constraint.Attachment0 = a0
constraint.Attachment1 = a1
constraint.LimitsEnabled = true
constraint.UpperAngle = limits.UpperAngle
constraint.TwistLimitsEnabled = true
constraint.TwistLowerAngle = limits.TwistLowerAngle
constraint.TwistUpperAngle = limits.TwistUpperAngle
-- Scale constant torque limit for joint friction relative to gravity and the mass of
-- the body part.
local gravityScale = workspace.Gravity / REFERENCE_GRAVITY
local referenceMass = limits.ReferenceMass
local massScale = referenceMass and (part1:GetMass() / referenceMass) or 1
local maxTorque = limits.FrictionTorque or DEFAULT_MAX_FRICTION_TORQUE
constraint.MaxFrictionTorque = maxTorque * massScale * gravityScale
constraint.Parent = part1
end
end
end
end
local function createAdditionalAttachments(parts, attachments)
for _, attachmentParams in ipairs(attachments) do
local partName, attachmentName, cframe, baseAttachmentName = unpack(attachmentParams)
local part = parts[partName]
if part then
local attachment = part:FindFirstChild(attachmentName)
-- Create or update existing attachment
if not attachment or attachment:IsA("Attachment") then
if baseAttachmentName then
local base = part:FindFirstChild(baseAttachmentName)
if base and base:IsA("Attachment") then
cframe = base.CFrame * cframe
end
end
-- The attachment names are unique within a part, so we can re-use
if not attachment then
attachment = Instance.new("Attachment")
attachment.Name = attachmentName
attachment.CFrame = cframe
attachment.Parent = part
else
attachment.CFrame = cframe
end
end
end
end
end
local function createNoCollides(parts, noCollides)
-- This one's trickier to handle for an already rigged character since a part will have multiple
-- NoCollide children with the same name. Having fewer unique names is better for
-- replication so we suck it up and deal with the complexity here.
-- { [Part1] = { [Part0] = true, ... }, ...}
local needed = {}
-- Following the convention of the Motor6Ds and everything else here we parent the NoCollide to
-- Part1, so we start by building the set of Part0s we need a NoCollide with for each Part1
for _, namePair in ipairs(noCollides) do
local part0Name, part1Name = unpack(namePair)
local p0, p1 = parts[part0Name], parts[part1Name]
if p0 and p1 then
local p0Set = needed[p1]
if not p0Set then
p0Set = {}
needed[p1] = p0Set
end
p0Set[p0] = true
end
end
-- Go through NoCollides that exist and remove Part0s from the needed set if we already have
-- them covered. Gather NoCollides that aren't between parts in the set for resue
local reusableNoCollides = {}
for part1, neededPart0s in pairs(needed) do
local reusables = {}
for _, child in ipairs(part1:GetChildren()) do
if child:IsA("NoCollisionConstraint") and child.Name == NO_COLLIDE_NAME then
local p0 = child.Part0
local p1 = child.Part1
if p1 == part1 and neededPart0s[p0] then
-- If this matches one that we needed, we don't need to create it anymore.
neededPart0s[p0] = nil
else
-- Otherwise we're free to reuse this NoCollide
table.insert(reusables, child)
end
end
end
reusableNoCollides[part1] = reusables
end
-- Create the remaining NoCollides needed, re-using old ones if possible
for part1, neededPart0s in pairs(needed) do
local reusables = reusableNoCollides[part1]
for part0, _ in pairs(neededPart0s) do
local constraint = table.remove(reusables)
if not constraint then
constraint = Instance.new("NoCollisionConstraint")
end
constraint.Name = NO_COLLIDE_NAME
constraint.Part0 = part0
constraint.Part1 = part1
constraint.Parent = part1
end
end
end
local function disableMotorSet(model, motorSet)
local motors = {}
-- Destroy all regular joints:
for _, params in ipairs(motorSet) do
local part = model:FindFirstChild(params[2])
if part then
local motor = part:FindFirstChild(params[1])
if motor and motor:IsA("Motor6D") then
table.insert(motors, motor)
motor.Enabled = false
end
end
end
return motors
end
function Rigging.createRagdollJoints(model, rigType)
local parts = indexParts(model)
if rigType == Enum.HumanoidRigType.R6 then
createAdditionalAttachments(parts, R6_ADDITIONAL_ATTACHMENTS)
createRigJoints(parts, R6_RAGDOLL_RIG)
createNoCollides(parts, R6_NO_COLLIDES)
elseif rigType == Enum.HumanoidRigType.R15 then
createAdditionalAttachments(parts, R15_ADDITIONAL_ATTACHMENTS)
createRigJoints(parts, R15_RAGDOLL_RIG)
createNoCollides(parts, R15_NO_COLLIDES)
else
error("unknown rig type", 2)
end
end
function Rigging.removeRagdollJoints(model)
for _, descendant in pairs(model:GetDescendants()) do
-- Remove BallSockets and NoCollides, leave the additional Attachments
if (descendant:IsA("BallSocketConstraint") and descendant.Name == BALL_SOCKET_NAME)
or (descendant:IsA("NoCollisionConstraint") and descendant.Name == NO_COLLIDE_NAME)
then
descendant:Destroy()
end
end
end
function Rigging.disableMotors(model, rigType)
-- Note: We intentionally do not disable the root joint so that the mechanism root of the
-- character stays consistent when we break joints on the client. This avoid the need for the client to wait
-- for re-assignment of network ownership of a new mechanism, which creates a visible hitch.
local motors
if rigType == Enum.HumanoidRigType.R6 then
motors = disableMotorSet(model, R6_MOTOR6DS)
elseif rigType == Enum.HumanoidRigType.R15 then
motors = disableMotorSet(model, R15_MOTOR6DS)
else
error("unknown rig type", 2)
end
-- Set the root part to non-collide
local rootPart = model.PrimaryPart or model:FindFirstChild("HumanoidRootPart")
if rootPart and rootPart:IsA("BasePart") then
rootPart.CanCollide = false
end
return motors
end
function Rigging.disableParticleEmittersAndFadeOut(character, duration)
if RunService:IsServer() then
-- This causes a lot of unnecesarry replicated property changes
error("disableParticleEmittersAndFadeOut should not be called on the server.", 2)
end
local descendants = character:GetDescendants()
local transparencies = {}
for _, instance in pairs(descendants) do
if instance:IsA("BasePart") or instance:IsA("Decal") then
transparencies[instance] = instance.Transparency
elseif instance:IsA("ParticleEmitter") then
instance.Enabled = false
end
end
local t = 0
while t < duration do
-- Using heartbeat because we want to update just before rendering next frame, and not
-- block the render thread kicking off (as RenderStepped does)
local dt = RunService.Heartbeat:Wait()
t = t + dt
local alpha = math.min(t / duration, 1)
for part, initialTransparency in pairs(transparencies) do
part.Transparency = (1 - alpha) * initialTransparency + alpha
end
end
end
function Rigging.easeJointFriction(character, duration)
local descendants = character:GetDescendants()
-- { { joint, initial friction, end friction }, ... }
local frictionJoints = {}
for _, v in pairs(descendants) do
if v:IsA("BallSocketConstraint") and v.Name == BALL_SOCKET_NAME then
local current = v.MaxFrictionTorque
-- Keep the torso and neck a little stiffer...
local parentName = v.Parent.Name
local scale = (parentName == "UpperTorso" or parentName == "Head") and 0.5 or 0.05
local nextTorque = current * scale
frictionJoints[v] = { v, current, nextTorque }
end
end
local t = 0
while t < duration do
-- Using stepped because we want to update just before physics sim
local _, dt = RunService.Stepped:Wait()
t = t + dt
local alpha = math.min(t / duration, 1)
for _, tuple in pairs(frictionJoints) do
local ballSocket, a, b = unpack(tuple)
ballSocket.MaxFrictionTorque = (1 - alpha) * a + alpha * b
end
end
end
return Rigging
@@ -0,0 +1,37 @@
-- Domain list
local Urls = {}
local ContentProvider = game:GetService("ContentProvider")
local function getBaseDomain(baseUrl)
local _, prefixEnd = baseUrl:find("%.")
local baseDomain = baseUrl:sub(prefixEnd + 1)
if baseDomain:sub(-1) ~= "/" then
baseDomain = baseDomain .. "/"
end
return baseDomain
end
local baseUrl = ContentProvider.BaseUrl
local baseDomain = getBaseDomain(baseUrl)
local baseGameUrl = string.format("https://games.%s", baseDomain)
local baseRcsUrl = string.format("https://apis.rcs.%s", baseDomain)
local baseApisUrl = string.format("https://apis.%s", baseDomain)
local urlValues = {
GAME_URL = baseGameUrl,
RCS_URL = baseRcsUrl,
APIS_URL = baseApisUrl,
}
setmetatable(Urls, {
__newindex = function(t, key, index)
end,
__index = function(t, index)
return urlValues[index]
end
})
return Urls