--!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 "../../../Modules/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