Compile Fusion library from corescripts/Libraries directory
This commit is contained in:
parent
6daece9dc0
commit
e603de10d1
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)",
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
--!strict
|
||||
|
||||
--[[
|
||||
An empty function. Often used as a destructor to indicate no destruction.
|
||||
]]
|
||||
|
||||
local function doNothing(...: any) end
|
||||
|
||||
return doNothing
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
3417
luau/10000001.luau
3417
luau/10000001.luau
File diff suppressed because it is too large
Load Diff
|
|
@ -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,
|
||||
|
|
|
|||
141
luau/host.luau
141
luau/host.luau
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue