diff --git a/aftman.toml b/aftman.toml new file mode 100644 index 0000000..70c6861 --- /dev/null +++ b/aftman.toml @@ -0,0 +1,8 @@ +# This file lists tools managed by Aftman, a cross-platform toolchain manager. +# For more information, see https://github.com/LPGhatguy/aftman + +# To add a new tool, add an entry to this table. +[tools] +rojo = "rojo-rbx/rojo@7.4.1" +wally = "UpliftGames/wally@0.3.2" +selene = "Kampfkarren/selene@0.27.1" \ No newline at end of file diff --git a/default.project.json b/default.project.json index 4e84def..f4b43fd 100644 --- a/default.project.json +++ b/default.project.json @@ -1,22 +1,6 @@ { "name": "ReplicaService", - "servePlaceIds": null, "tree": { - "$className": "DataModel", - "ReplicatedStorage": { - "$className": "ReplicatedStorage", - "$ignoreUnknownInstances": true, - "$path": "src/ReplicatedStorage" - }, - "ServerScriptService": { - "$className": "ServerScriptService", - "$ignoreUnknownInstances": true, - "$path": "src/ServerScriptService" - }, - "ServerStorage": { - "$className": "ServerStorage", - "$ignoreUnknownInstances": true, - "$path": "src/ServerStorage" - } + "$path": "src" } } \ No newline at end of file diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..1f1e170 --- /dev/null +++ b/selene.toml @@ -0,0 +1 @@ +std = "roblox" \ No newline at end of file diff --git a/src/ReplicatedStorage/MadworkMaid.lua b/src/MadworkMaid.lua similarity index 100% rename from src/ReplicatedStorage/MadworkMaid.lua rename to src/MadworkMaid.lua diff --git a/src/ReplicatedStorage/MadworkScriptSignal.lua b/src/MadworkScriptSignal.lua similarity index 100% rename from src/ReplicatedStorage/MadworkScriptSignal.lua rename to src/MadworkScriptSignal.lua diff --git a/src/ReplicatedStorage/RateLimiter.lua b/src/RateLimiter.lua similarity index 100% rename from src/ReplicatedStorage/RateLimiter.lua rename to src/RateLimiter.lua diff --git a/src/ReplicatedStorage/ReplicaController.lua b/src/ReplicaController.lua similarity index 98% rename from src/ReplicatedStorage/ReplicaController.lua rename to src/ReplicaController.lua index 8357daa..0b4770d 100644 --- a/src/ReplicatedStorage/ReplicaController.lua +++ b/src/ReplicaController.lua @@ -683,6 +683,21 @@ Replica = {} Replica.__index = Replica -- Listening: +function Replica:Observe(path, listener) --> [ScriptConnection] listener(new_value) + if type(listener) ~= "function" then + error("[ReplicaController]: Only a function can be set as listener in Replica:Observe()") + end + + local path_array = (type(path) == "string") and StringPathToArray(path) or path + if #path_array < 1 then + error("[ReplicaController]: Passed empty path - a value key must be specified") + end + + listener(self.Data[path]) + + return self:ListenToChange(path, listener) +end + function Replica:ListenToChange(path, listener) --> [ScriptConnection] listener(new_value) if type(listener) ~= "function" then error("[ReplicaController]: Only a function can be set as listener in Replica:ListenToChange()") @@ -1086,4 +1101,4 @@ rev_ReplicaDestroy.OnClientEvent:Connect(function(replica_id) -- (replica_id) DestroyReplicaAndDescendantsRecursive(replica) end) -return ReplicaController \ No newline at end of file +return ReplicaController diff --git a/src/ServerScriptService/ReplicaService.lua b/src/ReplicaService.lua similarity index 100% rename from src/ServerScriptService/ReplicaService.lua rename to src/ReplicaService.lua diff --git a/src/ServerScriptService/Experimental/ReplicaServiceListeners.lua b/src/ServerScriptService/Experimental/ReplicaServiceListeners.lua deleted file mode 100644 index 87e2228..0000000 --- a/src/ServerScriptService/Experimental/ReplicaServiceListeners.lua +++ /dev/null @@ -1,464 +0,0 @@ ---[[ -{Madwork} - --[ReplicaServiceListeners]--------------------------------------- - Injects listener methods to the Replica class server-side - these methods are otherwise - only accessible client-side due to personal decisions based on code sanity. - - More information about the decision to opt out server-side listeners can be found here: - https://devforum.roblox.com/t/replicate-your-states-with-replicaservice-networking-system/894736/22 - - After this module is required it directly alters the Replica class in ReplicaService and - wraps the setter methods to drive connected listeners. - - Injected methods [Replica]: - - Replica:ListenToChange(path, listener) --> [ScriptConnection] (new_value, old_value) - Replica:ListenToNewKey(path, listener) --> [ScriptConnection] (new_value, new_key) - Replica:ListenToArrayInsert(path, listener) --> [ScriptConnection] (new_index, new_value) - Replica:ListenToArraySet(path, listener) --> [ScriptConnection] (index, new_value) - Replica:ListenToArrayRemove(path, listener) --> [ScriptConnection] (old_index, old_value) - Replica:ListenToWrite(function_name, listener) --> [ScriptConnection] (params...) - Replica:ListenToRaw(listener) --> [ScriptConnection] (action_name, path_array, params...) - - -- NOTICE: This module returns a reference to require(ReplicaService) - if your game code execution is - not linear then it's advised to fetch ReplicaService through this module. ---]] - -local SETTINGS = { - -} - ------ Module Table ----- - -local ReplicaServiceListeners = { - -} - ------ Loaded Modules ----- - -local ReplicaService = require(game:GetService("ServerScriptService"):FindFirstChild("ReplicaService", true)) - ------ Private functions ----- - -local function StringPathToArray(path) - local path_array = {} - if path ~= "" then - for s in string.gmatch(path, "[^%.]+") do - table.insert(path_array, s) - end - end - return path_array -end - -local function CreateTableListenerPathIndex(replica, path_array, listener_type) - -- Getting listener table: - local listeners = replica._table_listeners - -- Getting and or creating the structure nescessary to index the path for the listened key: - for i = 1, #path_array do - local key_listeners = listeners[1][path_array[i]] - if key_listeners == nil then - key_listeners = {[1] = {}} - listeners[1][path_array[i]] = key_listeners - end - listeners = key_listeners - end - - local listener_type_table = listeners[listener_type] - if listener_type_table == nil then - listener_type_table = {} - listeners[listener_type] = listener_type_table - end - return listener_type_table -end - -local function CleanTableListenerTable(disconnect_param) - local table_listeners = disconnect_param[1] - local path_array = disconnect_param[2] - local pointer = table_listeners - local pointer_stack = {pointer} - for i = 1, #path_array do - pointer = pointer[1][path_array[i]] - table.insert(pointer_stack, pointer) - end - for i = #pointer_stack, 2, -1 do - local listeners = pointer_stack[i] - if next(listeners[1]) ~= nil then - return -- Lower branches exist for this branch - this branch will not need further cleanup - end - for k = 2, 6 do - if listeners[k] ~= nil then - if #listeners[k] > 0 then - return -- A listener exists - this branch will not need further cleanup - end - end - end - pointer_stack[i - 1][1][path_array[i - 1]] = nil -- Clearing listeners table for this branch - end -end - --- ArrayScriptConnection object: -local ArrayScriptConnection = { - --[[ - _listener = function -- [function] - _listener_table = {} -- [table] -- Table from which the function entry will be removed - _disconnect_listener -- [function / nil] - _disconnect_param -- [value / nil] - --]] -} - -function ArrayScriptConnection:Disconnect() - local listener = self._listener - if listener ~= nil then - local listener_table = self._listener_table - local index = table.find(listener_table, listener) - if index ~= nil then - table.remove(listener_table, index) - end - self._listener = nil - end - if self._disconnect_listener ~= nil then - self._disconnect_listener(self._disconnect_param) - self._disconnect_listener = nil - end -end - -local function NewArrayScriptConnection(listener_table, listener, disconnect_listener, disconnect_param) --> [ScriptConnection] - return { - _listener = listener, - _listener_table = listener_table, - _disconnect_listener = disconnect_listener, - _disconnect_param = disconnect_param, - Disconnect = ArrayScriptConnection.Disconnect - } -end - ------ Initialize ----- - -do - - local Replica = ReplicaService._replica_class - - -- Wrapping Replica creation: - local new_replica_raw = ReplicaService.NewReplica - - function ReplicaService.NewReplica(replica_params) - local replica = new_replica_raw(replica_params) - - -- Creating listener tables: - replica._table_listeners = {[1] = {}} - replica._function_listeners = {} - replica._raw_listeners = {} - - return replica - end - - -- Wrapping Replica listeners: - local set_value_raw = Replica.SetValue -- (replica, path, value) - local set_values_raw = Replica.SetValues -- (replica, path, values) - local array_insert_raw = Replica.ArrayInsert -- (replica, path, value) --> new_index - local array_set_raw = Replica.ArraySet -- (replica, path, index, value) - local array_remove_raw = Replica.ArrayRemove -- (replica, path, index) --> removed_value - local write_raw = Replica.Write -- (replica, function_name, ...) --> return_params... - - function Replica:SetValue(path, value) - local path_array = (type(path) == "string") and StringPathToArray(path) or path - local replica = self - -- Getting path pointer and listener table: - local pointer = replica.Data - local listeners = replica._table_listeners - for i = 1, #path_array - 1 do - pointer = pointer[path_array[i]] - if listeners ~= nil then - listeners = listeners[1][path_array[i]] - end - end - -- Setting value: - local key = path_array[#path_array] - local old_value = pointer[key] - set_value_raw(replica, path_array, value) - -- Signaling listeners: - if old_value ~= value and listeners ~= nil then - if old_value == nil then - if listeners[3] ~= nil then -- "NewKey" listeners - for _, listener in ipairs(listeners[3]) do - listener(value, key) - end - end - end - listeners = listeners[1][path_array[#path_array]] - if listeners ~= nil then - if listeners[2] ~= nil then -- "Change" listeners - for _, listener in ipairs(listeners[2]) do - listener(value, old_value) - end - end - end - end - -- Raw listeners: - for _, listener in ipairs(replica._raw_listeners) do - listener("SetValue", path_array, value) - end - end - - function Replica:SetValues(path, values) - local path_array = (type(path) == "string") and StringPathToArray(path) or path - local replica = self - -- Getting path pointer and listener table: - local pointer = replica.Data - local listeners = replica._table_listeners - for i = 1, #path_array do - pointer = pointer[path_array[i]] - if listeners ~= nil then - listeners = listeners[1][path_array[i]] - end - end - -- Setting values: - for key, value in pairs(values) do - -- Set value: - local old_value = pointer[key] - pointer[key] = value - -- Signaling listeners: - if old_value ~= value and listeners ~= nil then - if old_value == nil then - if listeners[3] ~= nil then -- "NewKey" listeners - for _, listener in ipairs(listeners[3]) do - listener(value, key) - end - end - end - listeners = listeners[1][key] - if listeners ~= nil then - if listeners[2] ~= nil then -- "Change" listeners - for _, listener in ipairs(listeners[2]) do - listener(value, old_value) - end - end - end - end - end - -- We end up setting values twice here, but it shouldn't bother anyone too much: - set_values_raw(replica, path_array, values) - -- Raw listeners: - for _, listener in ipairs(replica._raw_listeners) do - listener("SetValues", path_array, values) - end - end - - function Replica:ArrayInsert(path, value) --> new_index - local path_array = (type(path) == "string") and StringPathToArray(path) or path - local replica = self - -- Getting path pointer and listener table: - local pointer = replica.Data - local listeners = replica._table_listeners - for i = 1, #path_array do - pointer = pointer[path_array[i]] - if listeners ~= nil then - listeners = listeners[1][path_array[i]] - end - end - -- Setting value: - array_insert_raw(replica, path_array, value) - -- Signaling listeners: - local new_index = #pointer - if listeners ~= nil then - if listeners[4] ~= nil then -- "ArrayInsert" listeners - for _, listener in ipairs(listeners[4]) do - listener(new_index, value) - end - end - end - -- Raw listeners: - for _, listener in ipairs(replica._raw_listeners) do - listener("ArrayInsert", path_array, value) - end - return new_index - end - - function Replica:ArraySet(path, index, value) - local path_array = (type(path) == "string") and StringPathToArray(path) or path - local replica = self - -- Getting path pointer and listener table: - local listeners = replica._table_listeners - for i = 1, #path_array do - if listeners ~= nil then - listeners = listeners[1][path_array[i]] - end - end - -- Setting value: - array_set_raw(replica, path_array, index, value) - -- Signaling listeners: - if listeners ~= nil then - if listeners[5] ~= nil then -- "ArraySet" listeners - for _, listener in ipairs(listeners[5]) do - listener(index, value) - end - end - end - -- Raw listeners: - for _, listener in ipairs(replica._raw_listeners) do - listener("ArraySet", path_array, index, value) - end - end - - function Replica:ArrayRemove(path, index) --> removed_value - local path_array = (type(path) == "string") and StringPathToArray(path) or path - local replica = self - -- Getting path pointer and listener table: - local listeners = replica._table_listeners - for i = 1, #path_array do - if listeners ~= nil then - listeners = listeners[1][path_array[i]] - end - end - -- Setting value: - local old_value = array_remove_raw(replica, path_array, index) - -- Signaling listeners: - if listeners ~= nil then - if listeners[6] ~= nil then -- "ArrayRemove" listeners - for _, listener in ipairs(listeners[6]) do - listener(index, old_value) - end - end - end - -- Raw listeners: - for _, listener in ipairs(replica._raw_listeners) do - listener("ArrayRemove", path_array, index, old_value) - end - return old_value - end - - function Replica:Write(function_name, ...) --> return_params... - local return_params = table.pack(write_raw(self, function_name, ...)) - -- Signaling listeners: - local func_id = self._write_lib[function_name] - func_id = func_id and func_id[1] - - local listeners = self._function_listeners[func_id] - if listeners ~= nil then - for _, listener in ipairs(listeners) do - listener(...) - end - end - return table.unpack(return_params) - end - - -- Listener methods: - function Replica:ListenToChange(path, listener) --> [ScriptConnection] listener(new_value) - if type(listener) ~= "function" then - error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToChange()") - end - - local path_array = (type(path) == "string") and StringPathToArray(path) or path - if #path_array < 1 then - error("[ReplicaService]: Passed empty path - a value key must be specified") - end - -- Getting listener table for given path: - local listeners = CreateTableListenerPathIndex(self, path_array, 2) - table.insert(listeners, listener) - -- ScriptConnection which allows the disconnection of the listener: - return NewArrayScriptConnection(listeners, listener, CleanTableListenerTable, {self._table_listeners, path_array}) - end - - function Replica:ListenToNewKey(path, listener) --> [ScriptConnection] listener(new_value, new_key) - if type(listener) ~= "function" then - error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToNewKey()") - end - - local path_array = (type(path) == "string") and StringPathToArray(path) or path - -- Getting listener table for given path: - local listeners = CreateTableListenerPathIndex(self, path_array, 3) - table.insert(listeners, listener) - -- ScriptConnection which allows the disconnection of the listener: - if #path_array == 0 then - return NewArrayScriptConnection(listeners, listener) - else - return NewArrayScriptConnection(listeners, listener, CleanTableListenerTable, {self._table_listeners, path_array}) - end - end - - function Replica:ListenToArrayInsert(path, listener) --> [ScriptConnection] listener(new_value, new_index) - if type(listener) ~= "function" then - error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToArrayInsert()") - end - - local path_array = (type(path) == "string") and StringPathToArray(path) or path - -- Getting listener table for given path: - local listeners = CreateTableListenerPathIndex(self, path_array, 4) - table.insert(listeners, listener) - -- ScriptConnection which allows the disconnection of the listener: - if #path_array == 0 then - return NewArrayScriptConnection(listeners, listener) - else - return NewArrayScriptConnection(listeners, listener, CleanTableListenerTable, {self._table_listeners, path_array}) - end - end - - function Replica:ListenToArraySet(path, listener) --> [ScriptConnection] listener(new_value, index) - if type(listener) ~= "function" then - error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToArraySet()") - end - - local path_array = (type(path) == "string") and StringPathToArray(path) or path - -- Getting listener table for given path: - local listeners = CreateTableListenerPathIndex(self, path_array, 5) - table.insert(listeners, listener) - -- ScriptConnection which allows the disconnection of the listener: - if #path_array == 0 then - return NewArrayScriptConnection(listeners, listener) - else - return NewArrayScriptConnection(listeners, listener, CleanTableListenerTable, {self._table_listeners, path_array}) - end - end - - function Replica:ListenToArrayRemove(path, listener) --> [ScriptConnection] listener(old_value, old_index) - if type(listener) ~= "function" then - error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToArrayRemove()") - end - - local path_array = (type(path) == "string") and StringPathToArray(path) or path - -- Getting listener table for given path: - local listeners = CreateTableListenerPathIndex(self, path_array, 6) - table.insert(listeners, listener) - -- ScriptConnection which allows the disconnection of the listener: - if #path_array == 0 then - return NewArrayScriptConnection(listeners, listener) - else - return NewArrayScriptConnection(listeners, listener, CleanTableListenerTable, {self._table_listeners, path_array}) - end - end - - function Replica:ListenToWrite(function_name, listener) --> [ScriptConnection] listener(params...) - if type(listener) ~= "function" then - error("[ReplicaService]: Only a function can be set as listener in Replica:ListenToWrite()") - end - if self._write_lib == nil then - error("[ReplicaService]: _write_lib was not declared for this replica") - end - - local func_id = self._write_lib[function_name] - func_id = func_id and func_id[1] - if func_id == nil then - error("[ReplicaService]: Write function \"" .. function_name .. "\" not declared inside _write_lib of this replica") - end - - -- Getting listener table for given path: - local listeners = self._function_listeners[func_id] - if listeners == nil then - listeners = {} - self._function_listeners[func_id] = listeners - end - table.insert(listeners, listener) - -- ScriptConnection which allows the disconnection of the listener: - return NewArrayScriptConnection(listeners, listener) - end - - function Replica:ListenToRaw(listener) --> [ScriptConnection] (action_name, params...) - local listeners = self._raw_listeners - table.insert(listeners, listener) - return NewArrayScriptConnection(listeners, listener) - end - -end - -return ReplicaService \ No newline at end of file diff --git a/src/ServerStorage/Examples/AbstractExample/ReplicaTestClient.client.lua b/src/ServerStorage/Examples/AbstractExample/ReplicaTestClient.client.lua deleted file mode 100644 index 0d91501..0000000 --- a/src/ServerStorage/Examples/AbstractExample/ReplicaTestClient.client.lua +++ /dev/null @@ -1,94 +0,0 @@ ---[[ -[Game] - --[ReplicaTestClient]--------------------------------------- - (AbstractExample) - Brief functionality test of ReplicaService; Client-side - - TO RUN THIS TEST: - Parent: - ReplicaTestServer -> ServerScriptService - ReplicaTestClient -> StarterPlayerScripts - TestWriteLib -> ReplicatedStorage - (Only one test can be run at a time) - - What happens: - Various data is randomly changed on the server and replicated to - all players in the game. - ---]] - -local SETTINGS = { - -} - ------ Module Table ----- - -local ReplicaTestClient = { - -} - ------ Loaded Modules ----- - -local ReplicaController = require(game:GetService("ReplicatedStorage"):WaitForChild("ReplicaController")) - ------ Private Variables ----- - ------ Private functions ----- - -local function GetAllMessages(messages) - if next(messages) == nil then - return "Empty!" - else - local result = "" - for message_name, text in pairs(messages) do - result ..= message_name .. " = \"" .. text .. "\"" - .. (next(messages, message_name) ~= nil and "; " or "") - end - return result - end -end - ------ Public functions ----- - ------ Initialize ----- - -ReplicaController.RequestData() - ------ Connections ----- - -ReplicaController.ReplicaOfClassCreated("ReplicaOne", function(replica_one) - - local messages = replica_one.Data.Messages - - print("ReplicaOne and all it's children have been replicated!") - print("Initially received state of all replicas:") - print(" " .. replica_one.Class .. ": " .. GetAllMessages(messages)) - for _, child in ipairs(replica_one.Children) do - local child_data = child.Data - print(" " .. child.Class .. ": (Tags: " .. GetAllMessages(child.Tags) - .. "); TestValue = " .. child_data.TestValue .. "; NestedValue = " .. child_data.TestTable.NestedValue) - - child:ListenToChange({"TestValue"}, function(new_value) - print("[" .. child.Class .. "]: (Index: " .. child.Tags.Index .. ") TestValue changed to " .. tostring(new_value)) - end) - child:ListenToChange({"TestTable", "NestedValue"}, function(new_value) - print("[" .. child.Class .. "]: (Index: " .. child.Tags.Index .. ") NestedValue changed to " .. child_data.TestTable.NestedValue) - end) - end - - print("Printing updates...") - - replica_one:ListenToWrite("SetMessage", function(message_name, text) - print("[" .. replica_one.Class .. "]: SetMessage - (" .. message_name .. " = \"" .. text .. "\") " .. GetAllMessages(messages)) - end) - - replica_one:ListenToWrite("SetAllMessages", function(text) - print("[" .. replica_one.Class .. "]: SetAllMessages - " .. GetAllMessages(messages)) - end) - - replica_one:ListenToWrite("DestroyAllMessages", function() - print("[" .. replica_one.Class .. "]: DestroyAllMessages - " .. GetAllMessages(messages)) - end) - -end) \ No newline at end of file diff --git a/src/ServerStorage/Examples/AbstractExample/ReplicaTestServer.server.lua b/src/ServerStorage/Examples/AbstractExample/ReplicaTestServer.server.lua deleted file mode 100644 index 7a44cb5..0000000 --- a/src/ServerStorage/Examples/AbstractExample/ReplicaTestServer.server.lua +++ /dev/null @@ -1,108 +0,0 @@ ---[[ -[Game] - --[ReplicaTestServer]--------------------------------------- - (AbstractExample) - Brief functionality test of ReplicaService; Server-side - - TO RUN THIS TEST: - Parent: - ReplicaTestServer -> ServerScriptService - ReplicaTestClient -> StarterPlayerScripts - TestWriteLib -> ReplicatedStorage - (Only one test can be run at a time) - - What happens: - Various data is randomly changed on the server and replicated to - all players in the game. - ---]] - -local SETTINGS = { - MessageUpdateTick = 5, -} - ------ Module Table ----- - -local ReplicaTestServer = { - -} - ------ Loaded Modules ----- - -local ReplicaService = require(game:GetService("ServerScriptService").ReplicaService) - ------ Private Variables ----- - -local RunService = game:GetService("RunService") - -local LastTick = os.clock() - --- Assume TestReplicaOne is a list of messages we want to broadcast -local TestReplicaOne = ReplicaService.NewReplica({ - ClassToken = ReplicaService.NewClassToken("ReplicaOne"), -- Create the token in reference for singleton replicas - Data = { - Messages = {}, -- {[message_name] = text, ...} - }, - Replication = "All", - -- Using WaitForChild here to throw a warning if you accidentally forget to include it ;) - -- Be aware that if you accidentally pass nil, the replica will be created without a WriteLib - WriteLib = game:GetService("ReplicatedStorage"):WaitForChild("TestWriteLib"), -}) - -local InstantiatedClassToken = ReplicaService.NewClassToken("InstantiatedReplica") -local InstantiatedReplicas = {} -- {replica, ...} - ------ Private functions ----- - ------ Public functions ----- - ------ Initialize ----- - -for i = 1, 3 do - local replica = ReplicaService.NewReplica({ - ClassToken = InstantiatedClassToken, - -- Optional params: - Tags = {Index = i}, -- "Tags" is a static table that can't be changed during the lifespan of a replica; - -- Use tags for identifying replicas with players (Tags = {Player = player}) or other parameters - Data = { - TestValue = 0, - TestTable = { - NestedValue = "-", - }, - }, - Parent = TestReplicaOne, - }) - InstantiatedReplicas[i] = replica -end - ------ Connections ----- - -RunService.Heartbeat:Connect(function() - if os.clock() - LastTick > SETTINGS.MessageUpdateTick then - LastTick = os.clock() - - local lucky_letter = string.char(math.random(65, 90)) -- Random letter from A to Z - - if math.random(1, 2) == 1 then - -- Do something with TestReplicaOne: - local roll_the_dice = math.random(1, 10) - if roll_the_dice <= 7 then - TestReplicaOne:Write("SetMessage", lucky_letter, "Hello!") - elseif roll_the_dice <= 9 then - TestReplicaOne:Write("SetAllMessages", "Bye!") - else - TestReplicaOne:Write("DestroyAllMessages") - end - else - -- Do something with InstantiatedReplicas: - local random_replica = InstantiatedReplicas[math.random(1, #InstantiatedReplicas)] - if math.random(1, 2) == 1 then - random_replica:SetValue({"TestValue"}, math.random(1, 10)) - else - random_replica:SetValue({"TestTable", "NestedValue"}, lucky_letter) - end - end - - end -end) \ No newline at end of file diff --git a/src/ServerStorage/Examples/AbstractExample/TestWriteLib.lua b/src/ServerStorage/Examples/AbstractExample/TestWriteLib.lua deleted file mode 100644 index 268df13..0000000 --- a/src/ServerStorage/Examples/AbstractExample/TestWriteLib.lua +++ /dev/null @@ -1,35 +0,0 @@ ---[[ -[Game] - --[TestWriteLib]--------------------------------------- - (AbstractExample) - ---]] - -local TestWriteLib = { - - -- Write libs allow you to create actions that will be performed both - -- on server and client-side with the benefit of the server only having - -- to tell the client which function to run; Additionally, the client - -- can listen to specific functions being called by the server to also - -- address bulk data changes as if it was a single change. - - SetMessage = function(replica, message_name, text) - replica:SetValue({"Messages", message_name}, text) - end, - - SetAllMessages = function(replica, text) - for message_name in pairs(replica.Data.Messages) do - replica:SetValue({"Messages", message_name}, text) - end - end, - - DestroyAllMessages = function(replica) - for message_name in pairs(replica.Data.Messages) do - replica:SetValue({"Messages", message_name}, nil) - end - end, - -} - -return TestWriteLib \ No newline at end of file diff --git a/src/ServerStorage/Examples/ExperimentalListenersExample/ReplicaTestClient.client.lua b/src/ServerStorage/Examples/ExperimentalListenersExample/ReplicaTestClient.client.lua deleted file mode 100644 index 922039f..0000000 --- a/src/ServerStorage/Examples/ExperimentalListenersExample/ReplicaTestClient.client.lua +++ /dev/null @@ -1,97 +0,0 @@ ---[[ -[Game] - --[ReplicaTestClient]--------------------------------------- - (ExperimentalListenersExample) - Brief functionality test of ReplicaService; Client-side - - TO RUN THIS TEST: - Parent: - ReplicaTestServer -> ServerScriptService - ReplicaTestClient -> StarterPlayerScripts - TestWriteLib -> ReplicatedStorage - (Only one test can be run at a time) - - What happens: - Various data is randomly changed on the server and replicated to - all players in the game. - - Same as AbstractExample, but with Experimental.ReplicaServiceListeners - being used. - ---]] - -local SETTINGS = { - -} - ------ Module Table ----- - -local ReplicaTestClient = { - -} - ------ Loaded Modules ----- - -local ReplicaController = require(game:GetService("ReplicatedStorage"):WaitForChild("ReplicaController")) - ------ Private Variables ----- - ------ Private functions ----- - -local function GetAllMessages(messages) - if next(messages) == nil then - return "Empty!" - else - local result = "" - for message_name, text in pairs(messages) do - result ..= message_name .. " = \"" .. text .. "\"" - .. (next(messages, message_name) ~= nil and "; " or "") - end - return result - end -end - ------ Public functions ----- - ------ Initialize ----- - -ReplicaController.RequestData() - ------ Connections ----- - -ReplicaController.ReplicaOfClassCreated("ReplicaOne", function(replica_one) - - local messages = replica_one.Data.Messages - - print("ReplicaOne and all it's children have been replicated!") - print("Initially received state of all replicas:") - print(" " .. replica_one.Class .. ": " .. GetAllMessages(messages)) - for _, child in ipairs(replica_one.Children) do - local child_data = child.Data - print(" " .. child.Class .. ": (Tags: " .. GetAllMessages(child.Tags) - .. "); TestValue = " .. child_data.TestValue .. "; NestedValue = " .. child_data.TestTable.NestedValue) - - child:ListenToChange({"TestValue"}, function(new_value) - print("[" .. child.Class .. "]: (Index: " .. child.Tags.Index .. ") TestValue changed to " .. tostring(new_value)) - end) - child:ListenToChange({"TestTable", "NestedValue"}, function(new_value) - print("[" .. child.Class .. "]: (Index: " .. child.Tags.Index .. ") NestedValue changed to " .. child_data.TestTable.NestedValue) - end) - end - - print("Printing updates...") - - replica_one:ListenToWrite("SetMessage", function(message_name, text) - print("[" .. replica_one.Class .. "]: SetMessage - (" .. message_name .. " = \"" .. text .. "\") " .. GetAllMessages(messages)) - end) - - replica_one:ListenToWrite("SetAllMessages", function(text) - print("[" .. replica_one.Class .. "]: SetAllMessages - " .. GetAllMessages(messages)) - end) - - replica_one:ListenToWrite("DestroyAllMessages", function() - print("[" .. replica_one.Class .. "]: DestroyAllMessages - " .. GetAllMessages(messages)) - end) - -end) \ No newline at end of file diff --git a/src/ServerStorage/Examples/ExperimentalListenersExample/ReplicaTestServer.server.lua b/src/ServerStorage/Examples/ExperimentalListenersExample/ReplicaTestServer.server.lua deleted file mode 100644 index 1398c74..0000000 --- a/src/ServerStorage/Examples/ExperimentalListenersExample/ReplicaTestServer.server.lua +++ /dev/null @@ -1,162 +0,0 @@ ---[[ -[Game] - --[ReplicaTestServer]--------------------------------------- - (ExperimentalListenersExample) - Brief functionality test of ReplicaService; Server-side - - TO RUN THIS TEST: - Parent: - ReplicaTestServer -> ServerScriptService - ReplicaTestClient -> StarterPlayerScripts - TestWriteLib -> ReplicatedStorage - (Only one test can be run at a time) - - What happens: - Various data is randomly changed on the server and replicated to - all players in the game. - - Same as AbstractExample, but with Experimental.ReplicaServiceListeners - being used. - ---]] - -local SETTINGS = { - MessageUpdateTick = 5, -} - ------ Module Table ----- - -local ReplicaTestServer = { - -} - ------ Loaded Modules ----- - -local ReplicaService = require(game:GetService("ServerScriptService"):FindFirstChild("ReplicaServiceListeners", true)) - ------ Private Variables ----- - -local RunService = game:GetService("RunService") - -local LastTick = os.clock() - --- Assume TestReplicaOne is a list of messages we want to broadcast -local TestReplicaOne = ReplicaService.NewReplica({ - ClassToken = ReplicaService.NewClassToken("ReplicaOne"), -- Create the token in reference for singleton replicas - Data = { - Messages = {}, -- {[message_name] = text, ...} - }, - Replication = "All", - -- Using WaitForChild here to throw a warning if you accidentally forget to include it ;) - -- Be aware that if you accidentally pass nil, the replica will be created without a WriteLib - WriteLib = game:GetService("ReplicatedStorage"):WaitForChild("TestWriteLib"), -}) - -local InstantiatedClassToken = ReplicaService.NewClassToken("InstantiatedReplica") -local InstantiatedReplicas = {} -- {replica, ...} - ------ Private functions ----- - ------ Public functions ----- - ------ Initialize ----- - -for i = 1, 3 do - local replica = ReplicaService.NewReplica({ - ClassToken = InstantiatedClassToken, - -- Optional params: - Tags = {Index = i}, -- "Tags" is a static table that can't be changed during the lifespan of a replica; - -- Use tags for identifying replicas with players (Tags = {Player = player}) or other parameters - Data = { - TestValue = 0, - TestTable = { - NestedValue = "-", - }, - }, - Parent = TestReplicaOne, - }) - InstantiatedReplicas[i] = replica -end - -do - - local function GetAllMessages(messages) - if next(messages) == nil then - return "Empty!" - else - local result = "" - for message_name, text in pairs(messages) do - result ..= message_name .. " = \"" .. text .. "\"" - .. (next(messages, message_name) ~= nil and "; " or "") - end - return result - end - end - - local replica_one = TestReplicaOne - - local messages = replica_one.Data.Messages - - print("ReplicaOne and all it's children have been replicated!") - print("Initially received state of all replicas:") - print(" " .. replica_one.Class .. ": " .. GetAllMessages(messages)) - for _, child in ipairs(replica_one.Children) do - local child_data = child.Data - print(" " .. child.Class .. ": (Tags: " .. GetAllMessages(child.Tags) - .. "); TestValue = " .. child_data.TestValue .. "; NestedValue = " .. child_data.TestTable.NestedValue) - - child:ListenToChange({"TestValue"}, function(new_value) - print("[" .. child.Class .. "]: (Index: " .. child.Tags.Index .. ") TestValue changed to " .. tostring(new_value)) - end) - child:ListenToChange({"TestTable", "NestedValue"}, function(new_value) - print("[" .. child.Class .. "]: (Index: " .. child.Tags.Index .. ") NestedValue changed to " .. child_data.TestTable.NestedValue) - end) - end - - print("Printing updates...") - - replica_one:ListenToWrite("SetMessage", function(message_name, text) - print("[" .. replica_one.Class .. "]: SetMessage - (" .. message_name .. " = \"" .. text .. "\") " .. GetAllMessages(messages)) - end) - - replica_one:ListenToWrite("SetAllMessages", function(text) - print("[" .. replica_one.Class .. "]: SetAllMessages - " .. GetAllMessages(messages)) - end) - - replica_one:ListenToWrite("DestroyAllMessages", function() - print("[" .. replica_one.Class .. "]: DestroyAllMessages - " .. GetAllMessages(messages)) - end) - -end - ------ Connections ----- - -RunService.Heartbeat:Connect(function() - if os.clock() - LastTick > SETTINGS.MessageUpdateTick then - LastTick = os.clock() - - local lucky_letter = string.char(math.random(65, 90)) -- Random letter from A to Z - - if math.random(1, 2) == 1 then - -- Do something with TestReplicaOne: - local roll_the_dice = math.random(1, 10) - if roll_the_dice <= 7 then - TestReplicaOne:Write("SetMessage", lucky_letter, "Hello!") - elseif roll_the_dice <= 9 then - TestReplicaOne:Write("SetAllMessages", "Bye!") - else - TestReplicaOne:Write("DestroyAllMessages") - end - else - -- Do something with InstantiatedReplicas: - local random_replica = InstantiatedReplicas[math.random(1, #InstantiatedReplicas)] - if math.random(1, 2) == 1 then - random_replica:SetValue({"TestValue"}, math.random(1, 10)) - else - random_replica:SetValue({"TestTable", "NestedValue"}, lucky_letter) - end - end - - end -end) \ No newline at end of file diff --git a/src/ServerStorage/Examples/ExperimentalListenersExample/TestWriteLib.lua b/src/ServerStorage/Examples/ExperimentalListenersExample/TestWriteLib.lua deleted file mode 100644 index b1d263d..0000000 --- a/src/ServerStorage/Examples/ExperimentalListenersExample/TestWriteLib.lua +++ /dev/null @@ -1,35 +0,0 @@ ---[[ -[Game] - --[TestWriteLib]--------------------------------------- - (ExperimentalListenersExample) - ---]] - -local TestWriteLib = { - - -- Write libs allow you to create actions that will be performed both - -- on server and client-side with the benefit of the server only having - -- to tell the client which function to run; Additionally, the client - -- can listen to specific functions being called by the server to also - -- address bulk data changes as if it was a single change. - - SetMessage = function(replica, message_name, text) - replica:SetValue({"Messages", message_name}, text) - end, - - SetAllMessages = function(replica, text) - for message_name in pairs(replica.Data.Messages) do - replica:SetValue({"Messages", message_name}, text) - end - end, - - DestroyAllMessages = function(replica) - for message_name in pairs(replica.Data.Messages) do - replica:SetValue({"Messages", message_name}, nil) - end - end, - -} - -return TestWriteLib \ No newline at end of file diff --git a/src/ServerStorage/Examples/ProfileServiceExample/ReplicaTestClient.client.lua b/src/ServerStorage/Examples/ProfileServiceExample/ReplicaTestClient.client.lua deleted file mode 100644 index 990ef4f..0000000 --- a/src/ServerStorage/Examples/ProfileServiceExample/ReplicaTestClient.client.lua +++ /dev/null @@ -1,61 +0,0 @@ ---[[ -[Game] - --[ReplicaTestClient]--------------------------------------- - (ProfileServiceExample) - Brief functionality test of ReplicaService; Client-side - - TO RUN THIS TEST: - Setup ProfileService: https://madstudioroblox.github.io/ProfileService/tutorial/settingup/ - Parent: - ReplicaTestServer -> ServerScriptService - ReplicaTestClient -> StarterPlayerScripts - TestWriteLib -> ReplicatedStorage - (Only one test can be run at a time) - - What happens: - All players will receive a payout every 3 seconds. Cash is saved - to the DataStore with ProfileService. We're going to replicate - the cash state of individual players to everyone. - ---]] - -local SETTINGS = { - -} - ------ Module Table ----- - -local ReplicaTestClient = { - -} - ------ Loaded Modules ----- - -local ReplicaController = require(game:GetService("ReplicatedStorage"):WaitForChild("ReplicaController")) - ------ Private Variables ----- - -local Players = game:GetService("Players") -local LocalPlayer = Players.LocalPlayer - ------ Private functions ----- - ------ Public functions ----- - ------ Initialize ----- - -ReplicaController.RequestData() - ------ Connections ----- - -ReplicaController.ReplicaOfClassCreated("PlayerProfile", function(replica) - local is_local = replica.Tags.Player == LocalPlayer - local player_name = is_local and "your" or replica.Tags.Player.Name .. "'s" - local replica_data = replica.Data - - print("Received " .. player_name .. " player profile; Cash:", replica_data.Cash) - replica:ListenToChange({"Cash"}, function(new_value) - print(player_name .. " cash changed:", replica_data.Cash) - end) -end) \ No newline at end of file diff --git a/src/ServerStorage/Examples/ProfileServiceExample/ReplicaTestServer.server.lua b/src/ServerStorage/Examples/ProfileServiceExample/ReplicaTestServer.server.lua deleted file mode 100644 index cbfbb6c..0000000 --- a/src/ServerStorage/Examples/ProfileServiceExample/ReplicaTestServer.server.lua +++ /dev/null @@ -1,139 +0,0 @@ ---[[ -[Game] - --[ReplicaTestServer]--------------------------------------- - (ProfileServiceExample) - Brief functionality test of ReplicaService; Server-side - - TO RUN THIS TEST: - Setup ProfileService: https://madstudioroblox.github.io/ProfileService/tutorial/settingup/ - Parent: - ReplicaTestServer -> ServerScriptService - ReplicaTestClient -> StarterPlayerScripts - TestWriteLib -> ReplicatedStorage - (Only one test can be run at a time) - - What happens: - All players will receive a payout every 3 seconds. Cash is saved - to the DataStore with ProfileService. We're going to replicate - the cash state of individual players to everyone. - ---]] - -local SETTINGS = { - ProfileTemplate = { - Cash = 0, - } -} - ------ Module Table ----- - -local ReplicaTestServer = { - -} - ------ Loaded Modules ----- - -local ReplicaService = require(game:GetService("ServerScriptService").ReplicaService) -local ProfileService = require(game:GetService("ServerScriptService").ProfileService) - ------ Private Variables ----- - -local RunService = game:GetService("RunService") -local Players = game:GetService("Players") - -local PlayerProfileClassToken = ReplicaService.NewClassToken("PlayerProfile") - -local GameProfileStore = ProfileService.GetProfileStore( - "PlayerData", - SETTINGS.ProfileTemplate -) - -local PlayerProfile -- PlayerProfile object -local PlayerProfiles = {} -- [player] = {Profile = profile, Replica = replica} - -local LastPayout = os.clock() - ------ Private functions ----- - -local function PlayerAdded(player) - local profile = GameProfileStore:LoadProfileAsync( - "Player_" .. player.UserId, - "ForceLoad" - ) - if profile ~= nil then - profile:AddUserId(player.UserId) - profile:Reconcile() - profile:ListenToRelease(function() - PlayerProfiles[player].Replica:Destroy() - PlayerProfiles[player] = nil - player:Kick() - end) - if player:IsDescendantOf(Players) == true then - local player_profile = { - Profile = profile, - Replica = ReplicaService.NewReplica({ - ClassToken = PlayerProfileClassToken, - Tags = {Player = player}, - Data = profile.Data, - Replication = "All", - }), - _player = player, - } - setmetatable(player_profile, PlayerProfile) - PlayerProfiles[player] = player_profile - else - profile:Release() - end - else - player:Kick() - end -end - ------ Public functions ----- - --- PlayerProfile object: -PlayerProfile = { - --[[ - _player = player, - --]] -} -PlayerProfile.__index = PlayerProfile - -function PlayerProfile:GiveCash(cash_amount) - if self:IsActive() == false then - return - end - self.Replica:SetValue({"Cash"}, self.Replica.Data.Cash + cash_amount) -end - -function PlayerProfile:IsActive() --> is_active - return PlayerProfiles[self._player] ~= nil -end - ------ Initialize ----- - -for _, player in ipairs(Players:GetPlayers()) do - coroutine.wrap(PlayerAdded)(player) -end - ------ Connections ----- - -RunService.Heartbeat:Connect(function() - if os.clock() - LastPayout > 3 then - LastPayout = os.clock() - print("Payout!") - for _, player_profile in pairs(PlayerProfiles) do - player_profile:GiveCash(100) - end - end -end) - -Players.PlayerAdded:Connect(PlayerAdded) - -Players.PlayerRemoving:Connect(function(player) - local player_profile = PlayerProfiles[player] - if player_profile ~= nil then - player_profile.Profile:Release() - end -end) \ No newline at end of file diff --git a/src/init.lua b/src/init.lua new file mode 100644 index 0000000..de74e0b --- /dev/null +++ b/src/init.lua @@ -0,0 +1,10 @@ +if game:GetService("RunService"):IsServer() then + return require(script.ReplicaService) + +else + local ReplicaService = script:FindFirstChild("ReplicaService") + if ReplicaService then + ReplicaService:Destroy() + end + return require(script.ReplicaController) +end \ No newline at end of file diff --git a/wally.toml b/wally.toml new file mode 100644 index 0000000..e59d099 --- /dev/null +++ b/wally.toml @@ -0,0 +1,9 @@ +[package] +name = "kurookku/replicaservice" +description = "ReplicaService forked from Madwork but includes Replica:Observe method" +version = "0.1.3" +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" +include = ["src", "default.project.json"] + +[dependencies]