-- Compiled with roblox-ts v3.0.0 local TS = _G[script] local Reflect = TS.import(script, TS.getModule(script, "@flamework", "core").out).Reflect local t = TS.import(script, TS.getModule(script, "@rbxts", "t").lib.ts).t -- eslint-disable @typescript-eslint/no-explicit-any local _components = TS.import(script, TS.getModule(script, "@flamework", "components").out) local BaseComponent = _components.BaseComponent local Component = _components.Component local Signal = TS.import(script, TS.getModule(script, "@rbxts", "beacon").out).Signal local _charm = TS.import(script, TS.getModule(script, "@rbxts", "charm")) local Charm = _charm local atom = _charm.atom local subscribe = _charm.subscribe local _services = TS.import(script, TS.getModule(script, "@rbxts", "services")) local Players = _services.Players local ReplicatedStorage = _services.ReplicatedStorage local RunService = _services.RunService local remotes = TS.import(script, script.Parent.Parent, "remotes").remotes local _utilities = TS.import(script, script.Parent.Parent, "utilities") local GenerateID = _utilities.GenerateID local GetConstructorIdentifier = _utilities.GetConstructorIdentifier local GetSharedComponentCtor = _utilities.GetSharedComponentCtor local logAssert = _utilities.logAssert local logWarning = _utilities.logWarning local patch = TS.import(script, script.Parent, "patch") local Pointer = TS.import(script, script.Parent, "pointer").Pointer local IsServer = RunService:IsServer() local IsClient = RunService:IsClient() local event = ReplicatedStorage:FindFirstChild("REFLEX_DEVTOOLS") local InstancesWithId = {} local GetInstanceWithId = function(id) local _id = id return InstancesWithId[_id] end local removeInstanceId local registerInstanceId = function(id, instance) local _id = id local _instance = instance InstancesWithId[_id] = _instance instance.Destroying:Connect(function() removeInstanceId(id) end) end removeInstanceId = function(id) local _id = id InstancesWithId[_id] = nil end local SharedComponent do local super = BaseComponent SharedComponent = setmetatable({}, { __tostring = function() return "SharedComponent" end, __index = super, }) SharedComponent.__index = SharedComponent function SharedComponent:constructor() super.constructor(self) self.isBlockingServerDispatches = false self.isAutoConnect = true self.remotes = {} self.isConnected = false self.isEnableDevTool = false self.listeners = {} self.connectedPlayers = {} self.uniqueId = "" self.onDoneHydrating = Signal.new() self.isDestroyed = false self.isDoneHydrating = false self.__Hydrate = function(player) self:onSendPayload(player, self.__GenerateHydrateData()) end self.__GenerateHydrateData = function() return { type = "init", data = self.state, } end local localAtom = atom() self.atom = (function(state) if state == nil then if localAtom() ~= self.state then localAtom(self.state) end return self.state end local prevState = self.state local newState = localAtom(state) self.state = state if IsServer then self:scheduleSync(prevState) end return newState end) self:initSharedActions() self.sharedComponentCtor = GetSharedComponentCtor(self:getConstructor(), SharedComponent) end function SharedComponent:onStart() end function SharedComponent:GetID() return self.uniqueId end function SharedComponent:GetState() return self.state end function SharedComponent:GetIsConnected() return self.isConnected end function SharedComponent:Subscribe(...) local args = { ... } if self.isDestroyed then return function() end end if #args == 1 then local _binding = args local listener = _binding[1] local unsubscribe = subscribe(self.atom, listener) self.listeners[unsubscribe] = true return function() unsubscribe() self.listeners[unsubscribe] = nil end end local _binding = args local selector = _binding[1] local listener = _binding[2] local unsubscribe = subscribe(function() return selector(self.atom()) end, listener) self.listeners[unsubscribe] = true return function() unsubscribe() self.listeners[unsubscribe] = nil end end function SharedComponent:Dispatch(newState) if self.isDestroyed then return nil end return self.atom(newState) end function SharedComponent:GenerateInfo() local id = self.instance:GetAttribute("__SERVER_ID") local _condition = self.info if _condition == nil then local _object = {} local _left = "InstanceId" local _condition_1 = id if _condition_1 == nil then _condition_1 = "" end _object[_left] = _condition_1 _object.Identifier = GetConstructorIdentifier(self:getConstructor()) _object.SharedIdentifier = GetConstructorIdentifier(self.sharedComponentCtor) _object.PointerID = if self.pointer then Pointer.GetPointerID(self.pointer) else nil _condition = _object end local info = _condition if info.InstanceId ~= id then local _condition_1 = id if _condition_1 == nil then _condition_1 = "" end info.InstanceId = _condition_1 end self.info = info return info end function SharedComponent:DisconnectPlayer(player) if self.isDestroyed then return nil end local _connectedPlayers = self.connectedPlayers local _player = player if not (_connectedPlayers[_player] ~= nil) then return nil end local _connectedPlayers_1 = self.connectedPlayers local _player_1 = player _connectedPlayers_1[_player_1] = nil self:OnDisconnectedPlayer(player) local __shared_component_disconnected = remotes._shared_component_disconnected local _exp = player local _condition = self.uniqueId if _condition == nil then _condition = self:GenerateInfo() end __shared_component_disconnected:fire(_exp, _condition) end SharedComponent.Disconnect = TS.async(function(self) if self.isDestroyed then return nil end if self:getServerId() == nil then logWarning("Client is not disconnected to a server component, because the ServerId is not set.") return nil end if not self.isConnected then return nil end return TS.await(self:sendDisconnectAction()) end) SharedComponent.Connect = TS.async(function(self) if self.isDestroyed then return nil end if self:getServerId() == nil then logWarning("Client is not connected to a server component, because the ServerId is not set.") return nil end if self.isConnected then return true end return TS.await(self:sendConnectAction()) end) function SharedComponent:ResolveIsSyncForPlayer(player, data) return true end function SharedComponent:ResolveConnectionPermission(player) return true end function SharedComponent:ResolveSyncForPlayer(player, data) return data end function SharedComponent:OnConnectedPlayer(player) end function SharedComponent:OnDisconnectedPlayer(player) end function SharedComponent:OnConnected() end function SharedComponent:OnDisconnected() end function SharedComponent:GetConnectedPlayers() return self.connectedPlayers end function SharedComponent:IsConnectedPlayer(player) if self.isDestroyed then return false end local _connectedPlayers = self.connectedPlayers local _player = player return _connectedPlayers[_player] ~= nil end function SharedComponent:AttachDevTool() logAssert(IsClient, "Must be a client") self.isEnableDevTool = true end function SharedComponent:DisableDevTool() logAssert(IsClient, "Must be a client") self.isEnableDevTool = false end function SharedComponent:__GetRemote(name) return self.remotes[name] end SharedComponent.__DispatchFromServer = TS.async(function(self, payload) if self.isBlockingServerDispatches or self.isDestroyed then return nil end if payload.type == "patch" and not self.isDoneHydrating then self.onDoneHydrating:Wait() end Charm.batch(function() if payload.type == "patch" then self.atom(patch.apply(self.state, payload.data)) return nil end self.isDoneHydrating = true self.onDoneHydrating:Fire() self.atom(payload.data) end) if not RunService:IsStudio() or not self.isEnableDevTool then return nil end event:FireServer({ name = `{getmetatable(self)}_serverDispatch`, args = {}, state = self.atom(), }) end) function SharedComponent:__OnPlayerConnect(player) if self.isDestroyed then return false end local isAccess = self:ResolveConnectionPermission(player) if not isAccess then return false end local _connectedPlayers = self.connectedPlayers local _player = player _connectedPlayers[_player] = true self:OnConnectedPlayer(player) return true end function SharedComponent:__OnPlayerDisconnect(player) local _connectedPlayers = self.connectedPlayers local _player = player _connectedPlayers[_player] = nil self:OnDisconnectedPlayer(player) end function SharedComponent:__Disconnected() if not self.isConnected or self.isDestroyed then return nil end self.isConnected = false local _instances = SharedComponent.instances local _uniqueId = self.uniqueId _instances[_uniqueId] = nil self.uniqueId = "" self.isDoneHydrating = false self:OnDisconnected() end function SharedComponent:onSetup() self.atom(self.state) local _result = self.pointer if _result ~= nil then _result:AddComponent(self) end local _ = IsServer and self:_onStartServer() local _1 = IsClient and self:_onStartClient() end function SharedComponent:scheduleSync(prevState) if self.scheduledSyncConnection or self.isDestroyed then return nil end self.scheduledSyncConnection = RunService.Heartbeat:Connect(function() if next(self.connectedPlayers) == nil then local _result = self.scheduledSyncConnection if _result ~= nil then _result:Disconnect() end self.scheduledSyncConnection = nil return nil end local payload = { type = "patch", data = patch.diff(prevState, self.atom()), } for player in self.connectedPlayers do self:onSendPayload(player, payload) end local _result = self.scheduledSyncConnection if _result ~= nil then _result:Disconnect() end self.scheduledSyncConnection = nil end) end function SharedComponent:onSendPayload(player, payload) if self.isDestroyed then return nil end if not self:ResolveIsSyncForPlayer(player, payload.data) then return nil end local data = self:ResolveSyncForPlayer(player, payload.data) payload.data = data remotes._shared_component_dispatch:fire(player, payload, self.uniqueId) end function SharedComponent:initSharedActions() local ctor = getmetatable(self) local original = ctor.onStart ctor.onStart = function(self) for i, remote in pairs(self.remotes) do local newRemote = remote newRemote.componentReferense = self newRemote.name = i end self:onSetup() local _result = original if _result ~= nil then _result(self) end end end function SharedComponent:getServerId() local _value = self.instanceServerId if _value ~= "" and _value then return self.instanceServerId end self.instanceServerId = self.instance:GetAttribute("__SERVER_ID") return self.instanceServerId end function SharedComponent:getConstructor() return getmetatable(self) end function SharedComponent:initInstanceID() if self:getServerId() ~= nil then return nil end local id = GenerateID() self.instance:SetAttribute("__SERVER_ID", id) registerInstanceId(id, self.instance) end function SharedComponent:_onStartServer() self:onAttributeChanged("__SERVER_ID", function(id, oldValue) if oldValue ~= "" and oldValue then local _oldValue = oldValue InstancesWithId[_oldValue] = nil end if id ~= "" and id then registerInstanceId(id, self.instance) end end) self.uniqueId = GenerateID() local _instances = SharedComponent.instances local _uniqueId = self.uniqueId local _self = self _instances[_uniqueId] = _self SharedComponent.onAddedInstances:Fire(self, self.uniqueId) self:initInstanceID() self.playerRemovingConnection = Players.PlayerRemoving:Connect(function(player) local _connectedPlayers = self.connectedPlayers local _player = player if not (_connectedPlayers[_player] ~= nil) then return nil end local _connectedPlayers_1 = self.connectedPlayers local _player_1 = player _connectedPlayers_1[_player_1] = nil self:OnDisconnectedPlayer(player) end) end SharedComponent.sendConnectAction = TS.async(function(self) if self.isDestroyed then return false end if self.isConnected then return true end local _binding = TS.await(remotes._shared_component_connection(self:GenerateInfo(), 0)) local success = _binding[1] local id = _binding[2] local payload = _binding[3] if not success then return false end if payload == nil then logWarning("Server not send hydrate data.") end if payload ~= nil then self:__DispatchFromServer(payload) end self.isConnected = true self.uniqueId = id local _instances = SharedComponent.instances local _uniqueId = self.uniqueId local _self = self _instances[_uniqueId] = _self SharedComponent.onAddedInstances:Fire(self, self.uniqueId) self:OnConnected() return true end) SharedComponent.sendDisconnectAction = TS.async(function(self) if self.isDestroyed then return nil end if not self.isConnected then return nil end TS.await(remotes._shared_component_connection(self.uniqueId, 1)) if not self.isConnected then return nil end self.isConnected = false self:OnDisconnected() return nil end) function SharedComponent:_onStartClient() local id = self:getServerId() if id ~= nil then registerInstanceId(id, self.instance) end local oldValueId = id self.attributeConnection = self.instance:GetAttributeChangedSignal("__SERVER_ID"):Connect(function() if oldValueId ~= nil then local _oldValueId = oldValueId InstancesWithId[_oldValueId] = nil end local id = self:getServerId() if id ~= nil then registerInstanceId(id, self.instance) end oldValueId = id if id ~= nil and not self.isConnected and self.isAutoConnect then self:sendConnectAction() end end) if id ~= nil and not self.isConnected and self.isAutoConnect then self:sendConnectAction() end end function SharedComponent:destroy() if self.isDestroyed then return nil end super.destroy(self) if IsServer then local _exp = self.connectedPlayers -- ▼ ReadonlySet.forEach ▼ local _callback = function(player) self:DisconnectPlayer(player) end for _v in _exp do _callback(_v, _v, _exp) end -- ▲ ReadonlySet.forEach ▲ end if IsClient then self:sendDisconnectAction() end local _result = self.attributeConnection if _result ~= nil then _result:Disconnect() end local _result_1 = self.playerRemovingConnection if _result_1 ~= nil then _result_1:Disconnect() end local _result_2 = self.scheduledSyncConnection if _result_2 ~= nil then _result_2:Disconnect() end self.onDoneHydrating:Destroy() local _exp = self.listeners -- ▼ ReadonlySet.forEach ▼ local _callback = function(unsubscribe) return unsubscribe() end for _v in _exp do _callback(_v, _v, _exp) end -- ▲ ReadonlySet.forEach ▲ table.clear(self.connectedPlayers) if self.uniqueId ~= "" then task.defer(function() local _instances = SharedComponent.instances local _uniqueId = self.uniqueId if _instances[_uniqueId] ~= self then return nil end local _instances_1 = SharedComponent.instances local _uniqueId_1 = self.uniqueId _instances_1[_uniqueId_1] = nil end) end for _, remote in pairs(self.remotes) do remote:Destroy() end self.isDoneHydrating = false self.isDestroyed = true end SharedComponent.instances = {} SharedComponent.onAddedInstances = Signal.new() end --(Flamework) SharedComponent metadata Reflect.defineMetadata(SharedComponent, "identifier", "$s:source/shared-component@SharedComponent") Reflect.defineMetadata(SharedComponent, "flamework:implements", { "$:flamework@OnStart" }) Reflect.decorate(SharedComponent, "$c:components@Component", Component, { { attributes = { __SERVER_ID = t.optional(t.string), }, instanceGuard = t.instanceIsA("Instance"), } }) return { InstancesWithId = InstancesWithId, GetInstanceWithId = GetInstanceWithId, removeInstanceId = removeInstanceId, SharedComponent = SharedComponent, }