RemoteTable-Light is a special type of network library that allows you to replicate tables from server to client with ease and minimal cost. RemoteTable-Light is meant to be a replacement to Replica. It uses a slightly modified version of Packet as the networking library to optimize data usage via buffers.
- Automatically replicate tables to connected listeners
- Selective replication
- Table ids to reduce network and cpu usage
- Buffer based batching
- Support dynamic and nested tables
- Provide all table mutation functionality from standard table library
- Compatible with ProfileStore
- Strictly Typed
- Multi script support
- Multiple listeners per remote table
- Very efficient with table operations
- Selective replication to hide internal data from players
- 1.3x faster initial write, 3x faster write, 20x faster read
- Pure tables, references can be reused for mutation
- Uses ids and references to identify tables instead of string arrays (O(1) lookup)
- Serializes and batches events into buffers
- Uses buffers to take advantage of roblox compression
- More reliable data signal behaviour (ChildAdded, ChildRemoved, Changed)
- Cleaner API (only 5 core server functions)
- Faster operations, no need to deal with strings, pure references
- Super easy to setup, plug and play compared to replica which requires you to call :RequestData yourself (RemoteTable Light handles connection logic internally)
- Timeouts for wait operations
- Clean modular code for devs to read and customize to their needs
Setup
- Get the module here.
- Put it under replicated storage.
Server.lua
local RemoteTable = require(game.ReplicatedStorage.RemoteTableLight).Server
local SelectiveReplicationData = {
Progression = {
Private = false
},
}
local PlayerDataTemplate = {
Cash = 1,
Experience = 0,
Progression = {
Private = {
PrivateValue = "This won't replicate",
}
},
Inventory = {"Phone", "Keys", "Wallet"},
Perks = {},
}
game.Players.PlayerRemoving:Connect(function(player: Player)
RemoteTable.Destroy("PlayerData"..player.UserId)
end)
game.Players.PlayerAdded:Connect(function(player: Player)
local token = "PlayerData"..player.UserId
local player_data = RemoteTable.Create(token, PlayerDataTemplate, SelectiveReplicationData)
RemoteTable.AddClient(token, player)
--[[
Give client a small delay to connect
This is only for demonstration purposes
In production it doesn't matter when client connects
as long as server does .AddClient() for the client
]]
task.wait(2)
RemoteTable.Set(player_data, "Cash", 1000)
task.wait(1)
RemoteTable.Set(player_data.Progression.Private, "PrivateValue", "This change won't be replicated.")
task.wait(1)
local perk = {Type = "Additive", Target = "WalkSpeed", Value = 5}
RemoteTable.Set(player_data.Perks, "SpeedPerk", perk)
task.wait(1)
-- We can reuse the perk reference to edit the perk value
RemoteTable.Increment(perk, "Value", 3)
task.wait(1)
RemoteTable.Increment(player_data, "Experience", 100)
task.wait(1)
-- {"Coffee", "Phone", "Keys", "Wallet"}
RemoteTable.Insert(player_data.Inventory, 1, "Coffee")
task.wait(1)
-- {"Coffee", "Phone", "Wallet"}
RemoteTable.Remove(player_data.Inventory, 3)
task.wait(1)
-- {"Wallet", "Phone"}
RemoteTable.SwapRemove(player_data.Inventory, 1)
task.wait(1)
-- {}
RemoteTable.Clear(player_data.Inventory)
task.wait(1)
RemoteTable.RemoveClient(token, player)
-- This won't replicate as the client is removed from listeners
RemoteTable.Set(player_data, "Cash", 99999)
task.wait(1)
end)
Client.lua
local Player = game:GetService("Players").LocalPlayer
local RemoteTable = require(game.ReplicatedStorage.RemoteTableLight).Client
local token = "PlayerData"..Player.UserId
local data = RemoteTable.WaitForTable(token)
RemoteTable.GetSignal(token, "ChildAdded", {"Inventory"}):Connect(function(child, key)
warn("Inventory added: ", child)
end)
RemoteTable.GetSignal(token, "ChildRemoved", {"Inventory"}):Connect(function(child, key)
warn("Inventory removed: ", child)
end)
RemoteTable.GetSignal(token, "Changed", {"Cash"}):Connect(function(new, old)
warn("Cash changed: ", new)
end)
--TIP: Enable "Show Tables Expanded by Default" for a better visual feedback
while task.wait(0.2) do
--Data changes automatically
print(data)
end
Bonus: Example with ProfileStore
DataService
local DataService = {}
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileStore = require(ServerScriptService.ProfileStore)
local RemoteTable = require(ReplicatedStorage.RemoteTableLight).Server
local Signal = require(ReplicatedStorage.RemoteTableLight.Shared.Zignal)
local PROFILE_TEMPLATE = {
Cash = 0,
Items = {},
}
local PlayerStore = ProfileStore.New("PlayerStore", PROFILE_TEMPLATE)
type PlayerProfile = typeof(PlayerStore:StartSessionAsync())
type ProfileData = typeof(PROFILE_TEMPLATE)
type PlayerData = {
Profile: PlayerProfile,
Data: ProfileData,
}
local PlayerDatas = {} :: {[Player]: PlayerData}
DataService.PlayerDatas = PlayerDatas
DataService.DataLoaded = Signal.new()
function DataService.GetData(player: Player): PlayerData?
return RemoteTable.Get(`PlayerData{player.UserId}`)
end
local function PlayerAdded(player: Player)
local data_token = `PlayerData{player.UserId}`
local profile = PlayerStore:StartSessionAsync(`{player.UserId}`, {
Cancel = function()
return player.Parent ~= Players
end,
})
if not profile then
player:Kick(`[{script.Name}]: Profile load fail - Please rejoin`)
return
end
profile:AddUserId(player.UserId)
profile:Reconcile()
profile.OnSessionEnd:Connect(function()
PlayerDatas[player] = nil
player:Kick(`[{script.Name}]: Profile session end - Please rejoin`)
--Release the token
RemoteTable.Destroy(data_token)
end)
if not player:IsDescendantOf(Players) then
profile:EndSession()
return
end
-- Connect and add client right after player data loads
local player_data = RemoteTable.Create(data_token, profile.Data)
RemoteTable.AddClient(data_token, player)
-- Override profile.Data with the replicated remote table
profile.Data = player_data
PlayerDatas[player] = {
Profile = profile,
Data = player_data
}
print(`[{script.Name}]: Profile loaded for {player.DisplayName}!`)
DataService.DataLoaded:Fire(player, player_data)
end
for _, player in Players:GetPlayers() do
task.defer(PlayerAdded, player)
end
Players.PlayerAdded:Connect(PlayerAdded)
Players.PlayerRemoving:Connect(function(player)
local profile = PlayerDatas[player].Profile
if profile ~= nil then
profile:EndSession()
end
end)
-- Edit functions
DataService.Set = RemoteTable.Set
DataService.Increment = RemoteTable.Increment
DataService.Insert = RemoteTable.Insert
DataService.InsertAt = RemoteTable.InsertAt
DataService.Remove = RemoteTable.Remove
DataService.SwapRemove = RemoteTable.SwapRemove
DataService.Clear = RemoteTable.Clear
return DataService
Server.lua
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local DataService = require(ReplicatedStorage.RemoteTableLight.Examples.ProfileStoreExample.DataService)
DataService.DataLoaded:Once(function(player: Player, data)
-- Add 100 cash each join
DataService.Increment(data, "Cash", 100)
-- Grant a ticket to player everytime they join
DataService.Insert(data.Items, {Name = "Ticket", Value = 10})
end)
Client.lua
local Player = game:GetService("Players").LocalPlayer
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RemoteTable = require(ReplicatedStorage.RemoteTableLight).Client
local TOKEN = `PlayerData{Player.UserId}`
local data = RemoteTable.WaitForTable(TOKEN)
--TIP: Enable "Show Tables Expanded by Default" for a better visual feedback
while task.wait(0.2) do
--Data changes automatically
print(data)
end
TIP: Private fields can be edited directly like regular tables since they are not replicated.
--Requiring the module
local RemoteTable = require(RemoteTable).Server -- or require(RemoteTable.Server)
RemoteTable.Create<T>(token_name: string, template: T, selective_replication: any): T
- Creates a new remote table with given selective replication data
RemoteTable.AddClient(token_name: TokenName, clients: Player | {Player})
- Permits client(s) to listen to a token
RemoteTable.RemoveClient(token_name: TokenName, clients: Player | {Player})
- Revokes client(s) listening permission(s) and disconnects them
RemoteTable.Destroy<T>(token_name: string)
- Releases the token and disconnects the remote table associated with the token.
RemoteTable.Get(token_name: string): any?
- Gets the remote table from token name
RemoteTable.Set(parent: any, key: Key, value: any)
- Set a value to the table with given key
RemoteTable.Increment(parent: any, key: Key, value: number)
- Changes the key by value
RemoteTable.InsertAt<V>(tbl: {V}, pos: number?, value: V)
- table.insert with a pos argument
RemoteTable.Insert<V>(tbl: {V}, value: V)
- table.insert without a pos argument
RemoteTable.Remove<V>(tbl: {V}, pos: number?): V?
- table.remove
RemoteTable.Clear(tbl: any)
- table.clear
-- Requiring the module
local RemoteTable = require(RemoteTable).Client -- or require(RemoteTable.Client)
RemoteTable.IsRemoteTableReady
- Checks if a remote table with a specific token is ready
@param token_name: String alias of the token
@return boolean
RemoteTable.WaitForTable
- Returns the table if available, waits for it if not.
@param token_name: String alias of the token
@param timeout: Timeout in seconds. Returns nil after timing out
@return data: Ready-only replicated table.
RemoteTable.GetSignal
- Gets the apropriate data signal
@param token_name: String alias of the token
@param signal_type: "Changed" | "ChildAdded" | "ChildRemoved"
@param path_list: A string array representing the desired path
@return Signal: Signal that fires (new, old) data or (value, key) for child signals
RemoteTable.DestroySignal
- Cleans up the specified signal
@param token_name: String alias of the token
@param signal_type: "Changed" | "ChildAdded" | "ChildRemoved"
@param path_list: A string array representing the desired path
1.0
- Initial release.
1.1
- Fixed: Unregister logic on client
- Small correction to Changed signal behaviour
1.2
- Fixed: Token registry not registering all tokens on initialize. Thanks to @calinpj04
- Destroying remote table now cleans up everything and releases all references.
- Improvement to listener logic
- Removing a client from server cleans up everything on client
- .Get now returns the root table of the remote table
- Fixed: Insert inserting at wrong index. Thanks to @phantasmal_deathrayz
- Added: Seperate opcode for Insert and InsertAt
- Fixed: Player on leave error
