diff --git a/Libraries/Fusion/Animation/Spring.luau b/Libraries/Fusion/Animation/Spring.luau new file mode 100644 index 0000000..5b0744f --- /dev/null +++ b/Libraries/Fusion/Animation/Spring.luau @@ -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( + goalState: PubTypes.Value, + speed: PubTypes.CanBeState?, + damping: PubTypes.CanBeState? +): Types.Spring + -- 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 diff --git a/Libraries/Fusion/Animation/SpringScheduler.luau b/Libraries/Fusion/Animation/SpringScheduler.luau new file mode 100644 index 0000000..86996ae --- /dev/null +++ b/Libraries/Fusion/Animation/SpringScheduler.luau @@ -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]: any } +type Spring = Types.Spring + +local SpringScheduler = {} + +local EPSILON = 0.0001 +local activeSprings: Set = {} +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 = {} + 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 diff --git a/Libraries/Fusion/Animation/Tween.luau b/Libraries/Fusion/Animation/Tween.luau new file mode 100644 index 0000000..5bd62a8 --- /dev/null +++ b/Libraries/Fusion/Animation/Tween.luau @@ -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( + goalState: PubTypes.StateObject, + tweenInfo: PubTypes.CanBeState? +): Types.Tween + 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 diff --git a/Libraries/Fusion/Animation/TweenScheduler.luau b/Libraries/Fusion/Animation/TweenScheduler.luau new file mode 100644 index 0000000..c822178 --- /dev/null +++ b/Libraries/Fusion/Animation/TweenScheduler.luau @@ -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]: any } +type Tween = Types.Tween + +local WEAK_KEYS_METATABLE = { __mode = "k" } + +-- all the tweens currently being updated +local allTweens: Set = {} +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 diff --git a/Libraries/Fusion/Animation/getTweenRatio.luau b/Libraries/Fusion/Animation/getTweenRatio.luau new file mode 100644 index 0000000..8ec5aac --- /dev/null +++ b/Libraries/Fusion/Animation/getTweenRatio.luau @@ -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 diff --git a/Libraries/Fusion/Animation/lerpType.luau b/Libraries/Fusion/Animation/lerpType.luau new file mode 100644 index 0000000..5067588 --- /dev/null +++ b/Libraries/Fusion/Animation/lerpType.luau @@ -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 diff --git a/Libraries/Fusion/Animation/packType.luau b/Libraries/Fusion/Animation/packType.luau new file mode 100644 index 0000000..a48e2c3 --- /dev/null +++ b/Libraries/Fusion/Animation/packType.luau @@ -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 diff --git a/Libraries/Fusion/Animation/springCoefficients.luau b/Libraries/Fusion/Animation/springCoefficients.luau new file mode 100644 index 0000000..e49b60c --- /dev/null +++ b/Libraries/Fusion/Animation/springCoefficients.luau @@ -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 diff --git a/Libraries/Fusion/Animation/unpackType.luau b/Libraries/Fusion/Animation/unpackType.luau new file mode 100644 index 0000000..770e8f8 --- /dev/null +++ b/Libraries/Fusion/Animation/unpackType.luau @@ -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 diff --git a/Libraries/Fusion/Colour/Oklab.luau b/Libraries/Fusion/Colour/Oklab.luau new file mode 100644 index 0000000..46fb01e --- /dev/null +++ b/Libraries/Fusion/Colour/Oklab.luau @@ -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 diff --git a/Libraries/Fusion/External.luau b/Libraries/Fusion/External.luau new file mode 100644 index 0000000..d620f8c --- /dev/null +++ b/Libraries/Fusion/External.luau @@ -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 diff --git a/Libraries/Fusion/Instances/Children.luau b/Libraries/Fusion/Instances/Children.luau new file mode 100644 index 0000000..68d32aa --- /dev/null +++ b/Libraries/Fusion/Instances/Children.luau @@ -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]: 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 = {} + local oldParented: Set = {} + + -- save disconnection functions for state object observers + local newDisconnects: { [PubTypes.StateObject]: () -> () } = {} + local oldDisconnects: { [PubTypes.StateObject]: () -> () } = {} + + 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 diff --git a/Libraries/Fusion/Instances/Cleanup.luau b/Libraries/Fusion/Instances/Cleanup.luau new file mode 100644 index 0000000..0f676aa --- /dev/null +++ b/Libraries/Fusion/Instances/Cleanup.luau @@ -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 diff --git a/Libraries/Fusion/Instances/Hydrate.luau b/Libraries/Fusion/Instances/Hydrate.luau new file mode 100644 index 0000000..79b53ce --- /dev/null +++ b/Libraries/Fusion/Instances/Hydrate.luau @@ -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 diff --git a/Libraries/Fusion/Instances/New.luau b/Libraries/Fusion/Instances/New.luau new file mode 100644 index 0000000..3b1502e --- /dev/null +++ b/Libraries/Fusion/Instances/New.luau @@ -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 diff --git a/Libraries/Fusion/Instances/OnChange.luau b/Libraries/Fusion/Instances/OnChange.luau new file mode 100644 index 0000000..1960848 --- /dev/null +++ b/Libraries/Fusion/Instances/OnChange.luau @@ -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 diff --git a/Libraries/Fusion/Instances/OnEvent.luau b/Libraries/Fusion/Instances/OnEvent.luau new file mode 100644 index 0000000..ecd54b2 --- /dev/null +++ b/Libraries/Fusion/Instances/OnEvent.luau @@ -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 diff --git a/Libraries/Fusion/Instances/Out.luau b/Libraries/Fusion/Instances/Out.luau new file mode 100644 index 0000000..d41df31 --- /dev/null +++ b/Libraries/Fusion/Instances/Out.luau @@ -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 diff --git a/Libraries/Fusion/Instances/Ref.luau b/Libraries/Fusion/Instances/Ref.luau new file mode 100644 index 0000000..0a033a7 --- /dev/null +++ b/Libraries/Fusion/Instances/Ref.luau @@ -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 diff --git a/Libraries/Fusion/Instances/applyInstanceProps.luau b/Libraries/Fusion/Instances/applyInstanceProps.luau new file mode 100644 index 0000000..0e554bd --- /dev/null +++ b/Libraries/Fusion/Instances/applyInstanceProps.luau @@ -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, + 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 diff --git a/Libraries/Fusion/Instances/defaultProps.luau b/Libraries/Fusion/Instances/defaultProps.luau new file mode 100644 index 0000000..10f6e98 --- /dev/null +++ b/Libraries/Fusion/Instances/defaultProps.luau @@ -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, + }, +} diff --git a/Libraries/Fusion/Logging/logError.luau b/Libraries/Fusion/Logging/logError.luau new file mode 100644 index 0000000..49f7323 --- /dev/null +++ b/Libraries/Fusion/Logging/logError.luau @@ -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 diff --git a/Libraries/Fusion/Logging/logErrorNonFatal.luau b/Libraries/Fusion/Logging/logErrorNonFatal.luau new file mode 100644 index 0000000..9d59733 --- /dev/null +++ b/Libraries/Fusion/Logging/logErrorNonFatal.luau @@ -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 diff --git a/Libraries/Fusion/Logging/logWarn.luau b/Libraries/Fusion/Logging/logWarn.luau new file mode 100644 index 0000000..ee6f5d6 --- /dev/null +++ b/Libraries/Fusion/Logging/logWarn.luau @@ -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 diff --git a/Libraries/Fusion/Logging/messages.luau b/Libraries/Fusion/Logging/messages.luau new file mode 100644 index 0000000..0a8ebea --- /dev/null +++ b/Libraries/Fusion/Logging/messages.luau @@ -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)", +} diff --git a/Libraries/Fusion/Logging/parseError.luau b/Libraries/Fusion/Logging/parseError.luau new file mode 100644 index 0000000..366f6b5 --- /dev/null +++ b/Libraries/Fusion/Logging/parseError.luau @@ -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 diff --git a/Libraries/Fusion/MercuryExternal.luau b/Libraries/Fusion/MercuryExternal.luau new file mode 100644 index 0000000..b6e9b4b --- /dev/null +++ b/Libraries/Fusion/MercuryExternal.luau @@ -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 diff --git a/Libraries/Fusion/Polyfill/TweenInfo.luau b/Libraries/Fusion/Polyfill/TweenInfo.luau new file mode 100644 index 0000000..1ffb808 --- /dev/null +++ b/Libraries/Fusion/Polyfill/TweenInfo.luau @@ -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 diff --git a/Libraries/Fusion/Polyfill/easing.luau b/Libraries/Fusion/Polyfill/easing.luau new file mode 100644 index 0000000..33a1ce7 --- /dev/null +++ b/Libraries/Fusion/Polyfill/easing.luau @@ -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 diff --git a/Libraries/Fusion/Polyfill/typeof.luau b/Libraries/Fusion/Polyfill/typeof.luau new file mode 100644 index 0000000..170ab2c --- /dev/null +++ b/Libraries/Fusion/Polyfill/typeof.luau @@ -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 diff --git a/Libraries/Fusion/PubTypes.luau b/Libraries/Fusion/PubTypes.luau new file mode 100644 index 0000000..6a1a3ff --- /dev/null +++ b/Libraries/Fusion/PubTypes.luau @@ -0,0 +1,162 @@ +--!strict + +--[[ + Stores common public-facing type information for Fusion APIs. +]] + +type Set = { [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 = { + type: "Contextual", + now: (Contextual) -> T, + is: (Contextual, T) -> ContextualIsMethods, +} + +type ContextualIsMethods = { + during: (ContextualIsMethods, (A...) -> T, A...) -> T, +} + +--[[ + Generic reactive graph types +]] + +-- A graph object which can have dependents. +export type Dependency = { + dependentSet: Set, +} + +-- A graph object which can have dependencies. +export type Dependent = { + update: (Dependent) -> boolean, + dependencySet: Set, +} + +-- An object which stores a piece of reactive state. +export type StateObject = Dependency & { + type: "State", + kind: string, + _typeIdentifier: T, +} + +-- Either a constant value of type T, or a state object containing type T. +export type CanBeState = StateObject | T + +-- Function signature for use callbacks. +export type Use = (target: CanBeState) -> T + +--[[ + Specific reactive graph types +]] + +-- A state object whose value can be set at any time by the user. +export type Value = StateObject & { + kind: "State", + set: (Value, newValue: any, force: boolean?) -> (), +} + +-- A state object whose value is derived from other objects using a callback. +export type Computed = StateObject & Dependent & { + kind: "Computed", +} + +-- A state object whose value is derived from other objects using a callback. +export type ForPairs = StateObject<{ [KO]: VO }> & Dependent & { + kind: "ForPairs", +} +-- A state object whose value is derived from other objects using a callback. +export type ForKeys = StateObject<{ [KO]: V }> & Dependent & { + kind: "ForKeys", +} +-- A state object whose value is derived from other objects using a callback. +export type ForValues = StateObject<{ [K]: VO }> & Dependent & { + kind: "ForKeys", +} + +-- A state object which follows another state object using tweens. +export type Tween = StateObject & Dependent & { + kind: "Tween", +} + +-- A state object which follows another state object using spring simulation. +export type Spring = StateObject & Dependent & { + kind: "Spring", + setPosition: (Spring, newPosition: Animatable) -> (), + setVelocity: (Spring, newVelocity: Animatable) -> (), + addVelocity: (Spring, 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 | { [any]: Children } + +-- A table that defines an instance's properties, handlers and children. +export type PropertyTable = { [string | SpecialKey]: any } + +return nil diff --git a/Libraries/Fusion/State/Computed.luau b/Libraries/Fusion/State/Computed.luau new file mode 100644 index 0000000..ced3fd6 --- /dev/null +++ b/Libraries/Fusion/State/Computed.luau @@ -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( + processor: () -> T, + destructor: ((T) -> ())? +): Types.Computed + 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 diff --git a/Libraries/Fusion/State/ForKeys.luau b/Libraries/Fusion/State/ForKeys.luau new file mode 100644 index 0000000..98087d6 --- /dev/null +++ b/Libraries/Fusion/State/ForKeys.luau @@ -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( + inputTable: PubTypes.CanBeState<{ [KI]: any }>, + processor: (KI) -> (KO, M?), + destructor: (KO, M?) -> ()? +): Types.ForKeys + 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 diff --git a/Libraries/Fusion/State/ForPairs.luau b/Libraries/Fusion/State/ForPairs.luau new file mode 100644 index 0000000..03ef607 --- /dev/null +++ b/Libraries/Fusion/State/ForPairs.luau @@ -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( + inputTable: PubTypes.CanBeState<{ [KI]: VI }>, + processor: (KI, VI) -> (KO, VO, M?), + destructor: (KO, VO, M?) -> ()? +): Types.ForPairs + 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 diff --git a/Libraries/Fusion/State/ForValues.luau b/Libraries/Fusion/State/ForValues.luau new file mode 100644 index 0000000..036101c --- /dev/null +++ b/Libraries/Fusion/State/ForValues.luau @@ -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( + inputTable: PubTypes.CanBeState<{ [any]: VI }>, + processor: (VI) -> (VO, M?), + destructor: (VO, M?) -> ()? +): Types.ForValues + 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 diff --git a/Libraries/Fusion/State/Observer.luau b/Libraries/Fusion/State/Observer.luau new file mode 100644 index 0000000..f457be2 --- /dev/null +++ b/Libraries/Fusion/State/Observer.luau @@ -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]: any } + +local class = {} +local CLASS_METATABLE = { __index = class } + +-- Table used to hold Observer objects in memory. +local strongRefs: Set = {} + +--[[ + 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): 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 diff --git a/Libraries/Fusion/State/Value.luau b/Libraries/Fusion/State/Value.luau new file mode 100644 index 0000000..01da6b1 --- /dev/null +++ b/Libraries/Fusion/State/Value.luau @@ -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(initialValue: T): Types.State + 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 diff --git a/Libraries/Fusion/State/isState.luau b/Libraries/Fusion/State/isState.luau new file mode 100644 index 0000000..45c5a35 --- /dev/null +++ b/Libraries/Fusion/State/isState.luau @@ -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 diff --git a/Libraries/Fusion/State/makeUseCallback.luau b/Libraries/Fusion/State/makeUseCallback.luau new file mode 100644 index 0000000..3120430 --- /dev/null +++ b/Libraries/Fusion/State/makeUseCallback.luau @@ -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]: any } + +local function makeUseCallback(dependencySet: Set) + local function use(target: PubTypes.CanBeState): T + if isState(target) then + dependencySet[target] = true + return (target :: Types.StateObject):_peek() + end + return target + end + return use +end + +return makeUseCallback diff --git a/Libraries/Fusion/State/peek.luau b/Libraries/Fusion/State/peek.luau new file mode 100644 index 0000000..2fd67ab --- /dev/null +++ b/Libraries/Fusion/State/peek.luau @@ -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(target: PubTypes.CanBeState): T + if isState(target) then + return (target :: Types.StateObject):_peek() + end + return target +end + +return peek diff --git a/Libraries/Fusion/State/updateAll.luau b/Libraries/Fusion/State/updateAll.luau new file mode 100644 index 0000000..865e649 --- /dev/null +++ b/Libraries/Fusion/State/updateAll.luau @@ -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]: 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 diff --git a/Libraries/Fusion/Types.luau b/Libraries/Fusion/Types.luau new file mode 100644 index 0000000..8470b30 --- /dev/null +++ b/Libraries/Fusion/Types.luau @@ -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]: 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 = PubTypes.Contextual & { + _valuesNow: { [thread]: { value: T } }, + _defaultValue: T, +} + +--[[ + Generic reactive graph types +]] + +export type StateObject = PubTypes.StateObject & { + _peek: (StateObject) -> T, +} + +--[[ + Specific reactive graph types +]] + +-- A state object whose value can be set at any time by the user. +export type State = PubTypes.Value & { + _value: T, +} + +-- A state object whose value is derived from other objects using a callback. +export type Computed = PubTypes.Computed & { + _oldDependencySet: Set, + _callback: (PubTypes.Use) -> T, + _value: T, +} + +-- A state object whose value is derived from other objects using a callback. +export type ForPairs = PubTypes.ForPairs & { + _oldDependencySet: Set, + _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, + oldDependencySet: Set, + dependencyValues: { [PubTypes.Dependency]: any }, + }, + }, +} + +-- A state object whose value is derived from other objects using a callback. +export type ForKeys = PubTypes.ForKeys & { + _oldDependencySet: Set, + _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, + oldDependencySet: Set, + dependencyValues: { [PubTypes.Dependency]: any }, + }, + }, +} + +-- A state object whose value is derived from other objects using a callback. +export type ForValues = PubTypes.ForValues & { + _oldDependencySet: Set, + _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, + oldDependencySet: Set, + dependencyValues: { [PubTypes.Dependency]: any }, + }, + }, +} + +-- A state object which follows another state object using tweens. +export type Tween = PubTypes.Tween & { + _goalState: State, + _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 = PubTypes.Spring & { + _speed: PubTypes.CanBeState, + _speedIsState: boolean, + _lastSpeed: number, + _damping: PubTypes.CanBeState, + _dampingIsState: boolean, + _lastDamping: number, + _goalState: State, + _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 diff --git a/Libraries/Fusion/Utility/Contextual.luau b/Libraries/Fusion/Utility/Contextual.luau new file mode 100644 index 0000000..193df50 --- /dev/null +++ b/Libraries/Fusion/Utility/Contextual.luau @@ -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(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(defaultValue: T): Types.Contextual + 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 diff --git a/Libraries/Fusion/Utility/None.luau b/Libraries/Fusion/Utility/None.luau new file mode 100644 index 0000000..6a40a49 --- /dev/null +++ b/Libraries/Fusion/Utility/None.luau @@ -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 diff --git a/Libraries/Fusion/Utility/cleanup.luau b/Libraries/Fusion/Utility/cleanup.luau new file mode 100644 index 0000000..b67c299 --- /dev/null +++ b/Libraries/Fusion/Utility/cleanup.luau @@ -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 diff --git a/Libraries/Fusion/Utility/doNothing.luau b/Libraries/Fusion/Utility/doNothing.luau new file mode 100644 index 0000000..b2cd5bc --- /dev/null +++ b/Libraries/Fusion/Utility/doNothing.luau @@ -0,0 +1,9 @@ +--!strict + +--[[ + An empty function. Often used as a destructor to indicate no destruction. +]] + +local function doNothing(...: any) end + +return doNothing diff --git a/Libraries/Fusion/Utility/isSimilar.luau b/Libraries/Fusion/Utility/isSimilar.luau new file mode 100644 index 0000000..ac93baf --- /dev/null +++ b/Libraries/Fusion/Utility/isSimilar.luau @@ -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 diff --git a/Libraries/Fusion/Utility/needsDestruction.luau b/Libraries/Fusion/Utility/needsDestruction.luau new file mode 100644 index 0000000..1ee6eb0 --- /dev/null +++ b/Libraries/Fusion/Utility/needsDestruction.luau @@ -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 diff --git a/Libraries/Fusion/Utility/restrictRead.luau b/Libraries/Fusion/Utility/restrictRead.luau new file mode 100644 index 0000000..9a279cc --- /dev/null +++ b/Libraries/Fusion/Utility/restrictRead.luau @@ -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 diff --git a/Libraries/Fusion/Utility/xtypeof.luau b/Libraries/Fusion/Utility/xtypeof.luau new file mode 100644 index 0000000..19187f1 --- /dev/null +++ b/Libraries/Fusion/Utility/xtypeof.luau @@ -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 diff --git a/Libraries/Fusion/init.luau b/Libraries/Fusion/init.luau new file mode 100644 index 0000000..43ac0e8 --- /dev/null +++ b/Libraries/Fusion/init.luau @@ -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 = PubTypes.StateObject +export type CanBeState = PubTypes.CanBeState +export type Symbol = PubTypes.Symbol +export type Value = PubTypes.Value +export type Computed = PubTypes.Computed +export type ForPairs = PubTypes.ForPairs +export type ForKeys = PubTypes.ForKeys +export type ForValues = PubTypes.ForKeys +export type Observer = PubTypes.Observer +export type Tween = PubTypes.Tween +export type Spring = PubTypes.Spring +export type Use = PubTypes.Use +export type Contextual = PubTypes.Contextual + +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: (initialValue: T) -> Value, + Computed: (callback: (Use) -> T, destructor: (T) -> ()?) -> Computed, + ForPairs: ( + inputTable: CanBeState<{ [KI]: VI }>, + processor: (Use, KI, VI) -> (KO, VO, M?), + destructor: (KO, VO, M?) -> ()? + ) -> ForPairs, + ForKeys: ( + inputTable: CanBeState<{ [KI]: any }>, + processor: (Use, KI) -> (KO, M?), + destructor: (KO, M?) -> ()? + ) -> ForKeys, + ForValues: ( + inputTable: CanBeState<{ [any]: VI }>, + processor: (Use, VI) -> (VO, M?), + destructor: (VO, M?) -> ()? + ) -> ForValues, + Observer: (watchedState: StateObject) -> Observer, + + Tween: (goalState: StateObject, tweenInfo: TweenInfo?) -> Tween, + Spring: ( + goalState: StateObject, + speed: CanBeState?, + damping: CanBeState? + ) -> Spring, + + Contextual: (defaultValue: T) -> Contextual, + cleanup: (...any) -> (), + doNothing: (...any) -> (), + peek: Use, +} + +return Fusion diff --git a/README.md b/README.md index 1f1a387..43cdb7d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/compile.sh b/compile.sh index bfd0355..dd505b5 100644 --- a/compile.sh +++ b/compile.sh @@ -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}") diff --git a/luau/10000001.luau b/luau/10000001.luau deleted file mode 100644 index 1c6cc5a..0000000 --- a/luau/10000001.luau +++ /dev/null @@ -1,3417 +0,0 @@ -local __DARKLUA_BUNDLE_MODULES = {} - -do - __DARKLUA_BUNDLE_MODULES.c = { - cannotAssignProperty = "The class type '%s' has no assignable property '%s'.", - cannotConnectChange = "The %s class doesn't have a property 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", - 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.]], - 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.", - invalidOutProperty = "The %s class doesn't have a property 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)]], - } -end -do - local messages = __DARKLUA_BUNDLE_MODULES.c - - local function logError(messageID, errObj, ...) - local formatString - - 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 - - __DARKLUA_BUNDLE_MODULES.d = logError -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local External = {} - local updateStepCallbacks = {} - local currentScheduler = nil - local lastUpdateStep = 0 - - function External.setExternalScheduler(newScheduler) - local oldScheduler = currentScheduler - - if oldScheduler ~= nil then - oldScheduler.stopScheduler() - end - - currentScheduler = newScheduler - - if newScheduler ~= nil then - newScheduler.startScheduler() - end - - return oldScheduler - end - function External.doTaskImmediate(resume) - if currentScheduler == nil then - logError "noTaskScheduler" - else - currentScheduler.doTaskImmediate(resume) - end - end - function External.doTaskDeferred(resume) - if currentScheduler == nil then - logError "noTaskScheduler" - else - currentScheduler.doTaskDeferred(resume) - end - end - function External.bindToUpdateStep(callback) - local uniqueIdentifier = {} - - updateStepCallbacks[uniqueIdentifier] = callback - - return function() - updateStepCallbacks[uniqueIdentifier] = nil - end - end - function External.performUpdateStep(now) - lastUpdateStep = now - - for _, callback in pairs(updateStepCallbacks) do - callback(now) - end - end - function External.lastUpdateStep() - return lastUpdateStep - end - - __DARKLUA_BUNDLE_MODULES.e = External -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - - local function restrictRead(tableName, strictTable) - local metatable = getmetatable(strictTable) - - if metatable == nil then - metatable = {} - - setmetatable(strictTable, metatable) - end - - function metatable:__index(memberName) - logError("strictReadError", nil, tostring(memberName), tableName) - end - - return strictTable - end - - __DARKLUA_BUNDLE_MODULES.f = restrictRead -end -do - local RunService = game:GetService "RunService" - local External = __DARKLUA_BUNDLE_MODULES.e - local MercuryExternal = {} - - function MercuryExternal.doTaskImmediate(resume) - Spawn(resume) - end - function MercuryExternal.doTaskDeferred(resume) - coroutine.resume(coroutine.create(resume)) - end - - local function performUpdateStep() - External.performUpdateStep(tick()) - end - - local stopSchedulerFunc = nil - - function MercuryExternal.startScheduler() - if stopSchedulerFunc ~= nil then - return - end - - local conn = RunService.RenderStepped:connect(performUpdateStep) - - stopSchedulerFunc = function() - conn:disconnect() - end - end - function MercuryExternal.stopScheduler() - if stopSchedulerFunc ~= nil then - stopSchedulerFunc() - - stopSchedulerFunc = nil - end - end - - __DARKLUA_BUNDLE_MODULES.g = MercuryExternal -end -do - __DARKLUA_BUNDLE_MODULES.h = { - 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, - }, - } -end -do - __DARKLUA_BUNDLE_MODULES.i = 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 - - local tests = { - { - "Instance", - { - "ClassName", - }, - }, - { - "EnumItem", - { - "EnumType", - "Name", - "Value", - }, - }, - { - "Enum", - { - "GetEnumItems", - }, - }, - { - "Enums", - { - "MembershipType", - }, - }, - { - "RBXScriptSignal", - { - "connect", - "wait", - }, - }, - { - "RBXScriptConnection", - { - "connected", - "disconnect", - }, - }, - { - "TweenInfo", - { - "EasingDirection", - "RepeatCount", - "EasingStyle", - }, - }, - { - "CFrame", - { - "p", - "x", - "y", - "z", - "lookVector", - }, - }, - { - "Vector3", - { - "Lerp", - "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", - }, - }, - } - - 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 - end - - return true - end) - - if ok and result then - return t - end - end - end -end -do - local typeof = __DARKLUA_BUNDLE_MODULES.i - - local function cleanupOne(task) - local taskType = typeof(task) - - if taskType == "Instance" then - task:Destroy() - elseif taskType == "RBXScriptConnection" then - task:disconnect() - elseif taskType == "function" then - task() - elseif taskType == "table" then - if type(task.destroy) == "function" then - task:destroy() - elseif type(task.Destroy) == "function" then - task:Destroy() - elseif task[1] ~= nil then - for _, subtask in ipairs(task) do - cleanupOne(subtask) - end - end - end - end - local function cleanup(...) - for index = 1, select("#", ...) do - cleanupOne(select(index, ...)) - end - end - - __DARKLUA_BUNDLE_MODULES.j = cleanup -end -do - local typeof = __DARKLUA_BUNDLE_MODULES.i - - local function xtypeof(x) - local typeString = typeof(x) - - if typeString == "table" and type(x.type) == "string" then - return x.type - else - return typeString - end - end - - __DARKLUA_BUNDLE_MODULES.k = xtypeof -end -do - local External = __DARKLUA_BUNDLE_MODULES.e - local class = {} - local CLASS_METATABLE = { __index = class } - local strongRefs = {} - - function class:update() - for _, callback in pairs(self._changeListeners) do - External.doTaskImmediate(callback) - end - - return false - end - function class:onChange(callback) - local uniqueIdentifier = {} - - self._numChangeListeners = self._numChangeListeners + 1 - self._changeListeners[uniqueIdentifier] = callback - strongRefs[self] = true - - local disconnected = false - - return function() - if disconnected then - return - end - - disconnected = true - self._changeListeners[uniqueIdentifier] = nil - self._numChangeListeners = self._numChangeListeners - 1 - - if self._numChangeListeners == 0 then - strongRefs[self] = nil - end - end - end - function class:onBind(callback) - External.doTaskImmediate(callback) - - return self:onChange(callback) - end - - local function Observer(watchedState) - local self = setmetatable({ - type = "State", - kind = "Observer", - dependencySet = { [watchedState] = true }, - dependentSet = {}, - _changeListeners = {}, - _numChangeListeners = 0, - }, CLASS_METATABLE) - - watchedState.dependentSet[self] = true - - return self - end - - __DARKLUA_BUNDLE_MODULES.l = Observer -end -do - local function isState(target) - return type(target) == "table" and type(target._peek) == "function" - end - - __DARKLUA_BUNDLE_MODULES.m = isState -end -do - local isState = __DARKLUA_BUNDLE_MODULES.m - - local function peek(target) - if isState(target) then - return (target):_peek() - end - - return target - end - - __DARKLUA_BUNDLE_MODULES.n = peek -end -do - local External = __DARKLUA_BUNDLE_MODULES.e - local cleanup = __DARKLUA_BUNDLE_MODULES.j - local xtypeof = __DARKLUA_BUNDLE_MODULES.k - local logError = __DARKLUA_BUNDLE_MODULES.d - local Observer = __DARKLUA_BUNDLE_MODULES.l - local peek = __DARKLUA_BUNDLE_MODULES.n - local typeof = __DARKLUA_BUNDLE_MODULES.i - - local function setProperty_unsafe(instance, property, value) - (instance)[property] = value - end - local function testPropertyAssignable(instance, property) - (instance)[property] = (instance)[property] - end - local function setProperty(instance, property, value) - if not pcall(setProperty_unsafe, instance, property, value) then - if not pcall(testPropertyAssignable, instance, property) then - if instance == nil then - logError( - "setPropertyNilRef", - nil, - property, - tostring(value) - ) - else - logError( - "cannotAssignProperty", - nil, - instance.ClassName, - property - ) - end - else - local givenType = typeof(value) - local expectedType = typeof((instance)[property]) - - logError( - "invalidPropertyType", - nil, - instance.ClassName, - property, - expectedType, - givenType - ) - end - end - end - local function bindProperty(instance, property, value, cleanupTasks) - if xtypeof(value) == "State" then - 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):onChange(updateLater)) - else - setProperty(instance, property, value) - end - end - local function applyInstanceProps(props, applyTo) - local specialKeys = { - self = {}, - descendants = {}, - ancestor = {}, - observer = {}, - } - local cleanupTasks = {} - - for key, value in pairs(props) do - local keyType = xtypeof(key) - - if keyType == "string" then - if key ~= "Parent" then - bindProperty(applyTo, key, value, cleanupTasks) - end - elseif keyType == "SpecialKey" then - local stage = (key).stage - local keys = specialKeys[stage] - - if keys == nil then - logError("unrecognisedPropertyStage", nil, stage) - else - keys[key] = value - end - else - 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 - - if applyTo.Parent then - game.DescendantRemoving:connect(function(descendant) - if descendant == applyTo then - cleanup(cleanupTasks) - end - end) - end - end - - __DARKLUA_BUNDLE_MODULES.o = applyInstanceProps -end -do - local defaultProps = __DARKLUA_BUNDLE_MODULES.h - local applyInstanceProps = __DARKLUA_BUNDLE_MODULES.o - local logError = __DARKLUA_BUNDLE_MODULES.d - - local function New(className) - return function(props) - 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 - - __DARKLUA_BUNDLE_MODULES.p = New -end -do - local applyInstanceProps = __DARKLUA_BUNDLE_MODULES.o - - local function Hydrate(target) - return function(props) - applyInstanceProps(props, target) - - return target - end - end - - __DARKLUA_BUNDLE_MODULES.q = Hydrate -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local xtypeof = __DARKLUA_BUNDLE_MODULES.k - local Ref = {} - - Ref.type = "SpecialKey" - Ref.kind = "Ref" - Ref.stage = "observer" - - function Ref:apply(refState, applyTo, cleanupTasks) - 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 - - __DARKLUA_BUNDLE_MODULES.r = Ref -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local xtypeof = __DARKLUA_BUNDLE_MODULES.k - - local function Out(propertyName) - local outKey = {} - - outKey.type = "SpecialKey" - outKey.kind = "Out" - outKey.stage = "observer" - - function outKey:apply(outState, applyTo, cleanupTasks) - 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)[propertyName]) - table.insert( - cleanupTasks, - event:connect(function(prop) - if prop == propertyName then - outState:set((applyTo)[propertyName]) - end - end) - ) - table.insert(cleanupTasks, function() - outState:set(nil) - end) - end - end - - return outKey - end - - __DARKLUA_BUNDLE_MODULES.s = Out -end -do - local Cleanup = {} - - Cleanup.type = "SpecialKey" - Cleanup.kind = "Cleanup" - Cleanup.stage = "observer" - - function Cleanup:apply(userTask, _, cleanupTasks) - table.insert(cleanupTasks, userTask) - end - - __DARKLUA_BUNDLE_MODULES.t = Cleanup -end -do - local messages = __DARKLUA_BUNDLE_MODULES.c - - local function logWarn(messageID, ...) - local formatString - - if messages[messageID] ~= nil then - formatString = messages[messageID] - else - messageID = "unknownMessage" - formatString = messages[messageID] - end - - warn( - string.format( - "[Fusion] " .. formatString .. "\n(ID: " .. messageID .. ")", - ... - ) - ) - end - - __DARKLUA_BUNDLE_MODULES.u = logWarn -end -do - local External = __DARKLUA_BUNDLE_MODULES.e - local logWarn = __DARKLUA_BUNDLE_MODULES.u - local Observer = __DARKLUA_BUNDLE_MODULES.l - local peek = __DARKLUA_BUNDLE_MODULES.n - local isState = __DARKLUA_BUNDLE_MODULES.m - local typeof = __DARKLUA_BUNDLE_MODULES.i - local EXPERIMENTAL_AUTO_NAMING = false - local Children = {} - - Children.type = "SpecialKey" - Children.kind = "Children" - Children.stage = "descendants" - - function Children:apply(propValue, applyTo, cleanupTasks) - local newParented = {} - local oldParented = {} - local newDisconnects = {} - local oldDisconnects = {} - local updateQueued = false - local queueUpdate - - local function updateChildren() - if not updateQueued then - return - end - - updateQueued = false - oldParented, newParented = newParented, oldParented - oldDisconnects, newDisconnects = newDisconnects, oldDisconnects - - for i, _ in pairs(newParented) do - newParented[i] = nil - end - for i, _ in pairs(newDisconnects) do - newDisconnects[i] = nil - end - - local function processChild(child, autoName) - local childType = typeof(child) - - if childType == "Instance" then - newParented[child] = true - - if oldParented[child] == nil then - child.Parent = applyTo - else - oldParented[child] = nil - end - if EXPERIMENTAL_AUTO_NAMING and autoName ~= nil then - child.Name = autoName - end - elseif isState(child) then - local value = peek(child) - - if value ~= nil then - processChild(value, autoName) - end - - local disconnect = oldDisconnects[child] - - if disconnect == nil then - disconnect = Observer(child):onChange(queueUpdate) - else - oldDisconnects[child] = nil - end - - newDisconnects[child] = disconnect - elseif childType == "table" then - for key, subChild in pairs(child) do - local keyType = typeof(key) - local subAutoName = 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 - processChild(propValue) - end - - for oldInstance in pairs(oldParented) do - oldInstance.Parent = nil - end - 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) - - updateQueued = true - - updateChildren() - end - - __DARKLUA_BUNDLE_MODULES.v = Children -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local typeof = __DARKLUA_BUNDLE_MODULES.i - - local function getProperty_unsafe(instance, property) - return (instance)[property] - end - local function OnEvent(eventName) - local eventKey = {} - - eventKey.type = "SpecialKey" - eventKey.kind = "OnEvent" - eventKey.stage = "observer" - - function eventKey:apply(callback, applyTo, cleanupTasks) - 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 - - __DARKLUA_BUNDLE_MODULES.w = OnEvent -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local typeof = __DARKLUA_BUNDLE_MODULES.i - - local function OnChange(propertyName) - local changeKey = {} - - changeKey.type = "SpecialKey" - changeKey.kind = "OnChange" - changeKey.stage = "observer" - - function changeKey:apply(callback, applyTo, cleanupTasks) - 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)[propertyName]) - end - end) - ) - end - end - - return changeKey - end - - __DARKLUA_BUNDLE_MODULES.x = OnChange -end -do - local function updateAll(root) - local counters = {} - local flags = {} - local queue = {} - local queueSize = 0 - local queuePos = 1 - - for object in pairs(root.dependentSet) do - queueSize = queueSize + 1 - queue[queueSize] = object - flags[object] = true - end - - while queuePos <= queueSize do - local next = queue[queuePos] - local counter = counters[next] - - counters[next] = (function() - if counter == nil then - return 1 - else - return counter + 1 - end - end)() - - if next.dependentSet ~= nil then - for object in pairs(next.dependentSet) do - queueSize = queueSize + 1 - queue[queueSize] = object - end - end - - queuePos = queuePos + 1 - end - - 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 = queuePos + 1 - end - end - - __DARKLUA_BUNDLE_MODULES.y = updateAll -end -do - local function isSimilar(a, b) - if type(a) == "table" then - return false - end - - return a == b - end - - __DARKLUA_BUNDLE_MODULES.z = isSimilar -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local updateAll = __DARKLUA_BUNDLE_MODULES.y - local isSimilar = __DARKLUA_BUNDLE_MODULES.z - local class = {} - local CLASS_METATABLE = { __index = class } - local WEAK_KEYS_METATABLE = { - __mode = "k", - } - - function class:set(newValue, force) - local oldValue = self._value - - if force or not isSimilar(oldValue, newValue) then - self._value = newValue - - updateAll(self) - end - end - function class:_peek() - return self._value - end - function class:get() - logError "stateGetWasRemoved" - end - - local function Value(initialValue) - local self = setmetatable({ - type = "State", - kind = "Value", - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), - _value = initialValue, - }, CLASS_METATABLE) - - return self - end - - __DARKLUA_BUNDLE_MODULES.A = Value -end -do - local messages = __DARKLUA_BUNDLE_MODULES.c - - local function logErrorNonFatal(messageID, errObj, ...) - local formatString - - 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 - - __DARKLUA_BUNDLE_MODULES.B = logErrorNonFatal -end -do - local function parseError(err) - local trace = "Traceback not available" - - if debug and debug.traceback then - trace = debug.traceback(nil, 2) - end - - return { - type = "Error", - raw = err, - message = err:gsub("^.+:%d+:%s*", ""), - trace = trace, - } - end - - __DARKLUA_BUNDLE_MODULES.C = parseError -end -do - local typeof = __DARKLUA_BUNDLE_MODULES.i - - local function needsDestruction(x) - return typeof(x) == "Instance" - end - - __DARKLUA_BUNDLE_MODULES.D = needsDestruction -end -do - local isState = __DARKLUA_BUNDLE_MODULES.m - - local function makeUseCallback(dependencySet) - local function use(target) - if isState(target) then - dependencySet[target] = true - - return (target):_peek() - end - - return target - end - - return use - end - - __DARKLUA_BUNDLE_MODULES.E = makeUseCallback -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local logErrorNonFatal = __DARKLUA_BUNDLE_MODULES.B - local logWarn = __DARKLUA_BUNDLE_MODULES.u - local parseError = __DARKLUA_BUNDLE_MODULES.C - local isSimilar = __DARKLUA_BUNDLE_MODULES.z - local needsDestruction = __DARKLUA_BUNDLE_MODULES.D - local makeUseCallback = __DARKLUA_BUNDLE_MODULES.E - local class = {} - local CLASS_METATABLE = { __index = class } - local WEAK_KEYS_METATABLE = { - __mode = "k", - } - - function class:update() - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - - self._oldDependencySet, self.dependencySet = - self.dependencySet, self._oldDependencySet - - for i, _ in pairs(self.dependencySet) do - self.dependencySet[i] = nil - end - - local use = makeUseCallback(self.dependencySet) - 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 - - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = true - end - - return not similar - else - logErrorNonFatal("computedCallbackError", parseError(newValue)) - - self._oldDependencySet, self.dependencySet = - self.dependencySet, self._oldDependencySet - - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = true - end - - return false - end - end - function class:_peek() - return self._value - end - function class:get() - logError "stateGetWasRemoved" - end - - local function Computed(processor, destructor) - local dependencySet = {} - local self = setmetatable({ - type = "State", - kind = "Computed", - dependencySet = dependencySet, - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), - _oldDependencySet = {}, - _processor = processor, - _destructor = destructor, - _value = nil, - }, CLASS_METATABLE) - - self:update() - - return self - end - - __DARKLUA_BUNDLE_MODULES.F = Computed -end -do - local parseError = __DARKLUA_BUNDLE_MODULES.C - local logErrorNonFatal = __DARKLUA_BUNDLE_MODULES.B - local logError = __DARKLUA_BUNDLE_MODULES.d - local logWarn = __DARKLUA_BUNDLE_MODULES.u - local cleanup = __DARKLUA_BUNDLE_MODULES.j - local needsDestruction = __DARKLUA_BUNDLE_MODULES.D - local peek = __DARKLUA_BUNDLE_MODULES.n - local makeUseCallback = __DARKLUA_BUNDLE_MODULES.E - local isState = __DARKLUA_BUNDLE_MODULES.m - local class = {} - local CLASS_METATABLE = { __index = class } - local WEAK_KEYS_METATABLE = { - __mode = "k", - } - - function class:update() - local inputIsState = self._inputIsState - local newInputTable = peek(self._inputTable) - local oldInputTable = self._oldInputTable - local keyIOMap = self._keyIOMap - local meta = self._meta - local didChange = false - - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - - self._oldDependencySet, self.dependencySet = - self.dependencySet, self._oldDependencySet - - for i, _ in pairs(self.dependencySet) do - self.dependencySet[i] = nil - end - - if inputIsState then - self._inputTable.dependentSet[self] = true - self.dependencySet[self._inputTable] = true - end - - self._oldOutputTable, self._outputTable = - self._outputTable, self._oldOutputTable - - local oldOutputTable = self._oldOutputTable - local newOutputTable = self._outputTable - - for i, _ in pairs(newOutputTable) do - newOutputTable[i] = nil - end - for newInKey, newInValue in pairs(newInputTable) do - 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 - - local shouldRecalculate = oldInputTable[newInKey] ~= newInValue - - if shouldRecalculate == false then - for dependency, oldValue in pairs(keyData.dependencyValues) do - if oldValue ~= peek(dependency) then - shouldRecalculate = true - - break - end - end - end - if shouldRecalculate then - keyData.oldDependencySet, keyData.dependencySet = - keyData.dependencySet, keyData.oldDependencySet - - for i, _ in pairs(keyData.dependencySet) do - keyData.dependencySet[i] = nil - end - - local use = makeUseCallback(keyData.dependencySet) - 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 newOutputTable[newOutKey] ~= nil then - 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 - - oldInputTable[newInKey] = newInValue - keyIOMap[newInKey] = newOutKey - meta[newOutKey] = newMetaValue - newOutputTable[newOutKey] = newOutValue - didChange = true - else - keyData.oldDependencySet, keyData.dependencySet = - keyData.dependencySet, keyData.oldDependencySet - - logErrorNonFatal( - "forPairsProcessorError", - parseError(newOutKey) - ) - end - else - local storedOutKey = keyIOMap[newInKey] - - if newOutputTable[storedOutKey] ~= nil then - 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 - - newOutputTable[storedOutKey] = oldOutputTable[storedOutKey] - end - - for dependency in pairs(keyData.dependencySet) do - keyData.dependencyValues[dependency] = peek(dependency) - self.dependencySet[dependency] = true - dependency.dependentSet[self] = true - end - end - for oldOutKey, oldOutValue in pairs(oldOutputTable) do - if newOutputTable[oldOutKey] ~= oldOutValue then - 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 - 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 - function class:_peek() - return self._outputTable - end - function class:get() - logError "stateGetWasRemoved" - end - - local function ForPairs(inputTable, processor, destructor) - local inputIsState = isState(inputTable) - local self = setmetatable({ - type = "State", - kind = "ForPairs", - dependencySet = {}, - 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 - - __DARKLUA_BUNDLE_MODULES.G = ForPairs -end -do - local parseError = __DARKLUA_BUNDLE_MODULES.C - local logErrorNonFatal = __DARKLUA_BUNDLE_MODULES.B - local logError = __DARKLUA_BUNDLE_MODULES.d - local logWarn = __DARKLUA_BUNDLE_MODULES.u - local cleanup = __DARKLUA_BUNDLE_MODULES.j - local needsDestruction = __DARKLUA_BUNDLE_MODULES.D - local peek = __DARKLUA_BUNDLE_MODULES.n - local makeUseCallback = __DARKLUA_BUNDLE_MODULES.E - local isState = __DARKLUA_BUNDLE_MODULES.m - local class = {} - local CLASS_METATABLE = { __index = class } - local WEAK_KEYS_METATABLE = { - __mode = "k", - } - - function class:update() - 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 - - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - - self._oldDependencySet, self.dependencySet = - self.dependencySet, self._oldDependencySet - - for i, _ in pairs(self.dependencySet) do - self.dependencySet[i] = nil - end - - if inputIsState then - self._inputTable.dependentSet[self] = true - self.dependencySet[self._inputTable] = true - end - - for newInKey, value in pairs(newInputTable) do - 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 - - local shouldRecalculate = oldInputTable[newInKey] == nil - - if shouldRecalculate == false then - for dependency, oldValue in pairs(keyData.dependencyValues) do - if oldValue ~= peek(dependency) then - shouldRecalculate = true - - break - end - end - end - if shouldRecalculate then - keyData.oldDependencySet, keyData.dependencySet = - keyData.dependencySet, keyData.oldDependencySet - - for i, _ in pairs(keyData.dependencySet) do - keyData.dependencySet[i] = nil - end - - local use = makeUseCallback(keyData.dependencySet) - 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] - - if - oldInKey ~= newInKey - and newInputTable[oldInKey] ~= nil - then - logError( - "forKeysKeyCollision", - nil, - tostring(newOutKey), - tostring(oldInKey), - tostring(newOutKey) - ) - end - if - oldOutKey ~= newOutKey - and keyOIMap[oldOutKey] == newInKey - then - 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 - - oldInputTable[newInKey] = value - meta[newOutKey] = newMetaValue - keyOIMap[newOutKey] = newInKey - keyIOMap[newInKey] = newOutKey - outputTable[newOutKey] = value - didChange = true - else - keyData.oldDependencySet, keyData.dependencySet = - keyData.dependencySet, keyData.oldDependencySet - - logErrorNonFatal( - "forKeysProcessorError", - parseError(newOutKey) - ) - end - end - - for dependency in pairs(keyData.dependencySet) do - keyData.dependencyValues[dependency] = peek(dependency) - self.dependencySet[dependency] = true - dependency.dependentSet[self] = true - end - end - for outputKey, inputKey in pairs(keyOIMap) do - if newInputTable[inputKey] == nil then - local oldMetaValue = meta[outputKey] - local destructOK, err = - pcall(self._destructor or cleanup, outputKey, oldMetaValue) - - if not destructOK then - logErrorNonFatal("forKeysDestructorError", parseError(err)) - end - - oldInputTable[inputKey] = nil - meta[outputKey] = nil - keyOIMap[outputKey] = nil - keyIOMap[inputKey] = nil - outputTable[outputKey] = nil - self._keyData[inputKey] = nil - didChange = true - end - end - - return didChange - end - function class:_peek() - return self._outputTable - end - function class:get() - logError "stateGetWasRemoved" - end - - local function ForKeys(inputTable, processor, destructor) - local inputIsState = isState(inputTable) - local self = setmetatable({ - type = "State", - kind = "ForKeys", - dependencySet = {}, - 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 - - __DARKLUA_BUNDLE_MODULES.H = ForKeys -end -do - local parseError = __DARKLUA_BUNDLE_MODULES.C - local logError = __DARKLUA_BUNDLE_MODULES.d - local logErrorNonFatal = __DARKLUA_BUNDLE_MODULES.B - local logWarn = __DARKLUA_BUNDLE_MODULES.u - local cleanup = __DARKLUA_BUNDLE_MODULES.j - local needsDestruction = __DARKLUA_BUNDLE_MODULES.D - local peek = __DARKLUA_BUNDLE_MODULES.n - local makeUseCallback = __DARKLUA_BUNDLE_MODULES.E - local isState = __DARKLUA_BUNDLE_MODULES.m - local class = {} - local CLASS_METATABLE = { __index = class } - local WEAK_KEYS_METATABLE = { - __mode = "k", - } - - function class:update() - local inputIsState = self._inputIsState - local inputTable = peek(self._inputTable) - local outputValues = {} - local didChange = false - - self._oldValueCache, self._valueCache = - self._valueCache, self._oldValueCache - - local newValueCache = self._valueCache - local oldValueCache = self._oldValueCache - - for i, _ in pairs(newValueCache) do - newValueCache[i] = nil - end - for dependency in pairs(self.dependencySet) do - dependency.dependentSet[self] = nil - end - - self._oldDependencySet, self.dependencySet = - self.dependencySet, self._oldDependencySet - - for i, _ in pairs(self.dependencySet) do - self.dependencySet[i] = nil - end - - if inputIsState then - self._inputTable.dependentSet[self] = true - self.dependencySet[self._inputTable] = true - end - - for inKey, inValue in pairs(inputTable) do - local oldCachedValues = oldValueCache[inValue] - local shouldRecalculate = oldCachedValues == nil - 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 - if shouldRecalculate == false then - for dependency, oldValue in pairs(valueData.dependencyValues) do - if oldValue ~= peek(dependency) then - shouldRecalculate = true - - break - end - end - end - if shouldRecalculate then - valueData.oldDependencySet, valueData.dependencySet = - valueData.dependencySet, valueData.oldDependencySet - - for i, _ in pairs(valueData.dependencySet) do - valueData.dependencySet[i] = nil - end - - local use = makeUseCallback(valueData.dependencySet) - 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 - if value ~= nil then - local destructOK, err = - pcall(self._destructor or cleanup, value, meta) - - if not destructOK then - logErrorNonFatal( - "forValuesDestructorError", - parseError(err) - ) - end - end - - value = newOutValue - meta = newMetaValue - didChange = true - else - valueData.oldDependencySet, valueData.dependencySet = - valueData.dependencySet, valueData.oldDependencySet - - logErrorNonFatal( - "forValuesProcessorError", - parseError(newOutValue) - ) - end - end - - 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 - - for dependency in pairs(valueData.dependencySet) do - valueData.dependencyValues[dependency] = peek(dependency) - self.dependencySet[dependency] = true - dependency.dependentSet[self] = true - end - end - 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 - for i, _ in pairs(oldCachedValueInfo) do - oldCachedValueInfo[i] = nil - end - end - - self._outputTable = outputValues - - return didChange - end - function class:_peek() - return self._outputTable - end - function class:get() - logError "stateGetWasRemoved" - end - - local function ForValues(inputTable, processor, destructor) - local inputIsState = isState(inputTable) - local self = setmetatable({ - type = "State", - kind = "ForValues", - dependencySet = {}, - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), - _oldDependencySet = {}, - _processor = processor, - _destructor = destructor, - _inputIsState = inputIsState, - _inputTable = inputTable, - _outputTable = {}, - _valueCache = {}, - _oldValueCache = {}, - }, CLASS_METATABLE) - - self:update() - - return self - end - - __DARKLUA_BUNDLE_MODULES.I = ForValues -end -do - local Oklab = {} - - function Oklab.to(rgb) - 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 ^ 0.3333333333333333 - local mRoot = m ^ 0.3333333333333333 - local sRoot = s ^ 0.3333333333333333 - - return Vector3.new( - lRoot * 0.2104542553 + mRoot * 0.793617785 - sRoot * 0.0040720468, - lRoot * 1.9779984951 - mRoot * 2.428592205 + sRoot * 0.4505937099, - lRoot * 0.0259040371 + mRoot * 0.7827717662 - sRoot * 0.808675766 - ) - end - function Oklab.from(lab, unclamped) - 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.291485548 - 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 * -4.196086299999999E-3 - - m * 0.7034186147 - + s * 1.707614701 - - if not unclamped then - 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 - - __DARKLUA_BUNDLE_MODULES.J = Oklab -end -do - local Oklab = __DARKLUA_BUNDLE_MODULES.J - local typeof = __DARKLUA_BUNDLE_MODULES.i - - local function lerpType(from, to, ratio) - local typeString = typeof(from) - - if typeof(to) == typeString then - 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 == "Ray" then - return Ray.new( - from.Origin:Lerp(to.Origin, ratio), - from.Direction:Lerp(to.Direction, ratio) - ) - elseif typeString == "Region3" then - 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 - if ratio < 0.5 then - return from - end - - return to - end - - __DARKLUA_BUNDLE_MODULES.K = lerpType -end -do - 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 = 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 = t - 1 - - return c * (pow(t, 3) + 1) + b - end - easing.Cubic.InOut = function(t, b, c) - t = t * 2 - - if t < 1 then - return c / 2 * t * t * t + b - end - - t = 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 = t - 1 - - return -c * (pow(t, 4) - 1) + b - end - easing.Quart.InOut = function(t, b, c) - t = t * 2 - - if t < 1 then - return c / 2 * pow(t, 4) + b - end - - t = 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 = t - 1 - - return c * (pow(t, 5) + 1) + b - end - easing.Quint.InOut = function(t, b, c) - t = t * 2 - - if t < 1 then - return c / 2 * pow(t, 5) + b - end - - t = 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 = t * 2 - - if t < 1 then - return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 - end - - t = 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 = t - 1 - - return (c * sqrt(1 - pow(t, 2)) + b) - end - easing.Circular.InOut = function(t, b, c) - t = t * 2 - - if t < 1 then - return -c / 2 * (sqrt(1 - t * t) - 1) + b - end - - t = 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) - if t == 0 then - return b - elseif t == 1 then - return b + c - end - - local p = 0.3 - local s - - s = p / 4 - t = t - 1 - - return -(c * pow(2, 10 * t) * sin((t * 1 - s) * (2 * pi) / p)) + b - end - easing.Elastic.Out = function(t, b, c) - 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) - if t == 0 then - return b - end - - t = 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 = 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) - 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) - local s = 1.70158 - - return c * t * t * ((s + 1) * t - s) + b - end - easing.Back.Out = function(t, b, c) - local s = 1.70158 - - t = t - 1 - - return c * (t * t * ((s + 1) * t + s) + 1) + b - end - easing.Back.InOut = function(t, b, c) - local s = 2.5949095 - - t = t * 2 - - if t < 1 then - return c / 2 * (t * t * ((s + 1) * t - s)) + b - end - - t = t - 2 - - return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b - end - easing.Back.OutIn = function(t, b, c) - 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 < 0.36363636363636365 then - return c * (7.5625 * t * t) + b - elseif t < 0.7272727272727273 then - t = t - 0.5454545454545454 - - return c * (7.5625 * t * t + 0.75) + b - elseif t < 0.9090909090909091 then - t = t - 0.8181818181818182 - - return c * (7.5625 * t * t + 0.9375) + b - end - - t = t - 0.9545454545454546 - - 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 - __DARKLUA_BUNDLE_MODULES.L = easing -end -do - local easing = __DARKLUA_BUNDLE_MODULES.L - - local function getTweenRatio(tweenInfo, currentTime) - 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 = 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 easing[easeStyle][easeDirection](tweenProgress, 0, 1) - end - - __DARKLUA_BUNDLE_MODULES.M = getTweenRatio -end -do - local External = __DARKLUA_BUNDLE_MODULES.e - local lerpType = __DARKLUA_BUNDLE_MODULES.K - local getTweenRatio = __DARKLUA_BUNDLE_MODULES.M - local updateAll = __DARKLUA_BUNDLE_MODULES.y - local TweenScheduler = {} - local WEAK_KEYS_METATABLE = { - __mode = "k", - } - local allTweens = {} - - setmetatable(allTweens, WEAK_KEYS_METATABLE) - - function TweenScheduler.add(tween) - allTweens[tween] = true - end - function TweenScheduler.remove(tween) - allTweens[tween] = nil - end - - local function updateAllTweens(now) - for tween in pairs(allTweens) 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) - - __DARKLUA_BUNDLE_MODULES.N = TweenScheduler -end -do - local External = __DARKLUA_BUNDLE_MODULES.e - local TweenScheduler = __DARKLUA_BUNDLE_MODULES.N - local logError = __DARKLUA_BUNDLE_MODULES.d - local logErrorNonFatal = __DARKLUA_BUNDLE_MODULES.B - local xtypeof = __DARKLUA_BUNDLE_MODULES.k - local peek = __DARKLUA_BUNDLE_MODULES.n - local typeof = __DARKLUA_BUNDLE_MODULES.i - local class = {} - local CLASS_METATABLE = { __index = class } - local WEAK_KEYS_METATABLE = { - __mode = "k", - } - - function class:update() - local goalValue = peek(self._goalState) - - if goalValue == self._nextValue and not self._currentlyAnimating then - return false - end - - local tweenInfo = peek(self._tweenInfo) - - 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 = tweenDuration + tweenInfo.Time - end - - tweenDuration = tweenDuration * (tweenInfo.RepeatCount + 1) - self._currentTweenDuration = tweenDuration - - TweenScheduler.add(self) - - return false - end - function class:_peek() - return self._currentValue - end - function class:get() - logError "stateGetWasRemoved" - end - - local function Tween(goalState, tweenInfo) - local currentValue = peek(goalState) - - 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 typeof(startingTweenInfo) ~= "TweenInfo" then - logError("mistypedTweenInfo", nil, typeof(startingTweenInfo)) - end - - local self = setmetatable({ - type = "State", - kind = "Tween", - dependencySet = dependencySet, - dependentSet = setmetatable({}, WEAK_KEYS_METATABLE), - _goalState = goalState, - _tweenInfo = tweenInfo, - _tweenInfoIsState = tweenInfoIsState, - _prevValue = currentValue, - _nextValue = currentValue, - _currentValue = currentValue, - _currentTweenInfo = tweenInfo, - _currentTweenDuration = 0, - _currentTweenStartTime = 0, - _currentlyAnimating = false, - }, CLASS_METATABLE) - - goalState.dependentSet[self] = true - - return self - end - - __DARKLUA_BUNDLE_MODULES.O = Tween -end -do - local Oklab = __DARKLUA_BUNDLE_MODULES.J - - local function unpackType(value, typeString) - if typeString == "number" then - return { value } - elseif typeString == "CFrame" then - 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 - 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 - - __DARKLUA_BUNDLE_MODULES.P = unpackType -end -do - local Oklab = __DARKLUA_BUNDLE_MODULES.J - - local function packType(numbers, typeString) - 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 - 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 - - __DARKLUA_BUNDLE_MODULES.Q = packType -end -do - local function springCoefficients(time, damping, speed) - if time == 0 or speed == 0 then - return 1, 0, 0, 1 - end - - local posPos, posVel, velPos, velVel - - if damping > 1 then - 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 - 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 - 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 - - __DARKLUA_BUNDLE_MODULES.R = springCoefficients -end -do - local External = __DARKLUA_BUNDLE_MODULES.e - local packType = __DARKLUA_BUNDLE_MODULES.Q - local springCoefficients = __DARKLUA_BUNDLE_MODULES.R - local updateAll = __DARKLUA_BUNDLE_MODULES.y - local SpringScheduler = {} - local EPSILON = 0.0001 - local activeSprings = {} - local lastUpdateTime = External.lastUpdateStep() - - function SpringScheduler.add(spring) - 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) - activeSprings[spring] = nil - end - - local function updateAllSprings(now) - local springsToSleep = {} - - 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 - spring._currentValue = - packType(spring._springGoals, spring._currentType) - end - end - - External.bindToUpdateStep(updateAllSprings) - - __DARKLUA_BUNDLE_MODULES.S = SpringScheduler -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local logErrorNonFatal = __DARKLUA_BUNDLE_MODULES.B - local unpackType = __DARKLUA_BUNDLE_MODULES.P - local SpringScheduler = __DARKLUA_BUNDLE_MODULES.S - local updateAll = __DARKLUA_BUNDLE_MODULES.y - local xtypeof = __DARKLUA_BUNDLE_MODULES.k - local peek = __DARKLUA_BUNDLE_MODULES.n - local typeof = __DARKLUA_BUNDLE_MODULES.i - local class = {} - local CLASS_METATABLE = { __index = class } - local WEAK_KEYS_METATABLE = { - __mode = "k", - } - - function class:setPosition(newValue) - 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 - function class:setVelocity(newValue) - 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 - function class:addVelocity(deltaValue) - 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 - do - local __DARKLUA_VAR = self._springVelocities - - __DARKLUA_VAR[index] = __DARKLUA_VAR[index] + delta - end - end - - SpringScheduler.add(self) - end - function class:update() - local goalValue = peek(self._goalState) - - if goalValue == self._goalValue then - 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 - 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 - self._currentValue = self._goalValue - - local springPositions = {} - - for i = 1, numSprings do - springPositions[i] = 0 - end - - 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 - - SpringScheduler.remove(self) - - return true - elseif numSprings == 0 then - self._currentValue = self._goalValue - - return true - else - SpringScheduler.add(self) - - return false - end - end - end - function class:_peek() - return self._currentValue - end - function class:get() - logError "stateGetWasRemoved" - end - - local function Spring(goalState, speed, 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, - 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) - - goalState.dependentSet[self] = true - - self:update() - - return self - end - - __DARKLUA_BUNDLE_MODULES.T = Spring -end -do - local function doNothing() end - - __DARKLUA_BUNDLE_MODULES.U = doNothing -end -do - local logError = __DARKLUA_BUNDLE_MODULES.d - local TweenInfo = {} - - function TweenInfo.new( - time, - easingStyle, - easingDirection, - repeatCount, - reverses, - delayTime - ) - local proxy = newproxy(true) - local mt = getmetatable(proxy) - - 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, - } - mt.__newindex = function(_, prop) - error(prop .. " cannot be assigned to", math.huge) - 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 - - __DARKLUA_BUNDLE_MODULES.V = TweenInfo -end - -local External = __DARKLUA_BUNDLE_MODULES.e -local restrictRead = __DARKLUA_BUNDLE_MODULES.f - -do - local MercuryExternal = __DARKLUA_BUNDLE_MODULES.g - - External.setExternalScheduler(MercuryExternal) -end - -local Fusion = restrictRead("Fusion", { - version = { - major = 0, - minor = 3, - isRelease = false, - }, - New = __DARKLUA_BUNDLE_MODULES.p, - Hydrate = __DARKLUA_BUNDLE_MODULES.q, - Ref = __DARKLUA_BUNDLE_MODULES.r, - Out = __DARKLUA_BUNDLE_MODULES.s, - Cleanup = __DARKLUA_BUNDLE_MODULES.t, - Children = __DARKLUA_BUNDLE_MODULES.v, - OnEvent = __DARKLUA_BUNDLE_MODULES.w, - OnChange = __DARKLUA_BUNDLE_MODULES.x, - Value = __DARKLUA_BUNDLE_MODULES.A, - Computed = __DARKLUA_BUNDLE_MODULES.F, - ForPairs = __DARKLUA_BUNDLE_MODULES.G, - ForKeys = __DARKLUA_BUNDLE_MODULES.H, - ForValues = __DARKLUA_BUNDLE_MODULES.I, - Observer = __DARKLUA_BUNDLE_MODULES.l, - Tween = __DARKLUA_BUNDLE_MODULES.O, - Spring = __DARKLUA_BUNDLE_MODULES.T, - cleanup = __DARKLUA_BUNDLE_MODULES.j, - doNothing = __DARKLUA_BUNDLE_MODULES.U, - peek = __DARKLUA_BUNDLE_MODULES.n, - typeof = __DARKLUA_BUNDLE_MODULES.i, - TweenInfo = __DARKLUA_BUNDLE_MODULES.V, - Help = function() - return "See https://elttob.uk/Fusion/0.3/ for more information." - end, -}) - -return Fusion diff --git a/luau/46295863.luau b/luau/46295863.luau index f15dbda..17832b2 100644 --- a/luau/46295863.luau +++ b/luau/46295863.luau @@ -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, diff --git a/luau/host.luau b/luau/host.luau index 31f649f..2a7a35a 100644 --- a/luau/host.luau +++ b/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) diff --git a/luau/join.luau b/luau/join.luau index 4385577..58729ee 100644 --- a/luau/join.luau +++ b/luau/join.luau @@ -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)