Compile Fusion library from corescripts/Libraries directory

This commit is contained in:
Lewin Kelly 2024-01-28 09:11:25 +00:00
parent 6daece9dc0
commit e603de10d1
57 changed files with 4678 additions and 3574 deletions

View File

@ -0,0 +1,222 @@
--!nonstrict
--[[
Constructs a new computed state object, which follows the value of another
state object using a spring simulation.
]]
local PubTypes = require "../PubTypes"
local Types = require "../Types"
local logError = require "../Logging/logError"
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
local unpackType = require "../Animation/unpackType"
local SpringScheduler = require "../Animation/SpringScheduler"
local updateAll = require "../State/updateAll"
local xtypeof = require "../Utility/xtypeof"
local peek = require "../State/peek"
local typeof = require "../Polyfill/typeof"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Sets the position of the internal springs, meaning the value of this
Spring will jump to the given value. This doesn't affect velocity.
If the type doesn't match the current type of the spring, an error will be
thrown.
]]
function class:setPosition(newValue: PubTypes.Animatable)
local newType = typeof(newValue)
if newType ~= self._currentType then
logError("springTypeMismatch", nil, newType, self._currentType)
end
self._springPositions = unpackType(newValue, newType)
self._currentValue = newValue
SpringScheduler.add(self)
updateAll(self)
end
--[[
Sets the velocity of the internal springs, overwriting the existing velocity
of this Spring. This doesn't affect position.
If the type doesn't match the current type of the spring, an error will be
thrown.
]]
function class:setVelocity(newValue: PubTypes.Animatable)
local newType = typeof(newValue)
if newType ~= self._currentType then
logError("springTypeMismatch", nil, newType, self._currentType)
end
self._springVelocities = unpackType(newValue, newType)
SpringScheduler.add(self)
end
--[[
Adds to the velocity of the internal springs, on top of the existing
velocity of this Spring. This doesn't affect position.
If the type doesn't match the current type of the spring, an error will be
thrown.
]]
function class:addVelocity(deltaValue: PubTypes.Animatable)
local deltaType = typeof(deltaValue)
if deltaType ~= self._currentType then
logError("springTypeMismatch", nil, deltaType, self._currentType)
end
local springDeltas = unpackType(deltaValue, deltaType)
for index, delta in ipairs(springDeltas) do
self._springVelocities[index] += delta
end
SpringScheduler.add(self)
end
--[[
Called when the goal state changes value, or when the speed or damping has
changed.
]]
function class:update(): boolean
local goalValue = peek(self._goalState)
-- figure out if this was a goal change or a speed/damping change
if goalValue == self._goalValue then
-- speed/damping change
local damping = peek(self._damping)
if typeof(damping) ~= "number" then
logErrorNonFatal("mistypedSpringDamping", nil, typeof(damping))
elseif damping < 0 then
logErrorNonFatal("invalidSpringDamping", nil, damping)
else
self._currentDamping = damping
end
local speed = peek(self._speed)
if typeof(speed) ~= "number" then
logErrorNonFatal("mistypedSpringSpeed", nil, typeof(speed))
elseif speed < 0 then
logErrorNonFatal("invalidSpringSpeed", nil, speed)
else
self._currentSpeed = speed
end
return false
else
-- goal change - reconfigure spring to target new goal
self._goalValue = goalValue
local oldType = self._currentType
local newType = typeof(goalValue)
self._currentType = newType
local springGoals = unpackType(goalValue, newType)
local numSprings = #springGoals
self._springGoals = springGoals
if newType ~= oldType then
-- if the type changed, snap to the new value and rebuild the
-- position and velocity tables
self._currentValue = self._goalValue
-- local springPositions = (numSprings, 0)
local springPositions = {}
for i = 1, numSprings do
springPositions[i] = 0
end
-- local springVelocities = (numSprings, 0)
local springVelocities = {}
for i = 1, numSprings do
springVelocities[i] = 0
end
for index, springGoal in ipairs(springGoals) do
springPositions[index] = springGoal
end
self._springPositions = springPositions
self._springVelocities = springVelocities
-- the spring may have been animating before, so stop that
SpringScheduler.remove(self)
return true
-- otherwise, the type hasn't changed, just the goal...
elseif numSprings == 0 then
-- if the type isn't animatable, snap to the new value
self._currentValue = self._goalValue
return true
else
-- if it's animatable, let it animate to the goal
SpringScheduler.add(self)
return false
end
end
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._currentValue
end
function class:get()
logError "stateGetWasRemoved"
end
local function Spring<T>(
goalState: PubTypes.Value<T>,
speed: PubTypes.CanBeState<number>?,
damping: PubTypes.CanBeState<number>?
): Types.Spring<T>
-- apply defaults for speed and damping
if speed == nil then
speed = 10
end
if damping == nil then
damping = 1
end
local dependencySet = { [goalState] = true }
if xtypeof(speed) == "State" then
dependencySet[speed] = true
end
if xtypeof(damping) == "State" then
dependencySet[damping] = true
end
local self = setmetatable({
type = "State",
kind = "Spring",
dependencySet = dependencySet,
-- if we held strong references to the dependents, then they wouldn't be
-- able to get garbage collected when they fall out of scope
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
_speed = speed,
_damping = damping,
_goalState = goalState,
_goalValue = nil,
_currentType = nil,
_currentValue = nil,
_currentSpeed = peek(speed),
_currentDamping = peek(damping),
_springPositions = nil,
_springGoals = nil,
_springVelocities = nil,
}, CLASS_METATABLE)
-- add this object to the goal state's dependent set
goalState.dependentSet[self] = true
self:update()
return self
end
return Spring

View File

@ -0,0 +1,98 @@
--!strict
--[[
Manages batch updating of spring objects.
]]
local Types = require "../Types"
local External = require "../External"
local packType = require "../Animation/packType"
local springCoefficients = require "../Animation/springCoefficients"
local updateAll = require "../State/updateAll"
type Set<T> = { [T]: any }
type Spring = Types.Spring<any>
local SpringScheduler = {}
local EPSILON = 0.0001
local activeSprings: Set<Spring> = {}
local lastUpdateTime = External.lastUpdateStep()
function SpringScheduler.add(spring: Spring)
-- we don't necessarily want to use the most accurate time - here we snap to
-- the last update time so that springs started within the same frame have
-- identical time steps
spring._lastSchedule = lastUpdateTime
spring._startDisplacements = {}
spring._startVelocities = {}
for index, goal in ipairs(spring._springGoals) do
spring._startDisplacements[index] = spring._springPositions[index]
- goal
spring._startVelocities[index] = spring._springVelocities[index]
end
activeSprings[spring] = true
end
function SpringScheduler.remove(spring: Spring)
activeSprings[spring] = nil
end
local function updateAllSprings(now: number)
local springsToSleep: Set<Spring> = {}
lastUpdateTime = now
for spring in pairs(activeSprings) do
local posPos, posVel, velPos, velVel = springCoefficients(
lastUpdateTime - spring._lastSchedule,
spring._currentDamping,
spring._currentSpeed
)
local positions = spring._springPositions
local velocities = spring._springVelocities
local startDisplacements = spring._startDisplacements
local startVelocities = spring._startVelocities
local isMoving = false
for index, goal in ipairs(spring._springGoals) do
local oldDisplacement = startDisplacements[index]
local oldVelocity = startVelocities[index]
local newDisplacement = oldDisplacement * posPos
+ oldVelocity * posVel
local newVelocity = oldDisplacement * velPos + oldVelocity * velVel
if
math.abs(newDisplacement) > EPSILON
or math.abs(newVelocity) > EPSILON
then
isMoving = true
end
positions[index] = newDisplacement + goal
velocities[index] = newVelocity
end
if not isMoving then
springsToSleep[spring] = true
end
end
for spring in pairs(activeSprings) do
spring._currentValue =
packType(spring._springPositions, spring._currentType)
updateAll(spring)
end
for spring in pairs(springsToSleep) do
activeSprings[spring] = nil
-- Guarantee that springs reach exact goals, since mathematically they only approach it infinitely
spring._currentValue =
packType(spring._springGoals, spring._currentType)
end
end
External.bindToUpdateStep(updateAllSprings)
return SpringScheduler

View File

@ -0,0 +1,126 @@
--!nonstrict
--[[
Constructs a new computed state object, which follows the value of another
state object using a tween.
]]
local PubTypes = require "../PubTypes"
local External = require "../External"
local Types = require "../Types"
local TweenScheduler = require "../Animation/TweenScheduler"
local logError = require "../Logging/logError"
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
local xtypeof = require "../Utility/xtypeof"
local peek = require "../State/peek"
local typeof = require "../Polyfill/typeof"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Called when the goal state changes value; this will initiate a new tween.
Returns false as the current value doesn't change right away.
]]
function class:update(): boolean
local goalValue = peek(self._goalState)
-- if the goal hasn't changed, then this is a TweenInfo change.
-- in that case, if we're not currently animating, we can skip everything
if goalValue == self._nextValue and not self._currentlyAnimating then
return false
end
local tweenInfo = peek(self._tweenInfo)
-- if we receive a bad TweenInfo, then error and stop the update
if typeof(tweenInfo) ~= "TweenInfo" then
logErrorNonFatal("mistypedTweenInfo", nil, typeof(tweenInfo))
return false
end
self._prevValue = self._currentValue
self._nextValue = goalValue
self._currentTweenStartTime = External.lastUpdateStep()
self._currentTweenInfo = tweenInfo
local tweenDuration = tweenInfo.DelayTime + tweenInfo.Time
if tweenInfo.Reverses then
tweenDuration += tweenInfo.Time
end
tweenDuration *= tweenInfo.RepeatCount + 1
self._currentTweenDuration = tweenDuration
-- start animating this tween
TweenScheduler.add(self)
return false
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._currentValue
end
function class:get()
logError "stateGetWasRemoved"
end
local function Tween<T>(
goalState: PubTypes.StateObject<PubTypes.Animatable>,
tweenInfo: PubTypes.CanBeState<TweenInfo>?
): Types.Tween<T>
local currentValue = peek(goalState)
-- apply defaults for tween info
if tweenInfo == nil then
tweenInfo = TweenInfo.new()
end
local dependencySet = { [goalState] = true }
local tweenInfoIsState = xtypeof(tweenInfo) == "State"
if tweenInfoIsState then
dependencySet[tweenInfo] = true
end
local startingTweenInfo = peek(tweenInfo)
-- If we start with a bad TweenInfo, then we don't want to construct a Tween
if typeof(startingTweenInfo) ~= "TweenInfo" then
logError("mistypedTweenInfo", nil, typeof(startingTweenInfo))
end
local self = setmetatable({
type = "State",
kind = "Tween",
dependencySet = dependencySet,
-- if we held strong references to the dependents, then they wouldn't be
-- able to get garbage collected when they fall out of scope
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
_goalState = goalState,
_tweenInfo = tweenInfo,
_tweenInfoIsState = tweenInfoIsState,
_prevValue = currentValue,
_nextValue = currentValue,
_currentValue = currentValue,
-- store current tween into separately from 'real' tween into, so it
-- isn't affected by :setTweenInfo() until next change
_currentTweenInfo = tweenInfo,
_currentTweenDuration = 0,
_currentTweenStartTime = 0,
_currentlyAnimating = false,
}, CLASS_METATABLE)
-- add this object to the goal state's dependent set
goalState.dependentSet[self] = true
return self
end
return Tween

View File

@ -0,0 +1,71 @@
--!strict
--[[
Manages batch updating of tween objects.
]]
local Types = require "../Types"
local External = require "../External"
local lerpType = require "../Animation/lerpType"
local getTweenRatio = require "../Animation/getTweenRatio"
local updateAll = require "../State/updateAll"
local TweenScheduler = {}
type Set<T> = { [T]: any }
type Tween = Types.Tween<any>
local WEAK_KEYS_METATABLE = { __mode = "k" }
-- all the tweens currently being updated
local allTweens: Set<Tween> = {}
setmetatable(allTweens, WEAK_KEYS_METATABLE)
--[[
Adds a Tween to be updated every render step.
]]
function TweenScheduler.add(tween: Tween)
allTweens[tween] = true
end
--[[
Removes a Tween from the scheduler.
]]
function TweenScheduler.remove(tween: Tween)
allTweens[tween] = nil
end
--[[
Updates all Tween objects.
]]
local function updateAllTweens(now: number)
-- FIXME: Typed Luau doesn't understand this loop yet
for tween: Tween in pairs(allTweens :: any) do
local currentTime = now - tween._currentTweenStartTime
if
currentTime > tween._currentTweenDuration
and tween._currentTweenInfo.RepeatCount > -1
then
if tween._currentTweenInfo.Reverses then
tween._currentValue = tween._prevValue
else
tween._currentValue = tween._nextValue
end
tween._currentlyAnimating = false
updateAll(tween)
TweenScheduler.remove(tween)
else
local ratio = getTweenRatio(tween._currentTweenInfo, currentTime)
local currentValue =
lerpType(tween._prevValue, tween._nextValue, ratio)
tween._currentValue = currentValue
tween._currentlyAnimating = true
updateAll(tween)
end
end
end
External.bindToUpdateStep(updateAllTweens)
return TweenScheduler

View File

@ -0,0 +1,46 @@
--!strict
--[[
Given a `tweenInfo` and `currentTime`, returns a ratio which can be used to
tween between two values over time.
]]
-- local TweenService = game:GetService "TweenService"
local easing = require "../Polyfill/easing"
local function getTweenRatio(tweenInfo: TweenInfo, currentTime: number): number
local delay = tweenInfo.DelayTime
local duration = tweenInfo.Time
local reverses = tweenInfo.Reverses
local numCycles = 1 + tweenInfo.RepeatCount
local easeStyle = tweenInfo.EasingStyle
local easeDirection = tweenInfo.EasingDirection
local cycleDuration = delay + duration
if reverses then
cycleDuration += duration
end
if
currentTime >= cycleDuration * numCycles
and tweenInfo.RepeatCount > -1
then
return 1
end
local cycleTime = currentTime % cycleDuration
if cycleTime <= delay then
return 0
end
local tweenProgress = (cycleTime - delay) / duration
if tweenProgress > 1 then
tweenProgress = 2 - tweenProgress
end
-- return TweenService:GetValue(tweenProgress, easeStyle, easeDirection)
return easing[easeStyle][easeDirection](tweenProgress, 0, 1)
end
return getTweenRatio

View File

@ -0,0 +1,134 @@
--!strict
--[[
Linearly interpolates the given animatable types by a ratio.
If the types are different or not animatable, then the first value will be
returned for ratios below 0.5, and the second value for 0.5 and above.
FIXME: This function uses a lot of redefinitions to suppress false positives
from the Luau typechecker - ideally these wouldn't be required
]]
local Oklab = require "../Colour/Oklab"
local typeof = require "../Polyfill/typeof"
local function lerpType(from: any, to: any, ratio: number): any
local typeString = typeof(from)
if typeof(to) == typeString then
-- both types must match for interpolation to make sense
if typeString == "number" then
return (to - from) * ratio + from
elseif typeString == "CFrame" then
return from:Lerp(to, ratio)
elseif typeString == "Color3" then
local fromLab = Oklab.to(from)
local toLab = Oklab.to(to)
return Oklab.from(fromLab:Lerp(toLab, ratio), false)
-- elseif typeString == "ColorSequenceKeypoint" then
-- local to, from =
-- to :: ColorSequenceKeypoint, from :: ColorSequenceKeypoint
-- local fromLab = Oklab.to(from.Value)
-- local toLab = Oklab.to(to.Value)
-- return ColorSequenceKeypoint.new(
-- (to.Time - from.Time) * ratio + from.Time,
-- Oklab.from(fromLab:Lerp(toLab, ratio), false)
-- )
-- elseif typeString == "DateTime" then
-- local to, from = to :: DateTime, from :: DateTime
-- return DateTime.fromUnixTimestampMillis(
-- (to.UnixTimestampMillis - from.UnixTimestampMillis) * ratio
-- + from.UnixTimestampMillis
-- )
-- elseif typeString == "NumberRange" then
-- local to, from = to :: NumberRange, from :: NumberRange
-- return NumberRange.new(
-- (to.Min - from.Min) * ratio + from.Min,
-- (to.Max - from.Max) * ratio + from.Max
-- )
-- elseif typeString == "NumberSequenceKeypoint" then
-- local to, from =
-- to :: NumberSequenceKeypoint, from :: NumberSequenceKeypoint
-- return NumberSequenceKeypoint.new(
-- (to.Time - from.Time) * ratio + from.Time,
-- (to.Value - from.Value) * ratio + from.Value,
-- (to.Envelope - from.Envelope) * ratio + from.Envelope
-- )
-- elseif typeString == "PhysicalProperties" then
-- local to, from =
-- to :: PhysicalProperties, from :: PhysicalProperties
-- return PhysicalProperties.new(
-- (to.Density - from.Density) * ratio + from.Density,
-- (to.Friction - from.Friction) * ratio + from.Friction,
-- (to.Elasticity - from.Elasticity) * ratio + from.Elasticity,
-- (to.FrictionWeight - from.FrictionWeight) * ratio
-- + from.FrictionWeight,
-- (to.ElasticityWeight - from.ElasticityWeight) * ratio
-- + from.ElasticityWeight
-- )
elseif typeString == "Ray" then
return Ray.new(
from.Origin:Lerp(to.Origin, ratio),
from.Direction:Lerp(to.Direction, ratio)
)
-- elseif typeString == "Rect" then
-- local to, from = to :: Rect, from :: Rect
-- return Rect.new(
-- from.Min:Lerp(to.Min, ratio),
-- from.Max:Lerp(to.Max, ratio)
-- )
elseif typeString == "Region3" then
-- FUTURE: support rotated Region3s if/when they become constructable
local position =
from.CFrame.Position:Lerp(to.CFrame.Position, ratio)
local halfSize = from.Size:Lerp(to.Size, ratio) / 2
return Region3.new(position - halfSize, position + halfSize)
elseif typeString == "Region3int16" then
return Region3int16.new(
Vector3int16.new(
(to.Min.X - from.Min.X) * ratio + from.Min.X,
(to.Min.Y - from.Min.Y) * ratio + from.Min.Y,
(to.Min.Z - from.Min.Z) * ratio + from.Min.Z
),
Vector3int16.new(
(to.Max.X - from.Max.X) * ratio + from.Max.X,
(to.Max.Y - from.Max.Y) * ratio + from.Max.Y,
(to.Max.Z - from.Max.Z) * ratio + from.Max.Z
)
)
elseif typeString == "UDim" then
return UDim.new(
(to.Scale - from.Scale) * ratio + from.Scale,
(to.Offset - from.Offset) * ratio + from.Offset
)
elseif typeString == "UDim2" then
return UDim2.new(
(to.X.Scale - from.X.Scale) * ratio + from.X.Scale,
(to.X.Offset - from.X.Offset) * ratio + from.X.Offset,
(to.Y.Scale - from.Y.Scale) * ratio + from.Y.Scale,
(to.Y.Offset - from.Y.Offset) * ratio + from.Y.Offset
)
elseif typeString == "Vector2" or typeString == "Vector3" then
return from:Lerp(to, ratio)
elseif typeString == "Vector2int16" then
return Vector2int16.new(
(to.X - from.X) * ratio + from.X,
(to.Y - from.Y) * ratio + from.Y
)
elseif typeString == "Vector3int16" then
return Vector3int16.new(
(to.X - from.X) * ratio + from.X,
(to.Y - from.Y) * ratio + from.Y,
(to.Z - from.Z) * ratio + from.Z
)
end
end
-- fallback case: the types are different or not animatable
if ratio < 0.5 then
return from
end
return to
end
return lerpType

View File

@ -0,0 +1,86 @@
--!strict
--[[
Packs an array of numbers into a given animatable data type.
If the type is not animatable, nil will be returned.
FUTURE: When Luau supports singleton types, those could be used in
conjunction with intersection types to make this function fully statically
type checkable.
]]
local PubTypes = require "../PubTypes"
local Oklab = require "../Colour/Oklab"
local function packType(
numbers: { number },
typeString: string
): PubTypes.Animatable?
if typeString == "number" then
return numbers[1]
elseif typeString == "CFrame" then
return CFrame.new(numbers[1], numbers[2], numbers[3])
* CFrame.fromAxisAngle(
Vector3.new(numbers[4], numbers[5], numbers[6]).Unit,
numbers[7]
)
elseif typeString == "Color3" then
return Oklab.from(
Vector3.new(numbers[1], numbers[2], numbers[3]),
false
)
elseif typeString == "ColorSequenceKeypoint" then
return ColorSequenceKeypoint.new(
numbers[4],
Oklab.from(Vector3.new(numbers[1], numbers[2], numbers[3]), false)
)
elseif typeString == "DateTime" then
return DateTime.fromUnixTimestampMillis(numbers[1])
elseif typeString == "NumberRange" then
return NumberRange.new(numbers[1], numbers[2])
elseif typeString == "NumberSequenceKeypoint" then
return NumberSequenceKeypoint.new(numbers[2], numbers[1], numbers[3])
elseif typeString == "PhysicalProperties" then
return PhysicalProperties.new(
numbers[1],
numbers[2],
numbers[3],
numbers[4],
numbers[5]
)
elseif typeString == "Ray" then
return Ray.new(
Vector3.new(numbers[1], numbers[2], numbers[3]),
Vector3.new(numbers[4], numbers[5], numbers[6])
)
elseif typeString == "Rect" then
return Rect.new(numbers[1], numbers[2], numbers[3], numbers[4])
elseif typeString == "Region3" then
-- FUTURE: support rotated Region3s if/when they become constructable
local position = Vector3.new(numbers[1], numbers[2], numbers[3])
local halfSize =
Vector3.new(numbers[4] / 2, numbers[5] / 2, numbers[6] / 2)
return Region3.new(position - halfSize, position + halfSize)
elseif typeString == "Region3int16" then
return Region3int16.new(
Vector3int16.new(numbers[1], numbers[2], numbers[3]),
Vector3int16.new(numbers[4], numbers[5], numbers[6])
)
elseif typeString == "UDim" then
return UDim.new(numbers[1], numbers[2])
elseif typeString == "UDim2" then
return UDim2.new(numbers[1], numbers[2], numbers[3], numbers[4])
elseif typeString == "Vector2" then
return Vector2.new(numbers[1], numbers[2])
elseif typeString == "Vector2int16" then
return Vector2int16.new(numbers[1], numbers[2])
elseif typeString == "Vector3" then
return Vector3.new(numbers[1], numbers[2], numbers[3])
elseif typeString == "Vector3int16" then
return Vector3int16.new(numbers[1], numbers[2], numbers[3])
else
return nil
end
end
return packType

View File

@ -0,0 +1,86 @@
--!strict
--[[
Returns a 2x2 matrix of coefficients for a given time, damping and speed.
Specifically, this returns four coefficients - posPos, posVel, velPos, and
velVel - which can be multiplied with position and velocity like so:
local newPosition = oldPosition * posPos + oldVelocity * posVel
local newVelocity = oldPosition * velPos + oldVelocity * velVel
Special thanks to AxisAngle for helping to improve numerical precision.
]]
local function springCoefficients(
time: number,
damping: number,
speed: number
): (number, number, number, number)
-- if time or speed is 0, then the spring won't move
if time == 0 or speed == 0 then
return 1, 0, 0, 1
end
local posPos, posVel, velPos, velVel
if damping > 1 then
-- overdamped spring
-- solution to the characteristic equation:
-- z = -ζω ± Sqrt[ζ^2 - 1] ω
-- x[t] -> x0(e^(t z2) z1 - e^(t z1) z2)/(z1 - z2)
-- + v0(e^(t z1) - e^(t z2))/(z1 - z2)
-- v[t] -> x0(z1 z2(-e^(t z1) + e^(t z2)))/(z1 - z2)
-- + v0(z1 e^(t z1) - z2 e^(t z2))/(z1 - z2)
local scaledTime = time * speed
local alpha = math.sqrt(damping ^ 2 - 1)
local scaledInvAlpha = -0.5 / alpha
local z1 = -alpha - damping
local z2 = 1 / z1
local expZ1 = math.exp(scaledTime * z1)
local expZ2 = math.exp(scaledTime * z2)
posPos = (expZ2 * z1 - expZ1 * z2) * scaledInvAlpha
posVel = (expZ1 - expZ2) * scaledInvAlpha / speed
velPos = (expZ2 - expZ1) * scaledInvAlpha * speed
velVel = (expZ1 * z1 - expZ2 * z2) * scaledInvAlpha
elseif damping == 1 then
-- critically damped spring
-- x[t] -> x0(e^-tω)(1+tω) + v0(e^-tω)t
-- v[t] -> x0(t ω^2)(-e^-tω) + v0(1 - tω)(e^-tω)
local scaledTime = time * speed
local expTerm = math.exp(-scaledTime)
posPos = expTerm * (1 + scaledTime)
posVel = expTerm * time
velPos = expTerm * (-scaledTime * speed)
velVel = expTerm * (1 - scaledTime)
else
-- underdamped spring
-- factored out of the solutions to the characteristic equation:
-- α = Sqrt[1 - ζ^2]
-- x[t] -> x0(e^-tζω)(α Cos[tα] + ζω Sin[tα])/α
-- + v0(e^-tζω)(Sin[tα])/α
-- v[t] -> x0(-e^-tζω)(α^2 + ζ^2 ω^2)(Sin[tα])/α
-- + v0(e^-tζω)(α Cos[tα] - ζω Sin[tα])/α
local scaledTime = time * speed
local alpha = math.sqrt(1 - damping ^ 2)
local invAlpha = 1 / alpha
local alphaTime = alpha * scaledTime
local expTerm = math.exp(-scaledTime * damping)
local sinTerm = expTerm * math.sin(alphaTime)
local cosTerm = expTerm * math.cos(alphaTime)
local sinInvAlpha = sinTerm * invAlpha
local sinInvAlphaDamp = sinInvAlpha * damping
posPos = sinInvAlphaDamp + cosTerm
posVel = sinInvAlpha
velPos = -(sinInvAlphaDamp * damping + sinTerm * alpha)
velVel = cosTerm - sinInvAlphaDamp
end
return posPos, posVel, velPos, velVel
end
return springCoefficients

View File

@ -0,0 +1,92 @@
--!strict
--[[
Unpacks an animatable type into an array of numbers.
If the type is not animatable, an empty array will be returned.
FIXME: This function uses a lot of redefinitions to suppress false positives
from the Luau typechecker - ideally these wouldn't be required
FUTURE: When Luau supports singleton types, those could be used in
conjunction with intersection types to make this function fully statically
type checkable.
]]
local Oklab = require "../Colour/Oklab"
local function unpackType(value: any, typeString: string): { number }
if typeString == "number" then
return { value }
elseif typeString == "CFrame" then
-- FUTURE: is there a better way of doing this? doing distance
-- calculations on `angle` may be incorrect
local axis, angle = value:ToAxisAngle()
return { value.X, value.Y, value.Z, axis.X, axis.Y, axis.Z, angle }
elseif typeString == "Color3" then
local lab = Oklab.to(value)
return { lab.X, lab.Y, lab.Z }
elseif typeString == "ColorSequenceKeypoint" then
local lab = Oklab.to(value.Value)
return { lab.X, lab.Y, lab.Z, value.Time }
elseif typeString == "DateTime" then
return { value.UnixTimestampMillis }
elseif typeString == "NumberRange" then
return { value.Min, value.Max }
elseif typeString == "NumberSequenceKeypoint" then
return { value.Value, value.Time, value.Envelope }
elseif typeString == "PhysicalProperties" then
return {
value.Density,
value.Friction,
value.Elasticity,
value.FrictionWeight,
value.ElasticityWeight,
}
elseif typeString == "Ray" then
return {
value.Origin.X,
value.Origin.Y,
value.Origin.Z,
value.Direction.X,
value.Direction.Y,
value.Direction.Z,
}
elseif typeString == "Rect" then
return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }
elseif typeString == "Region3" then
-- FUTURE: support rotated Region3s if/when they become constructable
return {
value.CFrame.X,
value.CFrame.Y,
value.CFrame.Z,
value.Size.X,
value.Size.Y,
value.Size.Z,
}
elseif typeString == "Region3int16" then
return {
value.Min.X,
value.Min.Y,
value.Min.Z,
value.Max.X,
value.Max.Y,
value.Max.Z,
}
elseif typeString == "UDim" then
return { value.Scale, value.Offset }
elseif typeString == "UDim2" then
return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
elseif typeString == "Vector2" then
return { value.X, value.Y }
elseif typeString == "Vector2int16" then
return { value.X, value.Y }
elseif typeString == "Vector3" then
return { value.X, value.Y, value.Z }
elseif typeString == "Vector3int16" then
return { value.X, value.Y, value.Z }
else
return {}
end
end
return unpackType

View File

@ -0,0 +1,56 @@
--!strict
--[[
Provides functions for converting Color3s into Oklab space, for more
perceptually uniform colour blending.
See: https://bottosson.github.io/posts/oklab/
]]
local Oklab = {}
-- Converts a Color3 in RGB space to a Vector3 in Oklab space.
function Oklab.to(rgb: Color3): Vector3
local l = rgb.r * 0.4122214708 + rgb.g * 0.5363325363 + rgb.b * 0.0514459929
local m = rgb.r * 0.2119034982 + rgb.g * 0.6806995451 + rgb.b * 0.1073969566
local s = rgb.r * 0.0883024619 + rgb.g * 0.2817188376 + rgb.b * 0.6299787005
local lRoot = l ^ (1 / 3)
local mRoot = m ^ (1 / 3)
local sRoot = s ^ (1 / 3)
return Vector3.new(
lRoot * 0.2104542553 + mRoot * 0.7936177850 - sRoot * 0.0040720468,
lRoot * 1.9779984951 - mRoot * 2.4285922050 + sRoot * 0.4505937099,
lRoot * 0.0259040371 + mRoot * 0.7827717662 - sRoot * 0.8086757660
)
end
-- Converts a Vector3 in CIELAB space to a Color3 in RGB space.
-- The Color3 will be clamped by default unless specified otherwise.
function Oklab.from(lab: Vector3, unclamped: boolean?): Color3
local lRoot = lab.X + lab.Y * 0.3963377774 + lab.Z * 0.2158037573
local mRoot = lab.X - lab.Y * 0.1055613458 - lab.Z * 0.0638541728
local sRoot = lab.X - lab.Y * 0.0894841775 - lab.Z * 1.2914855480
local l = lRoot ^ 3
local m = mRoot ^ 3
local s = sRoot ^ 3
local red = l * 4.0767416621 - m * 3.3077115913 + s * 0.2309699292
local green = l * -1.2684380046 + m * 2.6097574011 - s * 0.3413193965
local blue = l * -0.0041960863 - m * 0.7034186147 + s * 1.7076147010
if not unclamped then
-- red = math.clamp(red, 0, 1)
-- green = math.clamp(green, 0, 1)
-- blue = math.clamp(blue, 0, 1)
red = math.max(0, math.min(red, 1))
green = math.max(0, math.min(green, 1))
blue = math.max(0, math.min(blue, 1))
end
return Color3.new(red, green, blue)
end
return Oklab

View File

@ -0,0 +1,97 @@
--!strict
--[[
Abstraction layer between Fusion internals and external environments,
allowing for flexible integration with schedulers and test mocks.
]]
local logError = require "./Logging/logError"
local External = {}
export type Scheduler = {
doTaskImmediate: (resume: () -> ()) -> (),
doTaskDeferred: (resume: () -> ()) -> (),
startScheduler: () -> (),
stopScheduler: () -> (),
}
local updateStepCallbacks = {}
local currentScheduler: Scheduler? = nil
local lastUpdateStep = 0
--[[
Sets the external scheduler that Fusion will use for queuing async tasks.
Returns the previous scheduler so it can be reset later.
]]
function External.setExternalScheduler(newScheduler: Scheduler?): Scheduler?
local oldScheduler = currentScheduler
if oldScheduler ~= nil then
oldScheduler.stopScheduler()
end
currentScheduler = newScheduler
if newScheduler ~= nil then
newScheduler.startScheduler()
end
return oldScheduler
end
--[[
Sends an immediate task to the external scheduler. Throws if none is set.
]]
function External.doTaskImmediate(resume: () -> ())
if currentScheduler == nil then
logError "noTaskScheduler"
else
currentScheduler.doTaskImmediate(resume)
end
end
--[[
Sends a deferred task to the external scheduler. Throws if none is set.
]]
function External.doTaskDeferred(resume: () -> ())
if currentScheduler == nil then
logError "noTaskScheduler"
else
currentScheduler.doTaskDeferred(resume)
end
end
--[[
Registers a callback to the update step of the external scheduler.
Returns a function that can be used to disconnect later.
Callbacks are given the current number of seconds since an arbitrary epoch.
TODO: This epoch may change between schedulers. We could investigate ways
of allowing schedulers to co-operate to keep the epoch the same, so that
monotonicity can be better preserved.
]]
function External.bindToUpdateStep(callback: (now: number) -> ()): () -> ()
local uniqueIdentifier = {}
updateStepCallbacks[uniqueIdentifier] = callback
return function()
updateStepCallbacks[uniqueIdentifier] = nil
end
end
--[[
Steps time-dependent systems with the current number of seconds since an
arbitrary epoch. This should be called as early as possible in the external
scheduler's update cycle.
]]
function External.performUpdateStep(now: number)
lastUpdateStep = now
for _, callback in pairs(updateStepCallbacks) do
callback(now)
end
end
--[[
Returns the timestamp of the last update step.
]]
function External.lastUpdateStep()
return lastUpdateStep
end
return External

View File

@ -0,0 +1,157 @@
--!strict
--[[
A special key for property tables, which parents any given descendants into
an instance.
]]
local PubTypes = require "../PubTypes"
local External = require "../External"
local logWarn = require "../Logging/logWarn"
local Observer = require "../State/Observer"
local peek = require "../State/peek"
local isState = require "../State/isState"
local typeof = require "../Polyfill/typeof"
type Set<T> = { [T]: boolean }
-- Experimental flag: name children based on the key used in the [Children] table
local EXPERIMENTAL_AUTO_NAMING = false
local Children = {}
Children.type = "SpecialKey"
Children.kind = "Children"
Children.stage = "descendants"
function Children:apply(
propValue: any,
applyTo: Instance,
cleanupTasks: { PubTypes.Task }
)
local newParented: Set<Instance> = {}
local oldParented: Set<Instance> = {}
-- save disconnection functions for state object observers
local newDisconnects: { [PubTypes.StateObject<any>]: () -> () } = {}
local oldDisconnects: { [PubTypes.StateObject<any>]: () -> () } = {}
local updateQueued = false
local queueUpdate: () -> ()
-- Rescans this key's value to find new instances to parent and state objects
-- to observe for changes; then unparents instances no longer found and
-- disconnects observers for state objects no longer present.
local function updateChildren()
if not updateQueued then
return -- this update may have been canceled by destruction, etc.
end
updateQueued = false
oldParented, newParented = newParented, oldParented
oldDisconnects, newDisconnects = newDisconnects, oldDisconnects
-- table.clear(newParented)
for i, _ in pairs(newParented) do
newParented[i] = nil
end
-- table.clear(newDisconnects)
for i, _ in pairs(newDisconnects) do
newDisconnects[i] = nil
end
local function processChild(child: any, autoName: string?)
local childType = typeof(child)
if childType == "Instance" then
-- case 1; single instance
newParented[child] = true
if oldParented[child] == nil then
-- wasn't previously present
-- TODO: check for ancestry conflicts here
child.Parent = applyTo
else
-- previously here; we want to reuse, so remove from old
-- set so we don't encounter it during unparenting
oldParented[child] = nil
end
if EXPERIMENTAL_AUTO_NAMING and autoName ~= nil then
child.Name = autoName
end
elseif isState(child) then
-- case 2; state object
local value = peek(child)
-- allow nil to represent the absence of a child
if value ~= nil then
processChild(value, autoName)
end
local disconnect = oldDisconnects[child]
if disconnect == nil then
-- wasn't previously present
disconnect = Observer(child):onChange(queueUpdate)
else
-- previously here; we want to reuse, so remove from old
-- set so we don't encounter it during unparenting
oldDisconnects[child] = nil
end
newDisconnects[child] = disconnect
elseif childType == "table" then
-- case 3; table of objects
for key, subChild in pairs(child) do
local keyType = typeof(key)
local subAutoName: string? = nil
if keyType == "string" then
subAutoName = key
elseif keyType == "number" and autoName ~= nil then
subAutoName = autoName .. "_" .. key
end
processChild(subChild, subAutoName)
end
else
logWarn("unrecognisedChildType", childType)
end
end
if propValue ~= nil then
-- `propValue` is set to nil on cleanup, so we don't process children
-- in that case
processChild(propValue)
end
-- unparent any children that are no longer present
for oldInstance in pairs(oldParented) do
oldInstance.Parent = nil
end
-- disconnect observers which weren't reused
for _, disconnect in pairs(oldDisconnects) do
disconnect()
end
end
queueUpdate = function()
if not updateQueued then
updateQueued = true
External.doTaskDeferred(updateChildren)
end
end
table.insert(cleanupTasks, function()
propValue = nil
updateQueued = true
updateChildren()
end)
-- perform initial child parenting
updateQueued = true
updateChildren()
end
return Children :: PubTypes.SpecialKey

View File

@ -0,0 +1,23 @@
--!strict
--[[
A special key for property tables, which adds user-specified tasks to be run
when the instance is destroyed.
]]
local PubTypes = require "../PubTypes"
local Cleanup = {}
Cleanup.type = "SpecialKey"
Cleanup.kind = "Cleanup"
Cleanup.stage = "observer"
function Cleanup:apply(
userTask: any,
_: Instance,
cleanupTasks: { PubTypes.Task }
)
table.insert(cleanupTasks, userTask)
end
return Cleanup

View File

@ -0,0 +1,18 @@
--!strict
--[[
Processes and returns an existing instance, with options for setting
properties, event handlers and other attributes on the instance.
]]
local PubTypes = require "../PubTypes"
local applyInstanceProps = require "../Instances/applyInstanceProps"
local function Hydrate(target: Instance)
return function(props: PubTypes.PropertyTable): Instance
applyInstanceProps(props, target)
return target
end
end
return Hydrate

View File

@ -0,0 +1,34 @@
--!strict
--[[
Constructs and returns a new instance, with options for setting properties,
event handlers and other attributes on the instance right away.
]]
local PubTypes = require "../PubTypes"
local defaultProps = require "../Instances/defaultProps"
local applyInstanceProps = require "../Instances/applyInstanceProps"
local logError = require "../Logging/logError"
local function New(className: string)
return function(props: PubTypes.PropertyTable): Instance
local ok, instance = pcall(Instance.new, className)
if not ok then
logError("cannotCreateClass", nil, className)
end
local classDefaults = defaultProps[className]
if classDefaults ~= nil then
for defaultProp, defaultValue in pairs(classDefaults) do
instance[defaultProp] = defaultValue
end
end
applyInstanceProps(props, instance)
return instance
end
end
return New

View File

@ -0,0 +1,53 @@
--!strict
--[[
Constructs special keys for property tables which connect property change
listeners to an instance.
]]
local PubTypes = require "../PubTypes"
local logError = require "../Logging/logError"
local typeof = require "../Polyfill/typeof"
local function OnChange(propertyName: string): PubTypes.SpecialKey
local changeKey = {}
changeKey.type = "SpecialKey"
changeKey.kind = "OnChange"
changeKey.stage = "observer"
function changeKey:apply(
callback: any,
applyTo: Instance,
cleanupTasks: { PubTypes.Task }
)
-- local ok, event =
-- pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName)
local ok, event = pcall(function()
return applyTo.Changed
end)
if not ok then
logError(
"cannotConnectChange",
nil,
applyTo.ClassName,
propertyName
)
elseif typeof(callback) ~= "function" then
logError("invalidChangeHandler", nil, propertyName)
else
table.insert(
cleanupTasks,
event:connect(function(prop)
if prop == propertyName then
callback((applyTo :: any)[propertyName])
end
end)
)
end
end
return changeKey
end
return OnChange

View File

@ -0,0 +1,40 @@
--!strict
--[[
Constructs special keys for property tables which connect event listeners to
an instance.
]]
local PubTypes = require "../PubTypes"
local logError = require "../Logging/logError"
local typeof = require "../Polyfill/typeof"
local function getProperty_unsafe(instance: Instance, property: string)
return (instance :: any)[property]
end
local function OnEvent(eventName: string): PubTypes.SpecialKey
local eventKey = {}
eventKey.type = "SpecialKey"
eventKey.kind = "OnEvent"
eventKey.stage = "observer"
function eventKey:apply(
callback: any,
applyTo: Instance,
cleanupTasks: { PubTypes.Task }
)
local ok, event = pcall(getProperty_unsafe, applyTo, eventName)
if not ok or typeof(event) ~= "RBXScriptSignal" then
logError("cannotConnectEvent", nil, applyTo.ClassName, eventName)
elseif typeof(callback) ~= "function" then
logError("invalidEventHandler", nil, eventName)
else
table.insert(cleanupTasks, event:connect(callback))
end
end
return eventKey
end
return OnEvent

View File

@ -0,0 +1,52 @@
--!strict
--[[
A special key for property tables, which allows users to extract values from
an instance into an automatically-updated Value object.
]]
local PubTypes = require "../PubTypes"
local logError = require "../Logging/logError"
local xtypeof = require "../Utility/xtypeof"
local function Out(propertyName: string): PubTypes.SpecialKey
local outKey = {}
outKey.type = "SpecialKey"
outKey.kind = "Out"
outKey.stage = "observer"
function outKey:apply(
outState: any,
applyTo: Instance,
cleanupTasks: { PubTypes.Task }
)
-- local ok, event =
-- pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName)
local ok, event = pcall(function()
return applyTo.Changed
end)
if not ok then
logError("invalidOutProperty", nil, applyTo.ClassName, propertyName)
elseif xtypeof(outState) ~= "State" or outState.kind ~= "Value" then
logError "invalidOutType"
else
outState:set((applyTo :: any)[propertyName])
table.insert(
cleanupTasks,
event:connect(function(prop)
if prop == propertyName then
outState:set((applyTo :: any)[propertyName])
end
end)
)
table.insert(cleanupTasks, function()
outState:set(nil)
end)
end
end
return outKey
end
return Out

View File

@ -0,0 +1,32 @@
--!strict
--[[
A special key for property tables, which stores a reference to the instance
in a user-provided Value object.
]]
local PubTypes = require "../PubTypes"
local logError = require "../Logging/logError"
local xtypeof = require "../Utility/xtypeof"
local Ref = {}
Ref.type = "SpecialKey"
Ref.kind = "Ref"
Ref.stage = "observer"
function Ref:apply(
refState: any,
applyTo: Instance,
cleanupTasks: { PubTypes.Task }
)
if xtypeof(refState) ~= "State" or refState.kind ~= "Value" then
logError "invalidRefType"
else
refState:set(applyTo)
table.insert(cleanupTasks, function()
refState:set(nil)
end)
end
end
return Ref

View File

@ -0,0 +1,160 @@
--!strict
--[[
Applies a table of properties to an instance, including binding to any
given state objects and applying any special keys.
No strong reference is kept by default - special keys should take care not
to accidentally hold strong references to instances forever.
If a key is used twice, an error will be thrown. This is done to avoid
double assignments or double bindings. However, some special keys may want
to enable such assignments - in which case unique keys should be used for
each occurence.
]]
local PubTypes = require "../PubTypes"
local External = require "../External"
local cleanup = require "../Utility/cleanup"
local xtypeof = require "../Utility/xtypeof"
local logError = require "../Logging/logError"
local Observer = require "../State/Observer"
local peek = require "../State/peek"
local typeof = require "../Polyfill/typeof"
local function setProperty_unsafe(
instance: Instance,
property: string,
value: any
)
(instance :: any)[property] = value
end
local function testPropertyAssignable(instance: Instance, property: string)
(instance :: any)[property] = (instance :: any)[property]
end
local function setProperty(instance: Instance, property: string, value: any)
if not pcall(setProperty_unsafe, instance, property, value) then
if not pcall(testPropertyAssignable, instance, property) then
if instance == nil then
-- reference has been lost
logError("setPropertyNilRef", nil, property, tostring(value))
else
-- property is not assignable
logError(
"cannotAssignProperty",
nil,
instance.ClassName,
property
)
end
else
-- property is assignable, but this specific assignment failed
-- this typically implies the wrong type was received
local givenType = typeof(value)
local expectedType = typeof((instance :: any)[property])
logError(
"invalidPropertyType",
nil,
instance.ClassName,
property,
expectedType,
givenType
)
end
end
end
local function bindProperty(
instance: Instance,
property: string,
value: PubTypes.CanBeState<any>,
cleanupTasks: { PubTypes.Task }
)
if xtypeof(value) == "State" then
-- value is a state object - assign and observe for changes
local willUpdate = false
local function updateLater()
if not willUpdate then
willUpdate = true
External.doTaskDeferred(function()
willUpdate = false
setProperty(instance, property, peek(value))
end)
end
end
setProperty(instance, property, peek(value))
table.insert(cleanupTasks, Observer(value :: any):onChange(updateLater))
else
-- value is a constant - assign once only
setProperty(instance, property, value)
end
end
local function applyInstanceProps(
props: PubTypes.PropertyTable,
applyTo: Instance
)
local specialKeys = {
self = {} :: { [PubTypes.SpecialKey]: any },
descendants = {} :: { [PubTypes.SpecialKey]: any },
ancestor = {} :: { [PubTypes.SpecialKey]: any },
observer = {} :: { [PubTypes.SpecialKey]: any },
}
local cleanupTasks = {}
for key, value in pairs(props) do
local keyType = xtypeof(key)
if keyType == "string" then
if key ~= "Parent" then
bindProperty(applyTo, key :: string, value, cleanupTasks)
end
elseif keyType == "SpecialKey" then
local stage = (key :: PubTypes.SpecialKey).stage
local keys = specialKeys[stage]
if keys == nil then
logError("unrecognisedPropertyStage", nil, stage)
else
keys[key] = value
end
else
-- we don't recognise what this key is supposed to be
logError("unrecognisedPropertyKey", nil, xtypeof(key))
end
end
for key, value in pairs(specialKeys.self) do
key:apply(value, applyTo, cleanupTasks)
end
for key, value in pairs(specialKeys.descendants) do
key:apply(value, applyTo, cleanupTasks)
end
if props.Parent ~= nil then
bindProperty(applyTo, "Parent", props.Parent, cleanupTasks)
end
for key, value in pairs(specialKeys.ancestor) do
key:apply(value, applyTo, cleanupTasks)
end
for key, value in pairs(specialKeys.observer) do
key:apply(value, applyTo, cleanupTasks)
end
-- applyTo.Destroying:connect(function()
-- cleanup(cleanupTasks)
-- end)
if applyTo.Parent then -- close enough?
game.DescendantRemoving:connect(function(descendant)
if descendant == applyTo then
cleanup(cleanupTasks)
end
end)
end
end
return applyInstanceProps

View File

@ -0,0 +1,117 @@
--!strict
--[[
Stores 'sensible default' properties to be applied to instances created by
the New function.
]]
return {
BillboardGui = {
Active = true,
},
Frame = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
},
TextLabel = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0),
FontSize = Enum.FontSize.Size14,
},
TextButton = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
AutoButtonColor = false,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0),
FontSize = Enum.FontSize.Size14,
},
TextBox = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
ClearTextOnFocus = false,
Font = Enum.Font.SourceSans,
Text = "",
TextColor3 = Color3.new(0, 0, 0),
FontSize = Enum.FontSize.Size14,
},
ImageLabel = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
},
ImageButton = {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
AutoButtonColor = false,
},
SpawnLocation = {
Duration = 0,
},
Part = {
Anchored = true,
Size = Vector3.new(1, 1, 1),
FrontSurface = Enum.SurfaceType.Smooth,
BackSurface = Enum.SurfaceType.Smooth,
LeftSurface = Enum.SurfaceType.Smooth,
RightSurface = Enum.SurfaceType.Smooth,
TopSurface = Enum.SurfaceType.Smooth,
BottomSurface = Enum.SurfaceType.Smooth,
},
TrussPart = {
Anchored = true,
Size = Vector3.new(2, 2, 2),
FrontSurface = Enum.SurfaceType.Smooth,
BackSurface = Enum.SurfaceType.Smooth,
LeftSurface = Enum.SurfaceType.Smooth,
RightSurface = Enum.SurfaceType.Smooth,
TopSurface = Enum.SurfaceType.Smooth,
BottomSurface = Enum.SurfaceType.Smooth,
},
CornerWedgePart = {
Anchored = true,
Size = Vector3.new(1, 1, 1),
FrontSurface = Enum.SurfaceType.Smooth,
BackSurface = Enum.SurfaceType.Smooth,
LeftSurface = Enum.SurfaceType.Smooth,
RightSurface = Enum.SurfaceType.Smooth,
TopSurface = Enum.SurfaceType.Smooth,
BottomSurface = Enum.SurfaceType.Smooth,
},
VehicleSeat = {
Anchored = true,
Size = Vector3.new(1, 1, 1),
FrontSurface = Enum.SurfaceType.Smooth,
BackSurface = Enum.SurfaceType.Smooth,
LeftSurface = Enum.SurfaceType.Smooth,
RightSurface = Enum.SurfaceType.Smooth,
TopSurface = Enum.SurfaceType.Smooth,
BottomSurface = Enum.SurfaceType.Smooth,
},
}

View File

@ -0,0 +1,43 @@
--!strict
--[[
Utility function to log a Fusion-specific error.
]]
local Types = require "../Types"
local messages = require "../Logging/messages"
local function logError(messageID: string, errObj: Types.Error?, ...)
local formatString: string
if messages[messageID] ~= nil then
formatString = messages[messageID]
else
messageID = "unknownMessage"
formatString = messages[messageID]
end
local errorString
if errObj == nil then
errorString = string.format(
"[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")",
...
)
else
formatString =
formatString:gsub("ERROR_MESSAGE", tostring(errObj.message))
errorString = string.format(
"[Fusion] "
.. formatString
.. "\n(ID: "
.. messageID
.. ")\n---- Stack trace ----\n"
.. tostring(errObj.trace),
...
)
end
error(errorString:gsub("\n", "\n "), 0)
end
return logError

View File

@ -0,0 +1,43 @@
--!strict
--[[
Utility function to log a Fusion-specific error, without halting execution.
]]
local Types = require "../Types"
local messages = require "../Logging/messages"
local function logErrorNonFatal(messageID: string, errObj: Types.Error?, ...)
local formatString: string
if messages[messageID] ~= nil then
formatString = messages[messageID]
else
messageID = "unknownMessage"
formatString = messages[messageID]
end
local errorString
if errObj == nil then
errorString =
string.format(`[Fusion] {formatString}\n(ID: {messageID})`, ...)
else
formatString =
formatString:gsub("ERROR_MESSAGE", tostring(errObj.message))
errorString = string.format(
"[Fusion] "
.. formatString
.. "\n(ID: "
.. messageID
.. ")\n---- Stack trace ----\n"
.. tostring(errObj.trace),
...
)
end
coroutine.wrap(function()
error(errorString:gsub("\n", "\n "), 0)
end)()
end
return logErrorNonFatal

View File

@ -0,0 +1,22 @@
--!strict
--[[
Utility function to log a Fusion-specific warning.
]]
local messages = require "../Logging/messages"
local function logWarn(messageID, ...)
local formatString: string
if messages[messageID] ~= nil then
formatString = messages[messageID]
else
messageID = "unknownMessage"
formatString = messages[messageID]
end
warn(string.format(`[Fusion] {formatString}\n(ID: {messageID})`, ...))
end
return logWarn

View File

@ -0,0 +1,55 @@
--!strict
--[[
Stores templates for different kinds of logging messages.
]]
return {
-- attributeNameNil = "Attribute name cannot be nil",
cannotAssignProperty = "The class type '%s' has no assignable property '%s'.",
cannotConnectChange = "The %s class doesn't have a property called '%s'.",
-- cannotConnectAttributeChange = "The %s class doesn't have an attribute called '%s'.",
cannotConnectEvent = "The %s class doesn't have an event called '%s'.",
cannotCreateClass = "Can't create a new instance of class '%s'.",
computedCallbackError = "Computed callback error: ERROR_MESSAGE",
contextualCallbackError = "Contextual callback error: ERROR_MESSAGE",
destructorNeededValue = "To save instances into Values, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.",
destructorNeededComputed = "To return instances from Computeds, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.",
multiReturnComputed = "Returning multiple values from Computeds is discouraged, as behaviour will change soon - see discussion #189 on GitHub.",
destructorNeededForKeys = "To return instances from ForKeys, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.",
destructorNeededForValues = "To return instances from ForValues, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.",
destructorNeededForPairs = "To return instances from ForPairs, provide a destructor function. This will be an error soon - see discussion #183 on GitHub.",
forKeysProcessorError = "ForKeys callback error: ERROR_MESSAGE",
forKeysKeyCollision = "ForKeys should only write to output key '%s' once when processing key changes, but it wrote to it twice. Previously input key: '%s'; New input key: '%s'",
forKeysDestructorError = "ForKeys destructor error: ERROR_MESSAGE",
forPairsDestructorError = "ForPairs destructor error: ERROR_MESSAGE",
forPairsKeyCollision = "ForPairs should only write to output key '%s' once when processing key changes, but it wrote to it twice. Previous input pair: '[%s] = %s'; New input pair: '[%s] = %s'",
forPairsProcessorError = "ForPairs callback error: ERROR_MESSAGE",
forValuesProcessorError = "ForValues callback error: ERROR_MESSAGE",
forValuesDestructorError = "ForValues destructor error: ERROR_MESSAGE",
invalidChangeHandler = "The change handler for the '%s' property must be a function.",
-- invalidAttributeChangeHandler = "The change handler for the '%s' attribute must be a function.",
invalidEventHandler = "The handler for the '%s' event must be a function.",
invalidPropertyType = "'%s.%s' expected a '%s' type, but got a '%s' type.",
invalidRefType = "Instance refs must be Value objects.",
invalidOutType = "[Out] properties must be given Value objects.",
-- invalidAttributeOutType = "[AttributeOut] properties must be given Value objects.",
invalidOutProperty = "The %s class doesn't have a property called '%s'.",
-- invalidOutAttributeName = "The %s class doesn't have an attribute called '%s'.",
invalidSpringDamping = "The damping ratio for a spring must be >= 0. (damping was %.2f)",
invalidSpringSpeed = "The speed of a spring must be >= 0. (speed was %.2f)",
mistypedSpringDamping = "The damping ratio for a spring must be a number. (got a %s)",
mistypedSpringSpeed = "The speed of a spring must be a number. (got a %s)",
mistypedTweenInfo = "The tween info of a tween must be a TweenInfo. (got a %s)",
noTaskScheduler = "Fusion is not connected to an external task scheduler.",
springTypeMismatch = "The type '%s' doesn't match the spring's type '%s'.",
stateGetWasRemoved = "`StateObject:get()` has been replaced by `use()` and `peek()` - see discussion #217 on GitHub.",
strictReadError = "'%s' is not a valid member of '%s'.",
unknownMessage = "Unknown error: ERROR_MESSAGE",
unrecognisedChildType = "'%s' type children aren't accepted by `[Children]`.",
unrecognisedPropertyKey = "'%s' keys aren't accepted in property tables.",
unrecognisedPropertyStage = "'%s' isn't a valid stage for a special key to be applied at.",
invalidEasingStyle = "The easing style must be a valid Enum.EasingStyle or a string of 'Linear', 'Quad', 'Cubic', 'Quart', 'Quint', 'Sine', 'Exponential', 'Circular', 'Elastic', 'Back', 'Bounce'. (got %s)",
invalidEasingDirection = "The easing direction must be a valid Enum.EasingDirection or a string of 'In', 'Out', 'InOut', 'OutIn'. (got %s)",
}

View File

@ -0,0 +1,24 @@
--!strict
--[[
An xpcall() error handler to collect and parse useful information about
errors, such as clean messages and stack traces.
]]
local Types = require "../Types"
local function parseError(err: string): Types.Error
local trace
if debug and debug.traceback then
trace = debug.traceback(nil, 2)
end
return {
type = "Error",
raw = err,
message = err:gsub("^.+:%d+:%s*", ""),
trace = trace or "Traceback not available",
}
end
return parseError

View File

@ -0,0 +1,75 @@
--!strict
--[[
Roblox implementation for Fusion's abstract scheduler layer.
]]
local RunService = game:GetService "RunService"
local External = require "./External"
local MercuryExternal = {}
--[[
Sends an immediate task to the external scheduler. Throws if none is set.
]]
function MercuryExternal.doTaskImmediate(resume: () -> ())
Spawn(resume)
end
--[[
Sends a deferred task to the external scheduler. Throws if none is set.
]]
function MercuryExternal.doTaskDeferred(resume: () -> ())
coroutine.resume(coroutine.create(resume))
end
--[[
Sends an update step to Fusion using the Roblox clock time.
]]
local function performUpdateStep()
External.performUpdateStep(time())
end
--[[
Binds Fusion's update step to RunService step events.
]]
local stopSchedulerFunc: () -> ()? = nil
function MercuryExternal.startScheduler()
if stopSchedulerFunc ~= nil then
return
end
-- if RunService:IsClient() then
-- In cases where multiple Fusion modules are running simultaneously,
-- -- this prevents collisions.
-- local id = "FusionUpdateStep_" .. HttpService:GenerateGUID()
-- RunService:BindToRenderStep(
-- id,
-- Enum.RenderPriority.First.Value,
-- performUpdateStep
-- )
-- stopSchedulerFunc = function()
-- RunService:UnbindFromRenderStep(id)
-- end
local conn = RunService.RenderStepped:connect(performUpdateStep)
stopSchedulerFunc = function()
conn:disconnect()
end
-- else
-- local connection = RunService.Heartbeat:connect(performUpdateStep)
-- stopSchedulerFunc = function()
-- connection:Disconnect()
-- end
-- end
end
--[[
Unbinds Fusion's update step from RunService step events.
]]
function MercuryExternal.stopScheduler()
if stopSchedulerFunc ~= nil then
stopSchedulerFunc()
stopSchedulerFunc = nil
end
end
return MercuryExternal

View File

@ -0,0 +1,112 @@
-- A basic polyfill for the TweenInfo.new function,
-- allows using Enum.EasingStyle/Direction or strings instead
local logError = require "../Logging/logError"
local TweenInfo = {}
function TweenInfo.new(
time: number?,
easingStyle: Enum.EasingStyle | string?,
easingDirection: Enum.EasingDirection | string?,
repeatCount: number?,
reverses: boolean?,
delayTime: number?
)
local proxy = newproxy(true)
local mt = getmetatable(proxy)
-- if easingStyle or easingDirection is an enum,
-- convert it to a string
if type(easingStyle) ~= "string" then
if easingStyle then
easingStyle = tostring(easingStyle):gsub("Enum.%w+.", "")
end
else
local ok
for _, s in ipairs {
"Linear",
"Quad",
"Cubic",
"Quart",
"Quint",
"Sine",
"Exponential",
"Circular",
"Elastic",
"Back",
"Bounce",
} do
if easingStyle == s then
ok = true
break
end
end
if not ok then
logError("invalidEasingStyle", nil, easingStyle)
end
end
if type(easingDirection) ~= "string" then
if easingDirection then
easingDirection = tostring(easingDirection):gsub("Enum.%w+.", "")
end
else
local ok
for _, d in ipairs {
"In",
"Out",
"InOut",
"OutIn",
} do
if easingDirection == d then
ok = true
break
end
end
if not ok then
logError("invalidEasingDirection", nil, easingDirection)
end
end
time = time or 1
easingStyle = easingStyle or "Quad"
easingDirection = easingDirection or "Out"
repeatCount = repeatCount or 0
reverses = reverses or false
delayTime = delayTime or 0
mt.__index = {
Time = time,
EasingStyle = easingStyle,
EasingDirection = easingDirection,
RepeatCount = repeatCount,
Reverses = reverses,
DelayTime = delayTime,
}
-- When attempting to assign to properties, throw an error
mt.__newindex = function(_, prop)
error(prop .. " cannot be assigned to", math.huge) -- lmfao
end
mt.__tostring = function()
return "Time:"
.. tostring(time)
.. " DelayTime:"
.. tostring(delayTime)
.. " RepeatCount:"
.. tostring(repeatCount)
.. " Reverses:"
.. (reverses and "True" or "False")
.. " EasingDirection:"
.. easingDirection
.. " EasingStyle:"
.. easingStyle
end
mt.__metatable = "The metatable is locked"
return proxy
end
return TweenInfo

View File

@ -0,0 +1,347 @@
--
-- Adapted from
-- Tweener's easing functions (Penner's Easing Equations)
-- and http://code.google.com/p/tweener/ (jstweener javascript version)
--
-- For all easing functions:
-- t = elapsed time
-- b = begin
-- c = change == ending - beginning
local pow = math.pow
local sin = math.sin
local cos = math.cos
local pi = math.pi
local sqrt = math.sqrt
local abs = math.abs
local asin = math.asin
local easing = {
Linear = {},
Quad = {},
Cubic = {},
Quart = {},
Quint = {},
Sine = {},
Exponential = {},
Circular = {},
Elastic = {},
Back = {},
Bounce = {},
}
local linear = function(t, b, c)
return c * t + b
end
easing.Linear.In = linear
easing.Linear.Out = linear
easing.Linear.InOut = linear
easing.Linear.OutIn = linear
easing.Quad.In = function(t, b, c)
return c * pow(t, 2) + b
end
easing.Quad.Out = function(t, b, c)
return -c * t * (t - 2) + b
end
easing.Quad.InOut = function(t, b, c)
t *= 2
if t < 1 then
return c / 2 * pow(t, 2) + b
end
return -c / 2 * ((t - 1) * (t - 3) - 1) + b
end
easing.Quad.OutIn = function(t, b, c)
if t < 0.5 then
return easing.Quad.Out(t * 2, b, c / 2)
end
return easing.Quad.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Cubic.In = function(t, b, c)
return c * pow(t, 3) + b
end
easing.Cubic.Out = function(t, b, c)
t -= 1
return c * (pow(t, 3) + 1) + b
end
easing.Cubic.InOut = function(t, b, c)
t *= 2
if t < 1 then
return c / 2 * t * t * t + b
end
t -= 2
return c / 2 * (t * t * t + 2) + b
end
easing.Cubic.OutIn = function(t, b, c)
if t < 0.5 then
return easing.Cubic.Out(t * 2, b, c / 2)
end
return easing.Cubic.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Quart.In = function(t, b, c)
return c * pow(t, 4) + b
end
easing.Quart.Out = function(t, b, c)
t -= 1
return -c * (pow(t, 4) - 1) + b
end
easing.Quart.InOut = function(t, b, c)
t *= 2
if t < 1 then
return c / 2 * pow(t, 4) + b
end
t -= 2
return -c / 2 * (pow(t, 4) - 2) + b
end
easing.Quart.OutIn = function(t, b, c)
if t < 0.5 then
return easing.Quart.Out(t * 2, b, c / 2)
end
return easing.Quart.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Quint.In = function(t, b, c)
return c * pow(t, 5) + b
end
easing.Quint.Out = function(t, b, c)
t -= 1
return c * (pow(t, 5) + 1) + b
end
easing.Quint.InOut = function(t, b, c)
t *= 2
if t < 1 then
return c / 2 * pow(t, 5) + b
end
t -= 2
return c / 2 * (pow(t, 5) + 2) + b
end
easing.Quint.OutIn = function(t, b, c)
if t < 0.5 then
return easing.Quint.Out(t * 2, b, c / 2)
end
return easing.Quint.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Sine.In = function(t, b, c)
return -c * cos(t * (pi / 2)) + c + b
end
easing.Sine.Out = function(t, b, c)
return c * sin(t * (pi / 2)) + b
end
easing.Sine.InOut = function(t, b, c)
return -c / 2 * (cos(pi * t) - 1) + b
end
easing.Sine.OutIn = function(t, b, c)
if t < 0.5 then
return easing.Sine.Out(t * 2, b, c / 2)
end
return easing.Sine.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Exponential.In = function(t, b, c)
if t == 0 then
return b
end
return c * pow(2, 10 * (t - 1)) + b - c * 0.001
end
easing.Exponential.Out = function(t, b, c)
if t == 1 then
return b + c
end
return c * 1.001 * (-pow(2, -10 * t) + 1) + b
end
easing.Exponential.InOut = function(t, b, c)
if t == 0 then
return b
elseif t == 1 then
return b + c
end
t *= 2
if t < 1 then
return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005
end
t -= 1
return c / 2 * 1.0005 * (-pow(2, -10 * t) + 2) + b
end
easing.Exponential.OutIn = function(t, b, c)
if t < 0.5 then
return t.Exponential.Out(t * 2, b, c / 2)
end
return t.Exponential.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Circular.In = function(t, b, c)
return (-c * (sqrt(1 - pow(t, 2)) - 1) + b)
end
easing.Circular.Out = function(t, b, c)
t -= 1
return (c * sqrt(1 - pow(t, 2)) + b)
end
easing.Circular.InOut = function(t, b, c)
t *= 2
if t < 1 then
return -c / 2 * (sqrt(1 - t * t) - 1) + b
end
t -= 2
return c / 2 * (sqrt(1 - t * t) + 1) + b
end
easing.Circular.OutIn = function(t, b, c)
if t < 0.5 then
return easing.Circular.Out(t * 2, b, c / 2)
end
return easing.Circular.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Elastic.In = function(t, b, c) --, a, p)
if t == 0 then
return b
elseif t == 1 then
return b + c
end
local p = 0.3
local s
s = p / 4
t -= 1
return -(c * pow(2, 10 * t) * sin((t * 1 - s) * (2 * pi) / p)) + b
end
easing.Elastic.Out = function(t, b, c) --, a, p)
if t == 0 then
return b
elseif t == 1 then
return b + c
end
local p = 0.3
local s
s = p / 4
return c * pow(2, -10 * t) * sin((t - s) * (2 * pi) / p) + c + b
end
easing.Elastic.InOut = function(t, b, c) --, a, p)
if t == 0 then
return b
end
t *= 2
if t == 2 then
return b + c
end
local p = 0.45
local a = 0
local s
if not a or a < abs(c) then
a = c
s = p / 4
else
s = p / (2 * pi) * asin(c / a)
end
t -= 1
if t < 1 then
return -0.5 * (a * pow(2, 10 * t) * sin((t - s) * (2 * pi) / p)) + b
end
return a * pow(2, -10 * t) * sin((t - s) * (2 * pi) / p) * 0.5 + c + b
end
easing.Elastic.OutIn = function(t, b, c) --, a, p)
if t < 0.5 then
return easing.Elastic.Out(t * 2, b, c / 2)
end
return easing.Elastic.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Back.In = function(t, b, c) --, s)
local s = 1.70158
return c * t * t * ((s + 1) * t - s) + b
end
easing.Back.Out = function(t, b, c) --, s)
local s = 1.70158
t -= 1
return c * (t * t * ((s + 1) * t + s) + 1) + b
end
easing.Back.InOut = function(t, b, c) --, s)
local s = 2.5949095
t *= 2
if t < 1 then
return c / 2 * (t * t * ((s + 1) * t - s)) + b
end
t -= 2
return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
end
easing.Back.OutIn = function(t, b, c) --, s)
if t < 0.5 then
return easing.Back.Out(t * 2, b, c / 2)
end
return easing.Back.In((t * 2) - 1, b + c / 2, c / 2)
end
easing.Bounce.Out = function(t, b, c)
if t < 1 / 2.75 then
return c * (7.5625 * t * t) + b
elseif t < 2 / 2.75 then
t -= 1.5 / 2.75
return c * (7.5625 * t * t + 0.75) + b
elseif t < 2.5 / 2.75 then
t -= 2.25 / 2.75
return c * (7.5625 * t * t + 0.9375) + b
end
t -= 2.625 / 2.75
return c * (7.5625 * t * t + 0.984375) + b
end
easing.Bounce.In = function(t, b, c)
return c - easing.Bounce.Out(1 - t, 0, c) + b
end
easing.Bounce.InOut = function(t, b, c)
if t < 0.5 then
return easing.Bounce.In(t * 2, 0, c) * 0.5 + b
end
return easing.Bounce.Out(t * 2 - 1, 0, c) * 0.5 + c * 0.5 + b
end
easing.Bounce.OutIn = function(t, b, c)
if t < 0.5 then
return easing.Bounce.Out(t * 2, b, c / 2)
end
return easing.Bounce.In((t * 2) - 1, b + c / 2, c / 2)
end
return easing

View File

@ -0,0 +1,181 @@
-- A basic polyfill for the typeof function
return function(value)
local basicType = type(value)
if
basicType == "nil"
or basicType == "boolean"
or basicType == "number"
or basicType == "string"
or basicType == "function"
or basicType == "thread"
or basicType == "table"
then
return basicType
end
-- Will short-circuit
--[[
{
name of type to check,
{ list of required properties },
}
]]
local tests = {
{
"Instance",
{ "ClassName" },
},
{
"EnumItem",
{ "EnumType", "Name", "Value" },
},
{
"Enum",
{ "GetEnumItems" },
},
{
"Enums",
{ "MembershipType" }, -- lmao
},
{
"RBXScriptSignal",
{
"connect",
-- "connected",
-- "connectFirst",
-- "connectLast",
"wait",
},
},
{
"RBXScriptConnection",
{
"connected",
"disconnect",
},
},
{
"TweenInfo",
{
"EasingDirection",
-- "Time",
-- "DelayTime",
"RepeatCount",
"EasingStyle",
-- "Reverses",
},
},
{
"CFrame",
{
"p",
"x",
"y",
"z",
"lookVector",
},
},
{
"Vector3",
{
"Lerp",
-- "Cross",
-- "Dot",
"unit",
"magnitude",
"x",
"y",
"z",
},
},
{
"Vector3int16",
{ "z", "x", "y" },
},
{
"Vector2",
{ "unit", "magnitude", "x", "y" },
},
{
"Vector2int16",
{ "x", "y" },
},
{
"Region3",
{ "CFrame", "Size" },
},
{
"Region3int16",
{ "Min", "Max" },
},
{
"Ray",
{
"Origin",
"Direction",
"Unit",
"ClosestPoint",
"Distance",
},
},
{
"UDim",
{ "Scale", "Offset" },
},
{
"Axes",
{ "Z", "X", "Y" },
},
{
"UDim2",
{ "X", "Y" },
},
{
"BrickColor",
{
"Number",
"Name",
"Color",
"r",
"g",
"b",
},
},
{
"Color3",
{ "r", "g", "b" },
},
{
"Faces",
{
"Right",
"Top",
"Back",
-- "Left",
-- "Bottom",
-- "Front",
},
},
}
for _, v in ipairs(tests) do
local t, test = v[1], v[2]
local ok, result = pcall(function()
for _, prop in ipairs(test) do
if value[prop] == nil then
return false
end
-- Cannot throw if the property does not exist,
-- as userdatas may allow nil indexing
end
return true
end)
if ok and result then
return t
end
end
end

View File

@ -0,0 +1,162 @@
--!strict
--[[
Stores common public-facing type information for Fusion APIs.
]]
type Set<T> = { [T]: any }
--[[
General use types
]]
-- A unique symbolic value.
export type Symbol = {
type: "Symbol",
name: string,
}
-- Types that can be expressed as vectors of numbers, and so can be animated.
export type Animatable =
number
| CFrame
| Color3
| ColorSequenceKeypoint
| DateTime
| NumberRange
| NumberSequenceKeypoint
| PhysicalProperties
| Ray
| Rect
| Region3
| Region3int16
| UDim
| UDim2
| Vector2
| Vector2int16
| Vector3
| Vector3int16
-- A task which can be accepted for cleanup.
export type Task =
Instance
| RBXScriptConnection
| () -> () | { destroy: (any) -> () } | { Destroy: (any) -> () } | { Task }
-- Script-readable version information.
export type Version = {
major: number,
minor: number,
isRelease: boolean,
}
-- An object which stores a value scoped in time.
export type Contextual<T> = {
type: "Contextual",
now: (Contextual<T>) -> T,
is: (Contextual<T>, T) -> ContextualIsMethods,
}
type ContextualIsMethods = {
during: <T, A...>(ContextualIsMethods, (A...) -> T, A...) -> T,
}
--[[
Generic reactive graph types
]]
-- A graph object which can have dependents.
export type Dependency = {
dependentSet: Set<Dependent>,
}
-- A graph object which can have dependencies.
export type Dependent = {
update: (Dependent) -> boolean,
dependencySet: Set<Dependency>,
}
-- An object which stores a piece of reactive state.
export type StateObject<T> = Dependency & {
type: "State",
kind: string,
_typeIdentifier: T,
}
-- Either a constant value of type T, or a state object containing type T.
export type CanBeState<T> = StateObject<T> | T
-- Function signature for use callbacks.
export type Use = <T>(target: CanBeState<T>) -> T
--[[
Specific reactive graph types
]]
-- A state object whose value can be set at any time by the user.
export type Value<T> = StateObject<T> & {
kind: "State",
set: (Value<T>, newValue: any, force: boolean?) -> (),
}
-- A state object whose value is derived from other objects using a callback.
export type Computed<T> = StateObject<T> & Dependent & {
kind: "Computed",
}
-- A state object whose value is derived from other objects using a callback.
export type ForPairs<KO, VO> = StateObject<{ [KO]: VO }> & Dependent & {
kind: "ForPairs",
}
-- A state object whose value is derived from other objects using a callback.
export type ForKeys<KO, V> = StateObject<{ [KO]: V }> & Dependent & {
kind: "ForKeys",
}
-- A state object whose value is derived from other objects using a callback.
export type ForValues<K, VO> = StateObject<{ [K]: VO }> & Dependent & {
kind: "ForKeys",
}
-- A state object which follows another state object using tweens.
export type Tween<T> = StateObject<T> & Dependent & {
kind: "Tween",
}
-- A state object which follows another state object using spring simulation.
export type Spring<T> = StateObject<T> & Dependent & {
kind: "Spring",
setPosition: (Spring<T>, newPosition: Animatable) -> (),
setVelocity: (Spring<T>, newVelocity: Animatable) -> (),
addVelocity: (Spring<T>, deltaVelocity: Animatable) -> (),
}
-- An object which can listen for updates on another state object.
export type Observer = Dependent & {
kind: "Observer",
onChange: (Observer, callback: () -> ()) -> (() -> ()),
}
--[[
Instance related types
]]
-- Denotes children instances in an instance or component's property table.
export type SpecialKey = {
type: "SpecialKey",
kind: string,
stage: "self" | "descendants" | "ancestor" | "observer",
apply: (
SpecialKey,
value: any,
applyTo: Instance,
cleanupTasks: { Task }
) -> (),
}
-- A collection of instances that may be parented to another instance.
export type Children = Instance | StateObject<Children> | { [any]: Children }
-- A table that defines an instance's properties, handlers and children.
export type PropertyTable = { [string | SpecialKey]: any }
return nil

View File

@ -0,0 +1,125 @@
--!nonstrict
--[[
Constructs and returns objects which can be used to model derived reactive
state.
]]
local Types = require "../Types"
-- Logging
local logError = require "../Logging/logError"
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
local logWarn = require "../Logging/logWarn"
local parseError = require "../Logging/parseError"
-- Utility
local isSimilar = require "../Utility/isSimilar"
local needsDestruction = require "../Utility/needsDestruction"
-- State
local makeUseCallback = require "../State/makeUseCallback"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Recalculates this Computed's cached value and dependencies.
Returns true if it changed, or false if it's identical.
]]
function class:update(): boolean
-- remove this object from its dependencies' dependent sets
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = nil
end
-- we need to create a new, empty dependency set to capture dependencies
-- into, but in case there's an error, we want to restore our old set of
-- dependencies. by using this table-swapping solution, we can avoid the
-- overhead of allocating new tables each update.
self._oldDependencySet, self.dependencySet =
self.dependencySet, self._oldDependencySet
-- table.clear(self.dependencySet)
for i, _ in pairs(self.dependencySet) do
self.dependencySet[i] = nil
end
local use = makeUseCallback(self.dependencySet)
-- local ok, newValue, newMetaValue =
-- xpcall(self._processor, parseError, use)
local ok, newValue, newMetaValue = pcall(self._processor, use)
if ok then
if self._destructor == nil and needsDestruction(newValue) then
logWarn "destructorNeededComputed"
end
if newMetaValue ~= nil then
logWarn "multiReturnComputed"
end
local oldValue = self._value
local similar = isSimilar(oldValue, newValue)
if self._destructor ~= nil then
self._destructor(oldValue)
end
self._value = newValue
-- add this object to the dependencies' dependent sets
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = true
end
return not similar
else
-- this needs to be non-fatal, because otherwise it'd disrupt the
-- update process
logErrorNonFatal("computedCallbackError", parseError(newValue))
-- restore old dependencies, because the new dependencies may be corrupt
self._oldDependencySet, self.dependencySet =
self.dependencySet, self._oldDependencySet
-- restore this object in the dependencies' dependent sets
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = true
end
return false
end
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._value
end
function class:get()
logError "stateGetWasRemoved"
end
local function Computed<T>(
processor: () -> T,
destructor: ((T) -> ())?
): Types.Computed<T>
local dependencySet = {}
local self = setmetatable({
type = "State",
kind = "Computed",
dependencySet = dependencySet,
-- if we held strong references to the dependents, then they wouldn't be
-- able to get garbage collected when they fall out of scope
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
_oldDependencySet = {},
_processor = processor,
_destructor = destructor,
_value = nil,
}, CLASS_METATABLE)
self:update()
return self
end
return Computed

View File

@ -0,0 +1,274 @@
--!nonstrict
--[[
Constructs a new ForKeys state object which maps keys of an array using
a `processor` function.
Optionally, a `destructor` function can be specified for cleaning up
calculated keys. If omitted, the default cleanup function will be used instead.
Optionally, a `meta` value can be returned in the processor function as the
second value to pass data from the processor to the destructor.
]]
local PubTypes = require "../PubTypes"
local Types = require "../Types"
-- Logging
local parseError = require "../Logging/parseError"
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
local logError = require "../Logging/logError"
local logWarn = require "../Logging/logWarn"
-- Utility
local cleanup = require "../Utility/cleanup"
local needsDestruction = require "../Utility/needsDestruction"
-- State
local peek = require "../State/peek"
local makeUseCallback = require "../State/makeUseCallback"
local isState = require "../State/isState"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Called when the original table is changed.
This will firstly find any keys meeting any of the following criteria:
- they were not previously present
- a dependency used during generation of this value has changed
It will recalculate those key pairs, storing information about any
dependencies used in the processor callback during output key generation,
and save the new key to the output array with the same value. If it is
overwriting an older value, that older value will be passed to the
destructor for cleanup.
Finally, this function will find keys that are no longer present, and remove
their output keys from the output table and pass them to the destructor.
]]
function class:update(): boolean
local inputIsState = self._inputIsState
local newInputTable = peek(self._inputTable)
local oldInputTable = self._oldInputTable
local outputTable = self._outputTable
local keyOIMap = self._keyOIMap
local keyIOMap = self._keyIOMap
local meta = self._meta
local didChange = false
-- clean out main dependency set
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = nil
end
self._oldDependencySet, self.dependencySet =
self.dependencySet, self._oldDependencySet
-- table.clear(self.dependencySet)
for i, _ in pairs(self.dependencySet) do
self.dependencySet[i] = nil
end
-- if the input table is a state object, add it as a dependency
if inputIsState then
self._inputTable.dependentSet[self] = true
self.dependencySet[self._inputTable] = true
end
-- STEP 1: find keys that changed or were not previously present
for newInKey, value in pairs(newInputTable) do
-- get or create key data
local keyData = self._keyData[newInKey]
if keyData == nil then
keyData = {
dependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE),
}
self._keyData[newInKey] = keyData
end
-- check if the key is new
local shouldRecalculate = oldInputTable[newInKey] == nil
-- check if the key's dependencies have changed
if shouldRecalculate == false then
for dependency, oldValue in pairs(keyData.dependencyValues) do
if oldValue ~= peek(dependency) then
shouldRecalculate = true
break
end
end
end
-- recalculate the output key if necessary
if shouldRecalculate then
keyData.oldDependencySet, keyData.dependencySet =
keyData.dependencySet, keyData.oldDependencySet
-- table.clear(keyData.dependencySet)
for i, _ in pairs(keyData.dependencySet) do
keyData.dependencySet[i] = nil
end
local use = makeUseCallback(keyData.dependencySet)
-- local processOK, newOutKey, newMetaValue =
-- xpcall(self._processor, parseError, use, newInKey)
local processOK, newOutKey, newMetaValue =
pcall(self._processor, use, newInKey)
if processOK then
if
self._destructor == nil
and (
needsDestruction(newOutKey)
or needsDestruction(newMetaValue)
)
then
logWarn "destructorNeededForKeys"
end
local oldInKey = keyOIMap[newOutKey]
local oldOutKey = keyIOMap[newInKey]
-- check for key collision
if oldInKey ~= newInKey and newInputTable[oldInKey] ~= nil then
logError(
"forKeysKeyCollision",
nil,
tostring(newOutKey),
tostring(oldInKey),
tostring(newOutKey)
)
end
-- check for a changed output key
if
oldOutKey ~= newOutKey
and keyOIMap[oldOutKey] == newInKey
then
-- clean up the old calculated value
local oldMetaValue = meta[oldOutKey]
local destructOK, err = pcall(
self._destructor or cleanup,
oldOutKey,
oldMetaValue
)
if not destructOK then
logErrorNonFatal(
"forKeysDestructorError",
parseError(err)
)
end
keyOIMap[oldOutKey] = nil
outputTable[oldOutKey] = nil
meta[oldOutKey] = nil
end
-- update the stored data for this key
oldInputTable[newInKey] = value
meta[newOutKey] = newMetaValue
keyOIMap[newOutKey] = newInKey
keyIOMap[newInKey] = newOutKey
outputTable[newOutKey] = value
-- if we had to recalculate the output, then we did change
didChange = true
else
-- restore old dependencies, because the new dependencies may be corrupt
keyData.oldDependencySet, keyData.dependencySet =
keyData.dependencySet, keyData.oldDependencySet
logErrorNonFatal("forKeysProcessorError", parseError(newOutKey))
end
end
-- save dependency values and add to main dependency set
for dependency in pairs(keyData.dependencySet) do
keyData.dependencyValues[dependency] = peek(dependency)
self.dependencySet[dependency] = true
dependency.dependentSet[self] = true
end
end
-- STEP 2: find keys that were removed
for outputKey, inputKey in pairs(keyOIMap) do
if newInputTable[inputKey] == nil then
-- clean up the old calculated value
local oldMetaValue = meta[outputKey]
local destructOK, err =
pcall(self._destructor or cleanup, outputKey, oldMetaValue)
if not destructOK then
logErrorNonFatal("forKeysDestructorError", parseError(err))
end
-- remove data
oldInputTable[inputKey] = nil
meta[outputKey] = nil
keyOIMap[outputKey] = nil
keyIOMap[inputKey] = nil
outputTable[outputKey] = nil
self._keyData[inputKey] = nil
-- if we removed a key, then the table/state changed
didChange = true
end
end
return didChange
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._outputTable
end
function class:get()
logError "stateGetWasRemoved"
end
local function ForKeys<KI, KO, M>(
inputTable: PubTypes.CanBeState<{ [KI]: any }>,
processor: (KI) -> (KO, M?),
destructor: (KO, M?) -> ()?
): Types.ForKeys<KI, KO, M>
local inputIsState = isState(inputTable)
local self = setmetatable({
type = "State",
kind = "ForKeys",
dependencySet = {},
-- if we held strong references to the dependents, then they wouldn't be
-- able to get garbage collected when they fall out of scope
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
_oldDependencySet = {},
_processor = processor,
_destructor = destructor,
_inputIsState = inputIsState,
_inputTable = inputTable,
_oldInputTable = {},
_outputTable = {},
_keyOIMap = {},
_keyIOMap = {},
_keyData = {},
_meta = {},
}, CLASS_METATABLE)
self:update()
return self
end
return ForKeys

View File

@ -0,0 +1,340 @@
--!nonstrict
--[[
Constructs a new ForPairs object which maps pairs of a table using
a `processor` function.
Optionally, a `destructor` function can be specified for cleaning up values.
If omitted, the default cleanup function will be used instead.
Additionally, a `meta` table/value can optionally be returned to pass data created
when running the processor to the destructor when the created object is cleaned up.
]]
local PubTypes = require "../PubTypes"
local Types = require "../Types"
-- Logging
local parseError = require "../Logging/parseError"
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
local logError = require "../Logging/logError"
local logWarn = require "../Logging/logWarn"
-- Utility
local cleanup = require "../Utility/cleanup"
local needsDestruction = require "../Utility/needsDestruction"
-- State
local peek = require "../State/peek"
local makeUseCallback = require "../State/makeUseCallback"
local isState = require "../State/isState"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Called when the original table is changed.
This will firstly find any keys meeting any of the following criteria:
- they were not previously present
- their associated value has changed
- a dependency used during generation of this value has changed
It will recalculate those key/value pairs, storing information about any
dependencies used in the processor callback during value generation, and
save the new key/value pair to the output array. If it is overwriting an
older key/value pair, that older pair will be passed to the destructor
for cleanup.
Finally, this function will find keys that are no longer present, and remove
their key/value pairs from the output table and pass them to the destructor.
]]
function class:update(): boolean
local inputIsState = self._inputIsState
local newInputTable = peek(self._inputTable)
local oldInputTable = self._oldInputTable
local keyIOMap = self._keyIOMap
local meta = self._meta
local didChange = false
-- clean out main dependency set
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = nil
end
self._oldDependencySet, self.dependencySet =
self.dependencySet, self._oldDependencySet
-- table.clear(self.dependencySet)
for i, _ in pairs(self.dependencySet) do
self.dependencySet[i] = nil
end
-- if the input table is a state object, add it as a dependency
if inputIsState then
self._inputTable.dependentSet[self] = true
self.dependencySet[self._inputTable] = true
end
-- clean out output table
self._oldOutputTable, self._outputTable =
self._outputTable, self._oldOutputTable
local oldOutputTable = self._oldOutputTable
local newOutputTable = self._outputTable
-- table.clear(newOutputTable)
for i, _ in pairs(newOutputTable) do
newOutputTable[i] = nil
end
-- Step 1: find key/value pairs that changed or were not previously present
for newInKey, newInValue in pairs(newInputTable) do
-- get or create key data
local keyData = self._keyData[newInKey]
if keyData == nil then
keyData = {
dependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE),
}
self._keyData[newInKey] = keyData
end
-- check if the pair is new or changed
local shouldRecalculate = oldInputTable[newInKey] ~= newInValue
-- check if the pair's dependencies have changed
if shouldRecalculate == false then
for dependency, oldValue in pairs(keyData.dependencyValues) do
if oldValue ~= peek(dependency) then
shouldRecalculate = true
break
end
end
end
-- recalculate the output pair if necessary
if shouldRecalculate then
keyData.oldDependencySet, keyData.dependencySet =
keyData.dependencySet, keyData.oldDependencySet
-- table.clear(keyData.dependencySet)
for i, _ in pairs(keyData.dependencySet) do
keyData.dependencySet[i] = nil
end
local use = makeUseCallback(keyData.dependencySet)
-- local processOK, newOutKey, newOutValue, newMetaValue =
-- xpcall(self._processor, parseError, use, newInKey, newInValue)
local processOK, newOutKey, newOutValue, newMetaValue =
pcall(self._processor, use, newInKey, newInValue)
if processOK then
if
self._destructor == nil
and (
needsDestruction(newOutKey)
or needsDestruction(newOutValue)
or needsDestruction(newMetaValue)
)
then
logWarn "destructorNeededForPairs"
end
-- if this key was already written to on this run-through, throw a fatal error.
if newOutputTable[newOutKey] ~= nil then
-- figure out which key/value pair previously wrote to this key
local previousNewKey, previousNewValue
for inKey, outKey in pairs(keyIOMap) do
if outKey == newOutKey then
previousNewValue = newInputTable[inKey]
if previousNewValue ~= nil then
previousNewKey = inKey
break
end
end
end
if previousNewKey ~= nil then
logError(
"forPairsKeyCollision",
nil,
tostring(newOutKey),
tostring(previousNewKey),
tostring(previousNewValue),
tostring(newInKey),
tostring(newInValue)
)
end
end
local oldOutValue = oldOutputTable[newOutKey]
if oldOutValue ~= newOutValue then
local oldMetaValue = meta[newOutKey]
if oldOutValue ~= nil then
local destructOK, err = pcall(
self._destructor or cleanup,
newOutKey,
oldOutValue,
oldMetaValue
)
if not destructOK then
logErrorNonFatal(
"forPairsDestructorError",
parseError(err)
)
end
end
oldOutputTable[newOutKey] = nil
end
-- update the stored data for this key/value pair
oldInputTable[newInKey] = newInValue
keyIOMap[newInKey] = newOutKey
meta[newOutKey] = newMetaValue
newOutputTable[newOutKey] = newOutValue
-- if we had to recalculate the output, then we did change
didChange = true
else
-- restore old dependencies, because the new dependencies may be corrupt
keyData.oldDependencySet, keyData.dependencySet =
keyData.dependencySet, keyData.oldDependencySet
logErrorNonFatal(
"forPairsProcessorError",
parseError(newOutKey)
)
end
else
local storedOutKey = keyIOMap[newInKey]
-- check for key collision
if newOutputTable[storedOutKey] ~= nil then
-- figure out which key/value pair previously wrote to this key
local previousNewKey, previousNewValue
for inKey, outKey in pairs(keyIOMap) do
if storedOutKey == outKey then
previousNewValue = newInputTable[inKey]
if previousNewValue ~= nil then
previousNewKey = inKey
break
end
end
end
if previousNewKey ~= nil then
logError(
"forPairsKeyCollision",
nil,
tostring(storedOutKey),
tostring(previousNewKey),
tostring(previousNewValue),
tostring(newInKey),
tostring(newInValue)
)
end
end
-- copy the stored key/value pair into the new output table
newOutputTable[storedOutKey] = oldOutputTable[storedOutKey]
end
-- save dependency values and add to main dependency set
for dependency in pairs(keyData.dependencySet) do
keyData.dependencyValues[dependency] = peek(dependency)
self.dependencySet[dependency] = true
dependency.dependentSet[self] = true
end
end
-- STEP 2: find keys that were removed
for oldOutKey, oldOutValue in pairs(oldOutputTable) do
-- check if this key/value pair is in the new output table
if newOutputTable[oldOutKey] ~= oldOutValue then
-- clean up the old output pair
local oldMetaValue = meta[oldOutKey]
if oldOutValue ~= nil then
local destructOK, err = pcall(
self._destructor or cleanup,
oldOutKey,
oldOutValue,
oldMetaValue
)
if not destructOK then
logErrorNonFatal("forPairsDestructorError", parseError(err))
end
end
-- check if the key was completely removed from the output table
if newOutputTable[oldOutKey] == nil then
meta[oldOutKey] = nil
self._keyData[oldOutKey] = nil
end
didChange = true
end
end
for key in pairs(oldInputTable) do
if newInputTable[key] == nil then
oldInputTable[key] = nil
keyIOMap[key] = nil
end
end
return didChange
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._outputTable
end
function class:get()
logError "stateGetWasRemoved"
end
local function ForPairs<KI, VI, KO, VO, M>(
inputTable: PubTypes.CanBeState<{ [KI]: VI }>,
processor: (KI, VI) -> (KO, VO, M?),
destructor: (KO, VO, M?) -> ()?
): Types.ForPairs<KI, VI, KO, VO, M>
local inputIsState = isState(inputTable)
local self = setmetatable({
type = "State",
kind = "ForPairs",
dependencySet = {},
-- if we held strong references to the dependents, then they wouldn't be
-- able to get garbage collected when they fall out of scope
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
_oldDependencySet = {},
_processor = processor,
_destructor = destructor,
_inputIsState = inputIsState,
_inputTable = inputTable,
_oldInputTable = {},
_outputTable = {},
_oldOutputTable = {},
_keyIOMap = {},
_keyData = {},
_meta = {},
}, CLASS_METATABLE)
self:update()
return self
end
return ForPairs

View File

@ -0,0 +1,272 @@
--!nonstrict
--[[
Constructs a new ForValues object which maps values of a table using
a `processor` function.
Optionally, a `destructor` function can be specified for cleaning up values.
If omitted, the default cleanup function will be used instead.
Additionally, a `meta` table/value can optionally be returned to pass data created
when running the processor to the destructor when the created object is cleaned up.
]]
local PubTypes = require "../PubTypes"
local Types = require "../Types"
-- Logging
local parseError = require "../Logging/parseError"
local logError = require "../Logging/logError"
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
local logWarn = require "../Logging/logWarn"
-- Utility
local cleanup = require "../Utility/cleanup"
local needsDestruction = require "../Utility/needsDestruction"
-- State
local peek = require "../State/peek"
local makeUseCallback = require "../State/makeUseCallback"
local isState = require "../State/isState"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Called when the original table is changed.
This will firstly find any values meeting any of the following criteria:
- they were not previously present
- a dependency used during generation of this value has changed
It will recalculate those values, storing information about any dependencies
used in the processor callback during value generation, and save the new value
to the output array with the same key. If it is overwriting an older value,
that older value will be passed to the destructor for cleanup.
Finally, this function will find values that are no longer present, and remove
their values from the output table and pass them to the destructor. You can re-use
the same value multiple times and this will function will update them as little as
possible; reusing the same values where possible.
]]
function class:update(): boolean
local inputIsState = self._inputIsState
local inputTable = peek(self._inputTable)
local outputValues = {}
local didChange = false
-- clean out value cache
self._oldValueCache, self._valueCache =
self._valueCache, self._oldValueCache
local newValueCache = self._valueCache
local oldValueCache = self._oldValueCache
-- table.clear(newValueCache)
for i, _ in pairs(newValueCache) do
newValueCache[i] = nil
end
-- clean out main dependency set
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = nil
end
self._oldDependencySet, self.dependencySet =
self.dependencySet, self._oldDependencySet
-- table.clear(self.dependencySet)
for i, _ in pairs(self.dependencySet) do
self.dependencySet[i] = nil
end
-- if the input table is a state object, add it as a dependency
if inputIsState then
self._inputTable.dependentSet[self] = true
self.dependencySet[self._inputTable] = true
end
-- STEP 1: find values that changed or were not previously present
for inKey, inValue in pairs(inputTable) do
-- check if the value is new or changed
local oldCachedValues = oldValueCache[inValue]
local shouldRecalculate = oldCachedValues == nil
-- get a cached value and its dependency/meta data if available
local value, valueData, meta
if type(oldCachedValues) == "table" and #oldCachedValues > 0 then
local valueInfo = table.remove(oldCachedValues, #oldCachedValues)
value = valueInfo.value
valueData = valueInfo.valueData
meta = valueInfo.meta
if #oldCachedValues <= 0 then
oldValueCache[inValue] = nil
end
elseif oldCachedValues ~= nil then
oldValueCache[inValue] = nil
shouldRecalculate = true
end
if valueData == nil then
valueData = {
dependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE),
}
end
-- check if the value's dependencies have changed
if shouldRecalculate == false then
for dependency, oldValue in pairs(valueData.dependencyValues) do
if oldValue ~= peek(dependency) then
shouldRecalculate = true
break
end
end
end
-- recalculate the output value if necessary
if shouldRecalculate then
valueData.oldDependencySet, valueData.dependencySet =
valueData.dependencySet, valueData.oldDependencySet
-- table.clear(valueData.dependencySet)
for i, _ in pairs(valueData.dependencySet) do
valueData.dependencySet[i] = nil
end
local use = makeUseCallback(valueData.dependencySet)
-- local processOK, newOutValue, newMetaValue =
-- xpcall(self._processor, parseError, use, inValue)
local processOK, newOutValue, newMetaValue =
pcall(self._processor, use, inValue)
if processOK then
if
self._destructor == nil
and (
needsDestruction(newOutValue)
or needsDestruction(newMetaValue)
)
then
logWarn "destructorNeededForValues"
end
-- pass the old value to the destructor if it exists
if value ~= nil then
local destructOK, err =
pcall(self._destructor or cleanup, value, meta)
if not destructOK then
logErrorNonFatal(
"forValuesDestructorError",
parseError(err)
)
end
end
-- store the new value and meta data
value = newOutValue
meta = newMetaValue
didChange = true
else
-- restore old dependencies, because the new dependencies may be corrupt
valueData.oldDependencySet, valueData.dependencySet =
valueData.dependencySet, valueData.oldDependencySet
logErrorNonFatal(
"forValuesProcessorError",
parseError(newOutValue)
)
end
end
-- store the value and its dependency/meta data
local newCachedValues = newValueCache[inValue]
if newCachedValues == nil then
newCachedValues = {}
newValueCache[inValue] = newCachedValues
end
table.insert(newCachedValues, {
value = value,
valueData = valueData,
meta = meta,
})
outputValues[inKey] = value
-- save dependency values and add to main dependency set
for dependency in pairs(valueData.dependencySet) do
valueData.dependencyValues[dependency] = peek(dependency)
self.dependencySet[dependency] = true
dependency.dependentSet[self] = true
end
end
-- STEP 2: find values that were removed
-- for tables of data, we just need to check if it's still in the cache
for _oldInValue, oldCachedValueInfo in pairs(oldValueCache) do
for _, valueInfo in ipairs(oldCachedValueInfo) do
local oldValue = valueInfo.value
local oldMetaValue = valueInfo.meta
local destructOK, err =
pcall(self._destructor or cleanup, oldValue, oldMetaValue)
if not destructOK then
logErrorNonFatal("forValuesDestructorError", parseError(err))
end
didChange = true
end
-- table.clear(oldCachedValueInfo)
for i, _ in pairs(oldCachedValueInfo) do
oldCachedValueInfo[i] = nil
end
end
self._outputTable = outputValues
return didChange
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._outputTable
end
function class:get()
logError "stateGetWasRemoved"
end
local function ForValues<VI, VO, M>(
inputTable: PubTypes.CanBeState<{ [any]: VI }>,
processor: (VI) -> (VO, M?),
destructor: (VO, M?) -> ()?
): Types.ForValues<VI, VO, M>
local inputIsState = isState(inputTable)
local self = setmetatable({
type = "State",
kind = "ForValues",
dependencySet = {},
-- if we held strong references to the dependents, then they wouldn't be
-- able to get garbage collected when they fall out of scope
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
_oldDependencySet = {},
_processor = processor,
_destructor = destructor,
_inputIsState = inputIsState,
_inputTable = inputTable,
_outputTable = {},
_valueCache = {},
_oldValueCache = {},
}, CLASS_METATABLE)
self:update()
return self
end
return ForValues

View File

@ -0,0 +1,90 @@
--!nonstrict
--[[
Constructs a new state object which can listen for updates on another state
object.
FIXME: enabling strict types here causes free types to leak
]]
local PubTypes = require "../PubTypes"
local Types = require "../Types"
local External = require "../External"
type Set<T> = { [T]: any }
local class = {}
local CLASS_METATABLE = { __index = class }
-- Table used to hold Observer objects in memory.
local strongRefs: Set<Types.Observer> = {}
--[[
Called when the watched state changes value.
]]
function class:update(): boolean
for _, callback in pairs(self._changeListeners) do
External.doTaskImmediate(callback)
end
return false
end
--[[
Adds a change listener. When the watched state changes value, the listener
will be fired.
Returns a function which, when called, will disconnect the change listener.
As long as there is at least one active change listener, this Observer
will be held in memory, preventing GC, so disconnecting is important.
]]
function class:onChange(callback: () -> ()): () -> ()
local uniqueIdentifier = {}
self._numChangeListeners += 1
self._changeListeners[uniqueIdentifier] = callback
-- disallow gc (this is important to make sure changes are received)
strongRefs[self] = true
local disconnected = false
return function()
if disconnected then
return
end
disconnected = true
self._changeListeners[uniqueIdentifier] = nil
self._numChangeListeners -= 1
if self._numChangeListeners == 0 then
-- allow gc if all listeners are disconnected
strongRefs[self] = nil
end
end
end
--[[
Similar to `class:onChange()`, however it runs the provided callback
immediately.
]]
function class:onBind(callback: () -> ()): () -> ()
External.doTaskImmediate(callback)
return self:onChange(callback)
end
local function Observer(watchedState: PubTypes.Value<any>): Types.Observer
local self = setmetatable({
type = "State",
kind = "Observer",
dependencySet = { [watchedState] = true },
dependentSet = {},
_changeListeners = {},
_numChangeListeners = 0,
}, CLASS_METATABLE)
-- add this object to the watched state's dependent set
watchedState.dependentSet[self] = true
return self
end
return Observer

View File

@ -0,0 +1,60 @@
--!nonstrict
--[[
Constructs and returns objects which can be used to model independent
reactive state.
]]
local Types = require "../Types"
-- Logging
local logError = require "../Logging/logError"
-- State
local updateAll = require "../State/updateAll"
-- Utility
local isSimilar = require "../Utility/isSimilar"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Updates the value stored in this State object.
If `force` is enabled, this will skip equality checks and always update the
state object and any dependents - use this with care as this can lead to
unnecessary updates.
]]
function class:set(newValue: any, force: boolean?)
local oldValue = self._value
if force or not isSimilar(oldValue, newValue) then
self._value = newValue
updateAll(self)
end
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._value
end
function class:get()
logError "stateGetWasRemoved"
end
local function Value<T>(initialValue: T): Types.State<T>
local self = setmetatable({
type = "State",
kind = "Value",
-- if we held strong references to the dependents, then they wouldn't be
-- able to get garbage collected when they fall out of scope
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
_value = initialValue,
}, CLASS_METATABLE)
return self
end
return Value

View File

@ -0,0 +1,11 @@
--!strict
--[[
Returns true if the given value can be assumed to be a valid state object.
]]
local function isState(target: any): boolean
return type(target) == "table" and type(target._peek) == "function"
end
return isState

View File

@ -0,0 +1,25 @@
--!strict
--[[
Constructs a 'use callback' for the purposes of collecting dependencies.
]]
local PubTypes = require "../PubTypes"
local Types = require "../Types"
-- State
local isState = require "../State/isState"
type Set<T> = { [T]: any }
local function makeUseCallback(dependencySet: Set<PubTypes.Dependency>)
local function use<T>(target: PubTypes.CanBeState<T>): T
if isState(target) then
dependencySet[target] = true
return (target :: Types.StateObject<T>):_peek()
end
return target
end
return use
end
return makeUseCallback

View File

@ -0,0 +1,19 @@
--!strict
--[[
A common interface for accessing the values of state objects or constants.
]]
local PubTypes = require "../PubTypes"
local Types = require "../Types"
-- State
local isState = require "../State/isState"
local function peek<T>(target: PubTypes.CanBeState<T>): T
if isState(target) then
return (target :: Types.StateObject<T>):_peek()
end
return target
end
return peek

View File

@ -0,0 +1,67 @@
--!strict
--[[
Given a reactive object, updates all dependent reactive objects.
Objects are only ever updated after all of their dependencies are updated,
are only ever updated once, and won't be updated if their dependencies are
unchanged.
]]
local PubTypes = require "../PubTypes"
type Set<T> = { [T]: any }
type Descendant = (PubTypes.Dependent & PubTypes.Dependency) | PubTypes.Dependent
-- Credit: https://blog.elttob.uk/2022/11/07/sets-efficient-topological-search.html
local function updateAll(root: PubTypes.Dependency)
local counters: { [Descendant]: number } = {}
local flags: { [Descendant]: boolean } = {}
local queue: { Descendant } = {}
local queueSize = 0
local queuePos = 1
for object in pairs(root.dependentSet) do
queueSize += 1
queue[queueSize] = object
flags[object] = true
end
-- Pass 1: counting up
while queuePos <= queueSize do
local next = queue[queuePos]
local counter = counters[next]
if counter == nil then
counters[next] = 1
else
counters[next] = counter + 1
end
if next.dependentSet ~= nil then
for object in pairs(next.dependentSet) do
queueSize += 1
queue[queueSize] = object
end
end
queuePos += 1
end
-- Pass 2: counting down + processing
queuePos = 1
while queuePos <= queueSize do
local next = queue[queuePos]
local counter = counters[next] - 1
counters[next] = counter
if
counter == 0
and flags[next]
and next:update()
and next.dependentSet ~= nil
then
for object in pairs(next.dependentSet) do
flags[object] = true
end
end
queuePos += 1
end
end
return updateAll

159
Libraries/Fusion/Types.luau Normal file
View File

@ -0,0 +1,159 @@
--!strict
--[[
Stores common type information used internally.
These types may be used internally so Fusion code can type-check, but
should never be exposed to public users, as these definitions are fair game
for breaking changes.
]]
local PubTypes = require "./PubTypes"
type Set<T> = { [T]: any }
--[[
General use types
]]
-- A symbol that represents the absence of a value.
export type None = PubTypes.Symbol & {
-- name: "None" (add this when Luau supports singleton types)
}
-- Stores useful information about Luau errors.
export type Error = {
type: string, -- replace with "Error" when Luau supports singleton types
raw: string,
message: string,
trace: string,
}
-- An object which stores a value scoped in time.
export type Contextual<T> = PubTypes.Contextual<T> & {
_valuesNow: { [thread]: { value: T } },
_defaultValue: T,
}
--[[
Generic reactive graph types
]]
export type StateObject<T> = PubTypes.StateObject<T> & {
_peek: (StateObject<T>) -> T,
}
--[[
Specific reactive graph types
]]
-- A state object whose value can be set at any time by the user.
export type State<T> = PubTypes.Value<T> & {
_value: T,
}
-- A state object whose value is derived from other objects using a callback.
export type Computed<T> = PubTypes.Computed<T> & {
_oldDependencySet: Set<PubTypes.Dependency>,
_callback: (PubTypes.Use) -> T,
_value: T,
}
-- A state object whose value is derived from other objects using a callback.
export type ForPairs<KI, VI, KO, VO, M> = PubTypes.ForPairs<KO, VO> & {
_oldDependencySet: Set<PubTypes.Dependency>,
_processor: (PubTypes.Use, KI, VI) -> (KO, VO),
_destructor: (VO, M?) -> (),
_inputIsState: boolean,
_inputTable: PubTypes.CanBeState<{ [KI]: VI }>,
_oldInputTable: { [KI]: VI },
_outputTable: { [KO]: VO },
_oldOutputTable: { [KO]: VO },
_keyIOMap: { [KI]: KO },
_meta: { [KO]: M? },
_keyData: {
[KI]: {
dependencySet: Set<PubTypes.Dependency>,
oldDependencySet: Set<PubTypes.Dependency>,
dependencyValues: { [PubTypes.Dependency]: any },
},
},
}
-- A state object whose value is derived from other objects using a callback.
export type ForKeys<KI, KO, M> = PubTypes.ForKeys<KO, any> & {
_oldDependencySet: Set<PubTypes.Dependency>,
_processor: (PubTypes.Use, KI) -> (KO),
_destructor: (KO, M?) -> (),
_inputIsState: boolean,
_inputTable: PubTypes.CanBeState<{ [KI]: KO }>,
_oldInputTable: { [KI]: KO },
_outputTable: { [KO]: any },
_keyOIMap: { [KO]: KI },
_meta: { [KO]: M? },
_keyData: {
[KI]: {
dependencySet: Set<PubTypes.Dependency>,
oldDependencySet: Set<PubTypes.Dependency>,
dependencyValues: { [PubTypes.Dependency]: any },
},
},
}
-- A state object whose value is derived from other objects using a callback.
export type ForValues<VI, VO, M> = PubTypes.ForValues<any, VO> & {
_oldDependencySet: Set<PubTypes.Dependency>,
_processor: (PubTypes.Use, VI) -> (VO),
_destructor: (VO, M?) -> (),
_inputIsState: boolean,
_inputTable: PubTypes.CanBeState<{ [VI]: VO }>,
_outputTable: { [any]: VI },
_valueCache: { [VO]: any },
_oldValueCache: { [VO]: any },
_meta: { [VO]: M? },
_valueData: {
[VI]: {
dependencySet: Set<PubTypes.Dependency>,
oldDependencySet: Set<PubTypes.Dependency>,
dependencyValues: { [PubTypes.Dependency]: any },
},
},
}
-- A state object which follows another state object using tweens.
export type Tween<T> = PubTypes.Tween<T> & {
_goalState: State<T>,
_tweenInfo: TweenInfo,
_prevValue: T,
_nextValue: T,
_currentValue: T,
_currentTweenInfo: TweenInfo,
_currentTweenDuration: number,
_currentTweenStartTime: number,
_currentlyAnimating: boolean,
}
-- A state object which follows another state object using spring simulation.
export type Spring<T> = PubTypes.Spring<T> & {
_speed: PubTypes.CanBeState<number>,
_speedIsState: boolean,
_lastSpeed: number,
_damping: PubTypes.CanBeState<number>,
_dampingIsState: boolean,
_lastDamping: number,
_goalState: State<T>,
_goalValue: T,
_currentType: string,
_currentValue: T,
_springPositions: { number },
_springGoals: { number },
_springVelocities: { number },
}
-- An object which can listen for updates on another state object.
export type Observer = PubTypes.Observer & {
_changeListeners: Set<() -> ()>,
_numChangeListeners: number,
}
return nil

View File

@ -0,0 +1,74 @@
--!strict
--!nolint LocalShadow
--[[
Time-based contextual values, to allow for transparently passing values down
the call stack.
]]
local Types = require "../Types"
-- Logging
local logError = require "../Logging/logError"
local parseError = require "../Logging/parseError"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Returns the current value of this contextual.
]]
function class:now(): unknown
local thread = coroutine.running()
local value = self._valuesNow[thread]
if typeof(value) ~= "table" then
return self._defaultValue
else
return value.value :: unknown
end
end
--[[
Temporarily assigns a value to this contextual.
]]
function class:is(newValue: unknown)
local methods = {}
-- Methods use colon `:` syntax for consistency and autocomplete but we
-- actually want them to operate on the `self` from this outer lexical scope
local contextual = self
function methods:during<T, A...>(callback: (A...) -> T, ...: A...): T
local thread = coroutine.running()
local prevValue = contextual._valuesNow[thread]
-- Storing the value in this format allows us to distinguish storing
-- `nil` from not calling `:during()` at all.
contextual._valuesNow[thread] = { value = newValue }
-- local ok, value = xpcall(callback, parseError, ...)
local ok, value = pcall(callback, ...)
contextual._valuesNow[thread] = prevValue
if ok then
return value
else
logError("contextualCallbackError", parseError(value))
end
end
return methods
end
local function Contextual<T>(defaultValue: T): Types.Contextual<T>
local self = setmetatable({
type = "Contextual",
-- if we held strong references to threads here, then if a thread was
-- killed before this contextual had a chance to finish executing its
-- callback, it would be held strongly in this table forever
_valuesNow = setmetatable({}, WEAK_KEYS_METATABLE),
_defaultValue = defaultValue,
}, CLASS_METATABLE)
return self
end
return Contextual

View File

@ -0,0 +1,12 @@
--!strict
--[[
A symbol for representing nil values in contexts where nil is not usable.
]]
local Types = require "../Types"
return {
type = "Symbol",
name = "None",
} :: Types.None

View File

@ -0,0 +1,54 @@
--!strict
--[[
Cleans up the tasks passed in as the arguments.
A task can be any of the following:
- an Instance - will be destroyed
- an RBXScriptConnection - will be disconnected
- a function - will be run
- a table with a `Destroy` or `destroy` function - will be called
- an array - `cleanup` will be called on each item
]]
local typeof = require "../Polyfill/typeof"
local function cleanupOne(task: any)
local taskType = typeof(task)
-- case 1: Instance
if taskType == "Instance" then
task:Destroy()
-- case 2: RBXScriptConnection
elseif taskType == "RBXScriptConnection" then
task:disconnect()
-- case 3: callback
elseif taskType == "function" then
task()
elseif taskType == "table" then
-- case 4: destroy() function
if type(task.destroy) == "function" then
task:destroy()
-- case 5: Destroy() function
elseif type(task.Destroy) == "function" then
task:Destroy()
-- case 6: array of tasks
elseif task[1] ~= nil then
for _, subtask in ipairs(task) do
cleanupOne(subtask)
end
end
end
end
local function cleanup(...: any)
for index = 1, select("#", ...) do
cleanupOne(select(index, ...))
end
end
return cleanup

View File

@ -0,0 +1,9 @@
--!strict
--[[
An empty function. Often used as a destructor to indicate no destruction.
]]
local function doNothing(...: any) end
return doNothing

View File

@ -0,0 +1,17 @@
--!strict
--[[
Returns true if A and B are 'similar' - i.e. any user of A would not need
to recompute if it changed to B.
]]
local function isSimilar(a: any, b: any): boolean
-- HACK: because tables are mutable data structures, don't make assumptions
-- about similarity from equality for now (see issue #44)
if type(a) == "table" then
return false
end
-- NaN does not equal itself but is the same
return a == b or a ~= a and b ~= b
end
return isSimilar

View File

@ -0,0 +1,14 @@
--!strict
--[[
Returns true if the given value is not automatically memory managed, and
requires manual cleanup.
]]
local typeof = require "../Polyfill/typeof"
local function needsDestruction(x: any): boolean
return typeof(x) == "Instance"
end
return needsDestruction

View File

@ -0,0 +1,27 @@
--!strict
--[[
Restricts the reading of missing members for a table.
]]
local logError = require "../Logging/logError"
type table = { [any]: any }
local function restrictRead(tableName: string, strictTable: table): table
-- FIXME: Typed Luau doesn't recognise this correctly yet
local metatable = getmetatable(strictTable :: any)
if metatable == nil then
metatable = {}
setmetatable(strictTable, metatable)
end
function metatable:__index(memberName)
logError("strictReadError", nil, tostring(memberName), tableName)
end
return strictTable
end
return restrictRead

View File

@ -0,0 +1,21 @@
--!strict
--[[
Extended typeof, designed for identifying custom objects.
If given a table with a `type` string, returns that.
Otherwise, returns `typeof()` the argument.
]]
local typeof = require "../Polyfill/typeof"
local function xtypeof(x: any)
local typeString = typeof(x)
if typeString == "table" and type(x.type) == "string" then
return x.type
else
return typeString
end
end
return xtypeof

115
Libraries/Fusion/init.luau Normal file
View File

@ -0,0 +1,115 @@
--!strict
--[[
The entry point for the Fusion library.
]]
local PubTypes = require "./PubTypes"
local External = require "./External"
local restrictRead = require "./Utility/restrictRead"
-- Down the line, this will be conditional based on whether Fusion is being
-- compiled for Mercury.
do
local MercuryExternal = require "./MercuryExternal"
External.setExternalScheduler(MercuryExternal)
end
local Fusion = restrictRead("Fusion", {
version = { major = 0, minor = 3, isRelease = false },
New = require "./Instances/New",
Hydrate = require "./Instances/Hydrate",
Ref = require "./Instances/Ref",
Out = require "./Instances/Out",
Cleanup = require "./Instances/Cleanup",
Children = require "./Instances/Children",
OnEvent = require "./Instances/OnEvent",
OnChange = require "./Instances/OnChange",
Value = require "./State/Value",
Computed = require "./State/Computed",
ForPairs = require "./State/ForPairs",
ForKeys = require "./State/ForKeys",
ForValues = require "./State/ForValues",
Observer = require "./State/Observer",
Tween = require "./Animation/Tween",
Spring = require "./Animation/Spring",
Contextual = require "./Utility/Contextual",
cleanup = require "./Utility/cleanup",
doNothing = require "./Utility/doNothing",
peek = require "./State/peek",
typeof = require "./Polyfill/typeof",
TweenInfo = require "./Polyfill/TweenInfo",
Help = function()
return "See https://elttob.uk/Fusion/0.3/ for more information."
end,
}) :: Fusion
export type StateObject<T> = PubTypes.StateObject<T>
export type CanBeState<T> = PubTypes.CanBeState<T>
export type Symbol = PubTypes.Symbol
export type Value<T> = PubTypes.Value<T>
export type Computed<T> = PubTypes.Computed<T>
export type ForPairs<KO, VO> = PubTypes.ForPairs<KO, VO>
export type ForKeys<KI, KO> = PubTypes.ForKeys<KI, KO>
export type ForValues<VI, VO> = PubTypes.ForKeys<VI, VO>
export type Observer = PubTypes.Observer
export type Tween<T> = PubTypes.Tween<T>
export type Spring<T> = PubTypes.Spring<T>
export type Use = PubTypes.Use
export type Contextual<T> = PubTypes.Contextual<T>
type Fusion = {
version: PubTypes.Version,
New: (
className: string
) -> ((propertyTable: PubTypes.PropertyTable) -> Instance),
Hydrate: (
target: Instance
) -> ((propertyTable: PubTypes.PropertyTable) -> Instance),
Ref: PubTypes.SpecialKey,
Cleanup: PubTypes.SpecialKey,
Children: PubTypes.SpecialKey,
Out: (propertyName: string) -> PubTypes.SpecialKey,
OnEvent: (eventName: string) -> PubTypes.SpecialKey,
OnChange: (propertyName: string) -> PubTypes.SpecialKey,
Value: <T>(initialValue: T) -> Value<T>,
Computed: <T>(callback: (Use) -> T, destructor: (T) -> ()?) -> Computed<T>,
ForPairs: <KI, VI, KO, VO, M>(
inputTable: CanBeState<{ [KI]: VI }>,
processor: (Use, KI, VI) -> (KO, VO, M?),
destructor: (KO, VO, M?) -> ()?
) -> ForPairs<KO, VO>,
ForKeys: <KI, KO, M>(
inputTable: CanBeState<{ [KI]: any }>,
processor: (Use, KI) -> (KO, M?),
destructor: (KO, M?) -> ()?
) -> ForKeys<KO, any>,
ForValues: <VI, VO, M>(
inputTable: CanBeState<{ [any]: VI }>,
processor: (Use, VI) -> (VO, M?),
destructor: (VO, M?) -> ()?
) -> ForValues<any, VO>,
Observer: (watchedState: StateObject<any>) -> Observer,
Tween: <T>(goalState: StateObject<T>, tweenInfo: TweenInfo?) -> Tween<T>,
Spring: <T>(
goalState: StateObject<T>,
speed: CanBeState<number>?,
damping: CanBeState<number>?
) -> Spring<T>,
Contextual: <T>(defaultValue: T) -> Contextual<T>,
cleanup: (...any) -> (),
doNothing: (...any) -> (),
peek: Use,
}
return Fusion

View File

@ -1,3 +1,3 @@
# Corescripts
After installing Aftman and running `aftman install`, run `./compile.sh` to compile the corescripts from ./luau/\*.luau to ./processed/\*.lua.
After installing Aftman and running `aftman install`, run `./corescripts/compile.sh` to compile the corescripts and libraries from this directory to ./corescripts/processed.

View File

@ -3,6 +3,9 @@ for file in ./corescripts/luau/[0-9]*.luau; do
darklua process -c dense.json5 $file ./corescripts/processed/$(basename "${file::-1}")
done
echo "Processing libraries..."
darklua process -c dense.json5 ./corescripts/Libraries/Fusion/init.luau ./corescripts/processed/10000001.lua
echo "Processing other corescripts..."
for file in ./corescripts/luau/[a-z]*.luau; do
darklua process -c lines.json5 $file ./corescripts/processed/$(basename "${file::-1}")

File diff suppressed because it is too large Load Diff

View File

@ -2166,13 +2166,13 @@ local createReportAbuseDialog = function()
frame.Active = true
frame.Parent = shield
settingsFrame = Instance.new "Frame"
settingsFrame.Name = "ReportAbuseStyle"
settingsFrame.Size = UDim2.new(1, 0, 1, 0)
settingsFrame.Style = Enum.FrameStyle.RobloxRound
settingsFrame.Active = true
settingsFrame.ZIndex = baseZIndex + 1
settingsFrame.Parent = frame
local reportAbuseFrame = Instance.new "Frame"
reportAbuseFrame.Name = "ReportAbuseStyle"
reportAbuseFrame.Size = UDim2.new(1, 0, 1, 0)
reportAbuseFrame.Style = Enum.FrameStyle.RobloxRound
reportAbuseFrame.Active = true
reportAbuseFrame.ZIndex = baseZIndex + 1
reportAbuseFrame.Parent = frame
local title = Instance.new "TextLabel"
title.Name = "Title"
@ -2182,7 +2182,7 @@ local createReportAbuseDialog = function()
title.Font = Enum.Font.ArialBold
title.FontSize = Enum.FontSize.Size36
title.ZIndex = baseZIndex + 2
title.Parent = settingsFrame
title.Parent = reportAbuseFrame
local description = Instance.new "TextLabel"
description.Name = "Description"
@ -2198,7 +2198,7 @@ local createReportAbuseDialog = function()
description.ZIndex = baseZIndex + 2
description.TextXAlignment = Enum.TextXAlignment.Left
description.TextYAlignment = Enum.TextYAlignment.Top
description.Parent = settingsFrame
description.Parent = reportAbuseFrame
local playerLabel = Instance.new "TextLabel"
playerLabel.Name = "PlayerLabel"
@ -2211,7 +2211,7 @@ local createReportAbuseDialog = function()
playerLabel.TextColor3 = Color3I(255, 255, 255)
playerLabel.TextXAlignment = Enum.TextXAlignment.Left
playerLabel.ZIndex = baseZIndex + 2
playerLabel.Parent = settingsFrame
playerLabel.Parent = reportAbuseFrame
local abusingPlayer
local abuse
@ -2261,7 +2261,7 @@ local createReportAbuseDialog = function()
abuseLabel.TextColor3 = Color3I(255, 255, 255)
abuseLabel.TextXAlignment = Enum.TextXAlignment.Left
abuseLabel.ZIndex = baseZIndex + 2
abuseLabel.Parent = settingsFrame
abuseLabel.Parent = reportAbuseFrame
local abuses = {
"Swearing",
@ -2288,7 +2288,7 @@ local createReportAbuseDialog = function()
abuseDropDown.ZIndex = baseZIndex + 2
abuseDropDown.Position = UDim2.new(0.425, 0, 0, 142)
abuseDropDown.Size = UDim2.new(0.55, 0, 0, 32)
abuseDropDown.Parent = settingsFrame
abuseDropDown.Parent = reportAbuseFrame
local shortDescriptionLabel = Instance.new "TextLabel"
shortDescriptionLabel.Name = "ShortDescriptionLabel"
@ -2301,7 +2301,7 @@ local createReportAbuseDialog = function()
shortDescriptionLabel.TextXAlignment = Enum.TextXAlignment.Left
shortDescriptionLabel.BackgroundTransparency = 1
shortDescriptionLabel.ZIndex = baseZIndex + 2
shortDescriptionLabel.Parent = settingsFrame
shortDescriptionLabel.Parent = reportAbuseFrame
local shortDescriptionWrapper = Instance.new "Frame"
shortDescriptionWrapper.Name = "ShortDescriptionWrapper"
@ -2310,7 +2310,7 @@ local createReportAbuseDialog = function()
shortDescriptionWrapper.BackgroundColor3 = Color3I(0, 0, 0)
shortDescriptionWrapper.BorderSizePixel = 0
shortDescriptionWrapper.ZIndex = baseZIndex + 2
shortDescriptionWrapper.Parent = settingsFrame
shortDescriptionWrapper.Parent = reportAbuseFrame
local shortDescriptionBox = Instance.new "TextBox"
shortDescriptionBox.Name = "TextBox"
@ -2342,7 +2342,7 @@ local createReportAbuseDialog = function()
submitReportButton.Text = "Submit Report"
submitReportButton.TextColor3 = Color3I(255, 255, 255)
submitReportButton.ZIndex = baseZIndex + 2
submitReportButton.Parent = settingsFrame
submitReportButton.Parent = reportAbuseFrame
submitReportButton.MouseButton1Click:connect(function()
if submitReportButton.Active then
@ -2377,11 +2377,11 @@ local createReportAbuseDialog = function()
cancelButton.Text = "Cancel"
cancelButton.TextColor3 = Color3I(255, 255, 255)
cancelButton.ZIndex = baseZIndex + 2
cancelButton.Parent = settingsFrame
cancelButton.Parent = reportAbuseFrame
closeAndResetDialog = function()
--Delete old player combo box
local oldComboBox = settingsFrame:FindFirstChild "PlayersComboBox"
local oldComboBox = reportAbuseFrame:FindFirstChild "PlayersComboBox"
if oldComboBox then
oldComboBox.Parent = nil
end
@ -2404,7 +2404,7 @@ local createReportAbuseDialog = function()
cancelButton.MouseButton1Click:connect(closeAndResetDialog)
reportAbuseButton.MouseButton1Click:connect(function()
createPlayersDropDown().Parent = settingsFrame
createPlayersDropDown().Parent = reportAbuseFrame
table.insert(centerDialogs, shield)
game.GuiService:AddCenterDialog(
shield,

View File

@ -1,5 +1,5 @@
-- Start Game
print "[Mercury]: Loaded Host corescript"
-- Start Game Script Arguments
local InsertService = game:GetService "InsertService"
local BadgeService = game:GetService "BadgeService"
@ -15,57 +15,13 @@ local Visit = game:GetService "Visit"
local NetworkServer = game:GetService "NetworkServer"
-- StartGame --
-- pcall(function()
-- ScriptContext:AddStarterScript(injectScriptAssetID)
-- end)
RunService:Run()
-- REQUIRES: StartGanmeSharedArgs.txt
-- REQUIRES: MonitorGameStatus.txt
------------------- UTILITY FUNCTIONS --------------------------
local function waitForChild(parent, childName)
while true do
local child = parent:findFirstChild(childName)
if child then
return child
end
parent.ChildAdded:wait()
end
end
-- returns the player object that killed this humanoid
-- returns nil if the killer is no longer in the game
local function getKillerOfHumanoidIfStillInGame(humanoid)
-- check for kill tag on humanoid - may be more than one - todo: deal with this
local tag = humanoid:findFirstChild "creator"
-- find player with name on tag
if tag then
local killer = tag.Value
if killer.Parent then -- killer still in game
return killer
end
end
return nil
end
-- send kill and death stats when a player dies
local function onDied(victim, humanoid)
local killer = getKillerOfHumanoidIfStillInGame(humanoid)
local victorId = 0
if killer then
victorId = killer.userId
print(`STAT: kill by {victorId} of {victim.userId}`)
game:HttpGet(`{url}/Game/Knockouts.ashx?UserID={victorId}`)
end
print(`STAT: death of {victim.userId} by {victorId}`)
game:HttpGet(`{url}/Game/Wipeouts.ashx?UserID={victim.userId}`)
end
-----------------------------------END UTILITY FUNCTIONS -------------------------
local url = _BASE_URL
-----------------------------------"CUSTOM" SHARED CODE----------------------------------
@ -91,8 +47,6 @@ end)
-----------------------------------START GAME SHARED SCRIPT------------------------------
local url = "_BASE_URL"
-- pcall(function()
-- ScriptContext:AddStarterScript(libraryRegistrationScriptAssetID)
-- end)
@ -111,34 +65,7 @@ if url ~= nil then
pcall(function()
ContentProvider:SetBaseUrl(`{url}/`)
end)
-- pcall(function()
-- Players:SetChatFilterUrl(
-- `{url}/Game/ChatFilter.ashx`
-- )
-- end)
-- BadgeService:SetPlaceId(placeId)
-- if access ~= nil then
-- BadgeService:SetAwardBadgeUrl(
-- `{url}/Game/Badge/AwardBadge.ashx?UserID=%d&BadgeID=%d&PlaceID=%d&{access}`
-- )
-- BadgeService:SetHasBadgeUrl(
-- `{url}/Game/Badge/HasBadge.ashx?UserID=%d&BadgeID=%d&{access}`
-- )
-- BadgeService:SetIsBadgeDisabledUrl(
-- `{url}/Game/Badge/IsBadgeDisabled.ashx?BadgeID=%d&PlaceID=%d&{access}`
-- )
-- FriendService:SetMakeFriendUrl(
-- `{servicesUrl}/Friend/CreateFriend?firstUserId=%d&secondUserId=%d&{access}`
-- )
-- FriendService:SetBreakFriendUrl(
-- `{servicesUrl}/Friend/BreakFriend?firstUserId=%d&secondUserId=%d&{access}`
-- )
-- FriendService:SetGetFriendsUrl(
-- `{servicesUrl}/Friend/AreFriends?userId={access}`
-- )
-- end
BadgeService:SetIsBadgeLegalUrl ""
InsertService:SetBaseSetsUrl(
`{url}/Game/Tools/InsertAsset.ashx?nsets=10&type=base`
@ -149,18 +76,6 @@ if url ~= nil then
InsertService:SetCollectionUrl(`{url}/Game/Tools/InsertAsset.ashx?sid=%d`)
InsertService:SetAssetUrl(`{url}/asset?id=%d`)
InsertService:SetAssetVersionUrl(`{url}/Asset/?assetversionid=%d`)
-- pcall(function()
-- loadfile(`{url}/Game/LoadPlaceInfo.ashx?PlaceId={placeId}`)()
-- end)
-- pcall(function()
-- if access then
-- loadfile(
-- `{url}/Game/PlaceSpecificScript.ashx?PlaceId={placeId}&{access}`
-- )()
-- end
-- end)
end
pcall(function()
@ -168,72 +83,26 @@ pcall(function()
end)
settings().Diagnostics.LuaRamLimit = 0
if placeId ~= nil and killID ~= nil and deathID ~= nil and url ~= nil then
-- listen for the death of a Player
function createDeathMonitor(player)
-- we don't need to clean up old monitors or connections since the Character will be destroyed soon
if player.Character then
local humanoid = waitForChild(player.Character, "Humanoid")
humanoid.Died:connect(function()
onDied(player, humanoid)
end)
end
end
-- listen to all Players' Characters
Players.ChildAdded:connect(function(player)
createDeathMonitor(player)
player.Changed:connect(function(property)
if property == "Character" then
createDeathMonitor(player)
end
end)
end)
end
Players.PlayerAdded:connect(function(player)
print(`Player {player.userId} added`)
-- if url and access and placeId and player and player.userId then
-- game:HttpGet(
-- `{url}/Game/ClientPresence.ashx?action=connect&{access}&PlaceID={placeId}&UserID={player.userId}`
-- )
-- game:HttpGet(
-- `{url}/Game/PlaceVisit.ashx?UserID={player.userId}&AssociatedPlaceID={placeId}&{access}`
-- )
-- end
end)
Players.PlayerRemoving:connect(function(player)
print(`Player {player.userId} leaving`)
-- if url and access and placeId and player and player.userId then
-- game:HttpGet(
-- `{url}/Game/ClientPresence.ashx?action=disconnect&{access}&PlaceID={placeId}&UserID={player.userId}`
-- )
-- end
end)
-- if placeId ~= nil and url ~= nil then
-- -- yield so that file load happens in the heartbeat thread
-- wait()
-- -- load the game
-- game:Load(`{url}/asset/?id={placeId}`)
-- end
if _MAP_LOCATION_EXISTS then
-- yield so that file load happens in the heartbeat thread
wait()
-- load the game
game:Load "_MAP_LOCATION"
game:Load(_MAP_LOCATION)
end
-- Now start the connection
NetworkServer:Start(_SERVER_PORT)
Visit:SetPing("_SERVER_PRESENCE_URL", 30)
Visit:SetPing(_SERVER_PRESENCE_URL, 30)
-- if timeout then
-- ScriptContext:SetTimeout(timeout)

View File

@ -160,7 +160,7 @@ local function onConnectionAccepted(_, replicator)
local waitingForMarker = true
local success, err = pcall(function()
Visit:SetPing("_PING_URL", 30)
Visit:SetPing(_PING_URL, 30)
loadingState += 1
game:SetMessageBrickCount()
@ -257,7 +257,7 @@ local success, err = pcall(function()
pcall(function()
player.Name = [========[_USER_NAME]========]
end)
player.CharacterAppearance = "_CHAR_APPEARANCE"
player.CharacterAppearance = _CHAR_APPEARANCE
Visit:SetUploadUrl ""
end)