624 lines
17 KiB
Lua
624 lines
17 KiB
Lua
-- Written by Kip Turner, Copyright Roblox 2015
|
|
local RunService = game:GetService('RunService')
|
|
local GuiService = game:GetService('GuiService')
|
|
local TextService = game:GetService('TextService')
|
|
local Players = game:GetService('Players')
|
|
|
|
local PlatformService = nil
|
|
pcall(function() PlatformService = game:GetService('PlatformService') end)
|
|
|
|
local Util = {}
|
|
do
|
|
|
|
function Util.IsFinite(num)
|
|
return num == num and num ~= 1/0 and num ~= -1/0
|
|
end
|
|
|
|
function Util.FindAncestorOfType(guiObject, class)
|
|
local parent = guiObject and guiObject.Parent
|
|
if parent then
|
|
if parent:IsA(class) then
|
|
return parent
|
|
else
|
|
return Util.FindAncestorOfType(parent, class)
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Util.CalculateRelativeDimensions(guiObject, guiDims, mockup_dims, rootGuiSize)
|
|
local guiResolution = GuiService:GetScreenResolution()
|
|
local parentSurfaceGui = Util.FindAncestorOfType(guiObject, 'SurfaceGui')
|
|
if parentSurfaceGui then
|
|
guiResolution = parentSurfaceGui.CanvasSize
|
|
end
|
|
local absolutePercentSize = (guiDims / mockup_dims)
|
|
if mockup_dims.y > 0 and guiResolution.y > 0 then
|
|
local mockupAspectRatio = mockup_dims.x / mockup_dims.y
|
|
local globalAspectRatio = guiResolution.x / guiResolution.y
|
|
absolutePercentSize = absolutePercentSize * (mockupAspectRatio / globalAspectRatio)
|
|
local parentObject = guiObject.Parent
|
|
if parentObject then
|
|
local parentPercentScreen = parentObject.AbsoluteSize / guiResolution
|
|
local parentSizeInverse = 1 / parentPercentScreen
|
|
if Util.IsFinite(parentSizeInverse.x) and Util.IsFinite(parentSizeInverse.y) then
|
|
return UDim2.new(parentSizeInverse.x * absolutePercentSize.x, 0, parentSizeInverse.y * absolutePercentSize.y, 0)
|
|
end
|
|
end
|
|
end
|
|
return UDim2.new(absolutePercentSize.x, 0, absolutePercentSize.y, 0)
|
|
end
|
|
|
|
|
|
-- Anchor Graph
|
|
-- 1 2 3
|
|
-- 4 5 6
|
|
-- 7 8 9
|
|
|
|
Util.Enum =
|
|
{
|
|
Anchor =
|
|
{
|
|
TopLeft = 1;
|
|
TopMiddle = 2;
|
|
TopRight = 3;
|
|
CenterLeft = 4;
|
|
Center = 5;
|
|
CenterRight = 6;
|
|
BottomLeft = 7;
|
|
BottomMiddle = 8;
|
|
BottomRight = 9;
|
|
};
|
|
};
|
|
|
|
function Util.CalculateFit(containerObject, rawImageSize)
|
|
local absSize = containerObject.AbsoluteSize
|
|
local scalar = absSize / rawImageSize
|
|
local fixedSize = rawImageSize * math.min(scalar.X, scalar.Y)
|
|
|
|
return UDim2.new(0, fixedSize.X , 0, fixedSize.Y)
|
|
end
|
|
|
|
function Util.CalculateFill(containerObject, rawImageSize)
|
|
local absSize = containerObject.AbsoluteSize
|
|
local scalar = absSize / rawImageSize
|
|
local fixedSize = rawImageSize * math.max(scalar.X, scalar.Y)
|
|
|
|
return UDim2.new(0, fixedSize.X , 0, fixedSize.Y)
|
|
end
|
|
|
|
function Util.Create(instanceType)
|
|
return function(data)
|
|
local obj = Instance.new(instanceType)
|
|
for k, v in pairs(data) do
|
|
if type(k) == 'number' then
|
|
v.Parent = obj
|
|
else
|
|
obj[k] = v
|
|
end
|
|
end
|
|
return obj
|
|
end
|
|
end
|
|
|
|
---- TWEENING ----
|
|
local ActiveTweens = {}
|
|
|
|
local function getActiveTween(prop, instance)
|
|
return ActiveTweens[prop] and ActiveTweens[prop][instance]
|
|
end
|
|
|
|
local function setActiveTween(prop, instance, newTween)
|
|
if not ActiveTweens[prop] then
|
|
ActiveTweens[prop] = {}
|
|
end
|
|
ActiveTweens[prop][instance] = newTween
|
|
end
|
|
|
|
function Util.Linear(t, b, c, d)
|
|
if t >= d then return b + c end
|
|
|
|
return c*t/d + b
|
|
end
|
|
|
|
function Util.EaseOutQuad(t, b, c, d)
|
|
if t >= d then return b + c end
|
|
|
|
t = t/d;
|
|
return -c * t*(t-2) + b
|
|
end
|
|
|
|
function Util.EaseInOutQuad(t, b, c, d)
|
|
if t >= d then return b + c end
|
|
|
|
t = t / (d/2);
|
|
if (t < 1) then return c/2*t*t + b end;
|
|
t = t - 1;
|
|
return -c/2 * (t*(t-2) - 1) + b;
|
|
end
|
|
|
|
function Util.SCurveUDim2(t, b, c, d)
|
|
if t >= d then return b + c end
|
|
t = t / d;
|
|
if t < 0 then t = 0 end
|
|
if t > 1 then t = 1 end
|
|
local T = 3 * t * t - 2 * t * t * t
|
|
return UDim2.new(
|
|
T * c.X.Scale,
|
|
T * c.X.Offset,
|
|
T * c.Y.Scale,
|
|
T * c.Y.Offset) + b
|
|
end
|
|
|
|
function Util.SCurveVector2(t, b, c, d)
|
|
if t >= d then return b + c end
|
|
t = t / d;
|
|
if t < 0 then t = 0 end
|
|
if t > 1 then t = 1 end
|
|
local T = 3 * t * t - 2 * t * t * t
|
|
return Vector2.new(
|
|
T * c.X,
|
|
T * c.Y) + b
|
|
end
|
|
|
|
function Util.PropertyTweener(instance, prop, start, final, duration, easingFunc, override, callbackFunction)
|
|
easingFunc = easingFunc or Util.Linear
|
|
override = override or false
|
|
|
|
local this = {}
|
|
this.StartTime = tick()
|
|
this.EndTime = this.StartTime + duration
|
|
this.Cancelled = false
|
|
|
|
local finished = false
|
|
local percentComplete = 0
|
|
|
|
|
|
local function setValue(newValue)
|
|
if instance then
|
|
instance[prop] = newValue
|
|
end
|
|
end
|
|
|
|
local function finalize()
|
|
setValue(easingFunc(1, start, final - start, 1))
|
|
finished = true
|
|
percentComplete = 1
|
|
|
|
if getActiveTween(prop, instance) == this then
|
|
setActiveTween(prop, instance, nil)
|
|
end
|
|
|
|
if callbackFunction then
|
|
callbackFunction()
|
|
end
|
|
end
|
|
|
|
if override or not getActiveTween(prop, instance) then
|
|
if getActiveTween(prop, instance) then
|
|
getActiveTween(prop, instance):Cancel()
|
|
end
|
|
setActiveTween(prop, instance, this)
|
|
|
|
-- Initial set
|
|
setValue(easingFunc(0, start, final - start, duration))
|
|
spawn(function()
|
|
local now = tick()
|
|
while now < this.EndTime and instance and not this.Cancelled do
|
|
setValue(easingFunc(now - this.StartTime, start, final - start, duration))
|
|
percentComplete = Util.Clamp(0, 1, (now - this.StartTime) / duration)
|
|
RunService.RenderStepped:wait()
|
|
now = tick()
|
|
end
|
|
if this.Cancelled == false and instance then
|
|
finalize()
|
|
end
|
|
|
|
if getActiveTween(prop, instance) == this then
|
|
setActiveTween(prop, instance, nil)
|
|
end
|
|
end)
|
|
else
|
|
finished = true
|
|
end
|
|
|
|
function this:GetFinal()
|
|
return final
|
|
end
|
|
|
|
function this:GetPercentComplete()
|
|
return percentComplete
|
|
end
|
|
|
|
function this:IsFinished()
|
|
return finished
|
|
end
|
|
|
|
function this:Finish()
|
|
if not finished then
|
|
self:Cancel()
|
|
finalize()
|
|
end
|
|
end
|
|
|
|
function this:Cancel()
|
|
this.Cancelled = true
|
|
finished = true
|
|
if getActiveTween(prop, instance) == this then
|
|
setActiveTween(prop, instance, nil)
|
|
end
|
|
end
|
|
|
|
return this
|
|
end
|
|
|
|
function Util.CancelTweens(tweens)
|
|
for i, tween in pairs(tweens) do
|
|
tween:Finish()
|
|
tweens[i] = nil
|
|
end
|
|
end
|
|
---------------
|
|
|
|
--- EVENTS ----
|
|
function Util.Signal()
|
|
local sig = {}
|
|
|
|
local mSignaler = Instance.new('BindableEvent')
|
|
|
|
local mArgData = nil
|
|
local mArgDataCount = nil
|
|
|
|
function sig:fire(...)
|
|
mArgData = {...}
|
|
mArgDataCount = select('#', ...)
|
|
mSignaler:Fire()
|
|
end
|
|
|
|
function sig:connect(f)
|
|
if not f then error("connect(nil)", 2) end
|
|
return mSignaler.Event:connect(function()
|
|
f(unpack(mArgData, 1, mArgDataCount))
|
|
end)
|
|
end
|
|
|
|
function sig:wait()
|
|
mSignaler.Event:wait()
|
|
assert(mArgData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.")
|
|
return unpack(mArgData, 1, mArgDataCount)
|
|
end
|
|
|
|
return sig
|
|
end
|
|
|
|
function Util.DisconnectEvent(conn)
|
|
if conn then
|
|
conn:disconnect()
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Util.DisconnectEvents(conns)
|
|
if conns and type(conns) == 'table' then
|
|
for _, conn in pairs(conns) do
|
|
conn:disconnect()
|
|
end
|
|
end
|
|
end
|
|
--------------
|
|
|
|
-- MATH --
|
|
function Util.Clamp(low, high, input)
|
|
return math.max(low, math.min(high, input))
|
|
end
|
|
|
|
function Util.ClampVector2(low, high, input)
|
|
return Vector2.new(Util.Clamp(low.x, high.x, input.x), Util.Clamp(low.y, high.y, input.y))
|
|
end
|
|
|
|
function Util.TweenPositionOrSet(guiObject, ...)
|
|
if guiObject:IsDescendantOf(game) then
|
|
guiObject:TweenPosition(...)
|
|
else
|
|
guiObject.Position = select(1, ...)
|
|
end
|
|
end
|
|
----
|
|
|
|
function Util.ClampCanvasPosition(scrollingContainer, position)
|
|
local container = scrollingContainer
|
|
local parentSize = container.Parent and container.Parent.AbsoluteSize or Vector2.new(0,0)
|
|
local absoluteCanvasSize = Vector2.new(container.CanvasSize.X.Scale * parentSize.X + container.CanvasSize.X.Offset,
|
|
container.CanvasSize.Y.Scale * parentSize.Y + container.CanvasSize.Y.Offset)
|
|
local nextX = Util.Clamp(0, absoluteCanvasSize.X - container.AbsoluteWindowSize.X, position.X)
|
|
local nextY = Util.Clamp(0, absoluteCanvasSize.Y - container.AbsoluteWindowSize.Y, position.Y)
|
|
|
|
return Vector2.new(nextX, nextY)
|
|
end
|
|
|
|
function Util.Round(num, roundToNearest)
|
|
roundToNearest = roundToNearest or 1
|
|
return math.floor((num + roundToNearest/2) / roundToNearest) * roundToNearest
|
|
end
|
|
--------------
|
|
|
|
-- FORMATING --
|
|
-- Removed whitespace from the beginning and end of the string
|
|
function Util.ChompString(str)
|
|
return tostring(str):gsub("^%s+" , ""):gsub("%s+$" , "")
|
|
end
|
|
|
|
-- replace multiple whitespace with one; remove leading and trailing whitespace
|
|
function Util.SpaceNormalizeString(str)
|
|
return tostring(str):gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , "")
|
|
end
|
|
|
|
function Util.FormatNumberString(value)
|
|
-- Make sure beginning and end of the string is clipped
|
|
local stringValue = Util.ChompString(value)
|
|
return stringValue:reverse():gsub("%d%d%d", "%1,"):reverse():gsub("^,", "")
|
|
end
|
|
|
|
-- PrettyPrint function for formatting data structures into flat strings, useful for debugging
|
|
|
|
function Util.PrettyPrint(tb)
|
|
if type(tb) == 'table' then
|
|
local str = "{"
|
|
for k, v in pairs(tb) do
|
|
str = ((str == "{") and str or str..", ")
|
|
if type(k) == 'string' then
|
|
str = str..k.." = "
|
|
elseif type(k) == 'number' then
|
|
-- nothing
|
|
else
|
|
str = str.."["..k.."] = "
|
|
end
|
|
str = str..Util.PrettyPrint(v)
|
|
end
|
|
return str.."}"
|
|
elseif type(tb) == 'string' then
|
|
return "'"..tb.."'"
|
|
else
|
|
return tostring(tb)
|
|
end
|
|
end
|
|
|
|
local isSandboxed = nil
|
|
function Util.DebugLog(...)
|
|
if isSandboxed == nil then
|
|
isSandboxed = PlatformService and PlatformService:IsSandboxed()
|
|
end
|
|
if isSandboxed or PlatformService == nil then
|
|
print(...)
|
|
end
|
|
end
|
|
|
|
-- K is a tunable parameter that changes the shape of the S-curve
|
|
-- the larger K is the more straight/linear the curve gets
|
|
local function SCurveTranform(t, k, lowerK)
|
|
k = k or 0.35
|
|
lowerK = lowerK or 0.8
|
|
t = Util.Clamp(-1,1,t)
|
|
if t >= 0 then
|
|
return (k*t) / (k - t + 1)
|
|
end
|
|
return -((lowerK*-t) / (lowerK + t + 1))
|
|
end
|
|
|
|
local function toSCurveSpace(t, deadzone)
|
|
deadzone = deadzone or 0.1
|
|
return (1 + deadzone) * (2*math.abs(t) - 1) - deadzone
|
|
end
|
|
|
|
local function fromSCurveSpace(t)
|
|
return t/2 + 0.5
|
|
end
|
|
|
|
function Util.GamepadLinearToCurve(thumbstickPosition, deadzone, k, lowerK)
|
|
local function onAxis(axisValue)
|
|
local sign = axisValue < 0 and -1 or 1
|
|
|
|
local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue), deadzone)), k, lowerK)
|
|
return Util.Clamp(-1,1, point * sign)
|
|
end
|
|
return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y))
|
|
end
|
|
|
|
|
|
function Util.IsFastFlagEnabled(flagName)
|
|
local success, isFlagEnabled = pcall(function()
|
|
return settings():GetFFlag(flagName)
|
|
end)
|
|
|
|
if success and not isFlagEnabled then
|
|
Util.DebugLog("Fast Flag:", flagName, "is currently not enabled.")
|
|
elseif not success then
|
|
Util.DebugLog("GetFFlag failed for flag:", flagName)
|
|
end
|
|
|
|
return success and isFlagEnabled
|
|
end
|
|
|
|
|
|
function Util.GetFastVariable(variableName)
|
|
local success, value = pcall(function()
|
|
return settings():GetFVariable(variableName)
|
|
end)
|
|
|
|
return success and value
|
|
end
|
|
|
|
local function getLocalPlayer()
|
|
while Players.LocalPlayer == nil do
|
|
wait()
|
|
end
|
|
return Players.LocalPlayer
|
|
end
|
|
|
|
function Util.IsFeatureNonZero(fastIntName)
|
|
return (tonumber(Util.GetFastVariable(fastIntName)) or 0) ~= 0
|
|
end
|
|
|
|
function Util.IsFeatureRolledOut(fastIntName)
|
|
return getLocalPlayer().UserId % 100 < (tonumber(Util.GetFastVariable(fastIntName)) or 0)
|
|
end
|
|
|
|
|
|
function Util.ExponentialRepeat(loopPredicate, loopBody, repeatCount)
|
|
repeatCount = repeatCount or 6
|
|
local retryCount = 1
|
|
local numRetries = repeatCount
|
|
|
|
while retryCount <= numRetries and loopPredicate() do
|
|
local done = loopBody()
|
|
if done then return end
|
|
wait(retryCount ^ 2)
|
|
retryCount = retryCount + 1
|
|
end
|
|
|
|
end
|
|
|
|
function Util.SplitString(str, sep)
|
|
local result = {}
|
|
if str and sep then
|
|
for word in string.gmatch(str, '([^' .. sep .. ']+)') do
|
|
table.insert(result, word)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
|
|
local function findAssetsHelper(object, result, baseUrl)
|
|
if not object then return end
|
|
|
|
if object:IsA('CharacterMesh') then
|
|
if object.MeshId > 0 then
|
|
table.insert(result, baseUrl .. tostring(object.MeshId))
|
|
end
|
|
if object.BaseTextureId > 0 then
|
|
table.insert(result, baseUrl .. tostring(object.BaseTextureId))
|
|
end
|
|
if object.OverlayTextureId > 0 then
|
|
table.insert(result, baseUrl .. tostring(object.OverlayTextureId))
|
|
end
|
|
elseif object:IsA('FileMesh') then
|
|
table.insert(result, object.MeshId)
|
|
table.insert(result, object.TextureId)
|
|
elseif object:IsA('Decal') then
|
|
table.insert(result, object.Texture)
|
|
elseif object:IsA('Pants') then
|
|
table.insert(result, object.PantsTemplate)
|
|
elseif object:IsA('Shirt') then
|
|
table.insert(result, object.ShirtTemplate)
|
|
end
|
|
|
|
for _, child in pairs(object:GetChildren()) do
|
|
findAssetsHelper(child, result, baseUrl)
|
|
end
|
|
end
|
|
function Util.FindAssetsInModel(object, baseUrl)
|
|
baseUrl = baseUrl or 'https://assetgame.roblox.com/asset/?id='
|
|
local result = {}
|
|
findAssetsHelper(object, result, baseUrl)
|
|
return result
|
|
end
|
|
|
|
function Util.ConvertFontSizeEnumToInt(fontSizeEnum)
|
|
local name = fontSizeEnum.Name
|
|
-- TODO: this is sort of gross?
|
|
local result = string.match(name, '%d+')
|
|
return result or 10
|
|
end
|
|
|
|
function Util.Upper(text)
|
|
if PlatformService then
|
|
return PlatformService:Upper(text)
|
|
else
|
|
return string.upper(text)
|
|
end
|
|
end
|
|
|
|
function Util.SetSelectedCoreObject(obj)
|
|
GuiService.SelectedCoreObject = obj
|
|
end
|
|
|
|
function Util.AddSelectionParent(selectionName, selectionParent)
|
|
GuiService:AddSelectionParent(selectionName, selectionParent)
|
|
end
|
|
|
|
function Util.RemoveSelectionGroup(selectionName)
|
|
GuiService:RemoveSelectionGroup(selectionName)
|
|
end
|
|
|
|
function Util.GetTextBounds(textGui)
|
|
local getFinalBounds = false
|
|
local textBound = nil
|
|
if textGui then
|
|
if textGui.TextFits then
|
|
textBound = textGui.TextBounds
|
|
getFinalBounds = true
|
|
else
|
|
--If TextWrapped is true, we can't really figure out what are the bounds should be, we need at least get some constraints on X/Y
|
|
if not textGui.TextWrapped then
|
|
textBound = TextService:GetTextSize(textGui.Text, textGui.TextSize, textGui.Font, Vector2.new(0, 0))
|
|
getFinalBounds = true
|
|
end
|
|
end
|
|
end
|
|
|
|
return getFinalBounds, textBound
|
|
end
|
|
|
|
--A utility func to resize button based on the textGui
|
|
--(textGui doesn't necessarily a child of the button, this func just makes sure the textGui can fit in the buttonGui),
|
|
--offsetX and offsetY are used to specify the distance from text border to button border
|
|
--We assume the alignment style is 'Center', so always add offsetX * 2 and offsetY * 2, need to adjust the input offset if the alignment is not 'Center'
|
|
--if it's nil, it means we don't overwrite the button's size on that axis
|
|
--Return the result which indicateds whether we resized the button
|
|
--Note: We resize the button by changing it's Size.Offset, so if it's original size is based on Size.Scale, the button won't be resized
|
|
function Util.ResizeButtonWithText(buttonGui, textGui, offsetX, offsetY)
|
|
local resized = false
|
|
local success, textBound = Util.GetTextBounds(textGui)
|
|
if success then
|
|
if buttonGui.Size.X.Scale == 0 and buttonGui.Size.Y.Scale == 0 then
|
|
--Can't depend on the AbsoluteSize, which will be 0,0 if the buttonGui is not a descendant of worksapce
|
|
local buttonAbsSize = Vector2.new(buttonGui.Size.X.Offset, buttonGui.Size.Y.Offset)
|
|
local newAbsSizeX, newAbsSizeY = buttonAbsSize.X, buttonAbsSize.Y
|
|
if offsetX and buttonAbsSize.X < textBound.X + offsetX * 2 then
|
|
newAbsSizeX = textBound.X + offsetX * 2
|
|
resized = true
|
|
end
|
|
if offsetY and buttonAbsSize.Y < textBound.Y + offsetY * 2 then
|
|
newAbsSizeY = textBound.Y + offsetY * 2
|
|
resized = true
|
|
end
|
|
|
|
if resized then
|
|
buttonGui.Size = UDim2.new(0, newAbsSizeX, 0, newAbsSizeY)
|
|
end
|
|
end
|
|
end
|
|
return resized
|
|
end
|
|
|
|
--This is used to get the suitable size for the buttonGui to fit different texts
|
|
--alternativeTexts is an array of all possible texts in the buttonGui
|
|
function Util.ResizeButtonWithDynamicText(buttonGui, textGui, alternativeTexts, offsetX, offsetY)
|
|
local resized = false
|
|
resized = Util.ResizeButtonWithText(buttonGui, textGui, offsetX, offsetY) or resized
|
|
local originalText = textGui.Text
|
|
if alternativeTexts and #alternativeTexts > 0 then
|
|
for i = 1, #alternativeTexts do
|
|
textGui.Text = alternativeTexts[i]
|
|
resized = Util.ResizeButtonWithText(buttonGui, textGui, offsetX, offsetY) or resized
|
|
end
|
|
end
|
|
|
|
textGui.Text = originalText
|
|
return resized
|
|
end
|
|
end
|
|
|
|
return Util
|