Introduction
ReplicatedRegistry2 is a client-server synchronized table registry module for Roblox that provides two-way data replication with built-in filtering, change tracking, and automatic synchronization.
Key Features
- Two-way synchronization between server and clients
- Change tracking with automatic diff calculation
- Built-in filters for access control and rate limiting
- Commit/revert system for managing changes
- Proxy interface for clean, chainable API
- Custom callbacks for validation and security
Basic Concept
ReplicatedRegistry2 maintains a registry of tables that can be synchronized between server and clients. Each registered table:
- Has a unique key for identification
- Tracks changes automatically
- Can be replicated with filters
- Supports both direct and proxy access patterns
When to Use
- Player data synchronization
- Shared game state (leaderboards, settings)
- Real-time collaborative data
- Any data that needs server-client sync with validation
Setup
Installation
Place the ReplicatedRegistry2 module in ReplicatedStorage or ServerScriptService depending on your project structure.
Remote Setup
ReplicatedRegistry2 requires two remotes for communication:
-- In ReplicatedStorage
local RequestFullRemote = Instance.new("RemoteFunction")
RequestFullRemote.Name = "RequestFull"
RequestFullRemote.Parent = ReplicatedStorage
local SendChangesRemote = Instance.new("RemoteEvent")
SendChangesRemote.Name = "SendChanges"
SendChangesRemote.Parent = ReplicatedStorage
Server Configuration
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
-- Option 1: Using RemoteInstances directly
ReplicatedRegistry.server.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
-- Option 2: Using custom callbacks (allows you to do modifications before send/recieve)
ReplicatedRegistry.server.set_remote_callbacks(
function(handler)
-- Initialize request handler
ReplicatedStorage.RequestFullRemote.OnServerInvoke = handler
end,
function(player, key, changes)
-- Send changes to client
ReplicatedStorage.SendChangesRemote:FireClient(player, key, changes)
end,
function(receiver)
-- Connect change receiver
ReplicatedStorage.SendChangesRemote.OnServerEvent:Connect(receiver)
end
)
Client Configuration
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
-- Option 1: Using RemoteInstances directly
ReplicatedRegistry.client.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
-- Option 2: Using custom callbacks (allows you to do modifications before send/recieve)
ReplicatedRegistry.client.set_remote_callbacks(
function(key)
-- Request full table
return ReplicatedStorage.RequestFullRemote:InvokeServer(key)
end,
function(key, changes)
-- Send changes to server
ReplicatedStorage.SendChangesRemote:FireServer(key, changes)
end,
function(receiver)
-- Connect change receiver
ReplicatedStorage.SendChangesRemote.OnClientEvent:Connect(receiver)
end
)
Server API
Registering Tables
server.register(key, table, filter?)
Registers a table on the server for replication.
local playerData = {
coins = 100,
level = 1,
inventory = {}
}
ReplicatedRegistry.server.register("player_123", playerData)
With filter:
local filter = ReplicatedRegistry.get_filter({
rate_limit = 10, -- 10 changes per second max
player_whitelist = {123, 456} -- Only these user IDs
})
ReplicatedRegistry.server.register("shared_data", sharedTable, filter)
Accessing Tables
server.view(key)
Returns the registered table directly.
local data = ReplicatedRegistry.server.view("player_123")
data.coins = 150
data.level = 2
server.view_as_proxy(key)
Returns a proxy interface with helper methods.
local proxy = ReplicatedRegistry.server.view_as_proxy("player_123")
-- Set values
proxy.set({"coins"}, 200)
proxy.set({"inventory", "sword"}, true)
-- Get values
local coins = proxy.get({"coins"})
-- Increment values
proxy.incr({"coins"}, 50)
proxy.incr({"level"}, 1)
-- Replicate to specific players
proxy.replicate({player1, player2})
-- Replicate to all players
proxy.replicate()
-- Get full table
local fullTable = proxy.full()
Replication
server.to_clients(key, players, sender?, changes?, auto_commit?)
Sends changes to specified clients.
-- Replicate to specific players
ReplicatedRegistry.server.to_clients(
"player_123",
{player1, player2}
)
-- Replicate to all players
ReplicatedRegistry.server.to_clients(
"global_data",
game.Players:GetPlayers()
)
Change Management
get_changes(key)
Gets all pending changes since last commit.
local changes = ReplicatedRegistry.get_changes("player_123")
-- changes = {{v = 150, p = {"coins"}}, {v = 2, p = {"level"}}}
commit_changes(key, changes?)
Commits changes, updating the internal copy.
ReplicatedRegistry.commit_changes("player_123")
revert_changes(key, changes?)
Reverts uncommitted changes.
local data = ReplicatedRegistry.server.view("player_123")
data.coins = 999 -- Oops, mistake
ReplicatedRegistry.revert_changes("player_123")
-- coins reverted to previous committed value
Client API
Accessing Tables
client.view(key)
Requests and returns the registered table from the server.
local playerData = ReplicatedRegistry.client.view("player_123")
print(playerData.coins) -- 100
-- Modify locally
playerData.coins = 150
client.view_as_proxy(key)
Returns a proxy interface with helper methods.
local proxy = ReplicatedRegistry.client.view_as_proxy("player_123")
-- Set values
proxy.set({"coins"}, 200)
proxy.set({"inventory", "sword"}, true)
-- Get values
local coins = proxy.get({"coins"})
-- Increment values
proxy.incr({"coins"}, 50)
-- Replicate changes to server
proxy.replicate()
-- Get full table
local fullTable = proxy.full()
Replication
client.to_server(key, sender?, changes?, auto_commit?)
Sends changes to the server.
local data = ReplicatedRegistry.client.view("player_123")
data.coins = 200
data.level = 5
-- Send changes to server
ReplicatedRegistry.client.to_server("player_123")
Manual change tracking:
local data = ReplicatedRegistry.client.view("player_123")
data.coins = 200
local changes = ReplicatedRegistry.get_changes("player_123")
ReplicatedRegistry.client.to_server("player_123", nil, changes)
Listening for Changes
on_recieve(key, callback)
Listens for incoming changes from the server.
local connection = ReplicatedRegistry.on_recieve("player_123", function(sender, table, changes)
print("Received changes from", sender)
for _, change in changes do
print("Path:", table.concat(change.p, "."))
print("Value:", change.v)
end
end)
-- Disconnect when done
connection:Disconnect()
Change Management
Same as server API:
get_changes(key)- Get pending changescommit_changes(key)- Commit changesrevert_changes(key)- Revert changes
Filters
Filters are a on-recieved per register key validation system everytime your context recieves TableChanges from the opposite context.
Built-in Filters
player_blacklist
Blocks specific user IDs from sending changes.
local filter = ReplicatedRegistry.get_filter({
player_blacklist = {123456, 789012}
})
ReplicatedRegistry.server.register("data", myTable, filter)
player_whitelist
Only allows specific user IDs to send changes.
local filter = ReplicatedRegistry.get_filter({
player_whitelist = {123456, 789012}
})
ReplicatedRegistry.server.register("admin_data", adminTable, filter)
rate_limit
Limits how many changes per second can be received.
local filter = ReplicatedRegistry.get_filter({
rate_limit = 5 -- Max 5 changes per second
})
ReplicatedRegistry.server.register("data", myTable, filter)
no_recieve
Prevents receiving changes entirely (send-only).
local filter = ReplicatedRegistry.get_filter({
no_recieve = true
})
ReplicatedRegistry.server.register("readonly_data", myTable, filter)
Custom Filters
You can use custom validation logic.
local function filter(sender, register_key, tbl, changes)
-- Only allow owner to modify their own data
if sender.UserId ~= register_key then
return false
end
for _, c in changes do
local path, value = c.p, c.v
-- Only allow positive coin values
if path[1] == "coins" and value < 0 then
return false
end
end
return true
end
ReplicatedRegistry.server.register("player_data", data, filter)
Combining Filters
You can combine multiple filters together into a composite filter using get_filter().
local filter = ReplicatedRegistry.get_filter({
player_whitelist = {123456, 789012},
rate_limit = 10,
custom = function(sender, _, _, changes)
for _, c in changes do
-- Additional validation
if type(value) ~= "number" then return false end
end
return true
end
})
Composite Filter Behavior
- All filters in a composite filter must pass for a change to be accepted
- If
customfilter is provided, it runs after built-in filters - If any filter returns
false, the change is rejected
Callbacks
Callbacks allow you to customize behavior at key points in the replication process.
Global Callbacks
on_changes_recieved
Called whenever changes are received from a remote.
ReplicatedRegistry.callbacks.on_changes_recieved = function(sender, register_key, changes)
-- sender: Player who sent (nil on client)
-- register_key: The key of the registree
-- changes: Array of changes
print(`Received {#changes} changes for {register_key}`)
-- Return values:
-- "pass" - Continue normal processing
-- "return" - Stop processing, don't apply changes
-- "disable" - Disable this key entirely
return "pass"
end
With validation:
ReplicatedRegistry.callbacks.on_changes_recieved = function(sender, register_key, changes)
-- Validate changes
for _, change in changes do
if not isValidChange(change) then
return "disable", "Invalid change detected"
end
end
return "pass"
end
on_key_disabled
Called when a key is disabled due to an error or validation failure.
ReplicatedRegistry.callbacks.on_key_disabled = function(register_key, reason)
warn(`Key {register_key} was disabled: {reason}`)
-- Clean up or notify admins
notifyAdmins(register_key, reason)
end
Server Callbacks
validate_full_request
Validates whether a client can request the full table.
ReplicatedRegistry.server.callbacks.validate_full_request = function(player, key)
-- Check if player owns this data
if type(key) == "number" then
return player.UserId == key
end
-- Check if key contains player's UserId
if type(key) == "string" then
return string.find(key, tostring(player.UserId)) ~= nil
end
return true
end
Default behavior:
The default callback includes rate limiting and UserId validation:
-- Default implementation
on_request_with_validate_key = function(plr, key)
-- Rate limit: 5 requests per second
if not replication_filters.rate_limit(5)(plr) then
return false
end
-- Wait up to 5 seconds for key to exist
for i = 1, 5 do
if registrees[key] then break end
task.wait(1)
end
-- Validate ownership
if type(key) == "number" then
return plr.UserId == key
end
if type(key) == "string" then
return string.find(key, tostring(plr.UserId)) ~= nil
end
return false
end
Custom Default Callbacks
You can restore default callbacks if needed:
-- Use default callbacks
ReplicatedRegistry.server.callbacks.validate_full_request =
ReplicatedRegistry.default_callbacks.on_request
ReplicatedRegistry.callbacks.on_changes_recieved =
ReplicatedRegistry.default_callbacks.on_changes_recieved
Types Reference
Type definitions for ReplicatedRegistry2.
Core Types
Table
export type Table = {[any]: any}
A generic table type used for registree data.
TableChanges
export type TableChanges = {
{v: any, p: {any}}
}
An array of changes where:
v: The new valuep: The path to the value as an array
Example:
{
{v = 100, p = {"coins"}},
{v = 5, p = {"level"}},
{v = true, p = {"inventory", "sword"}}
}
Filter Types
Filter
export type Filter = (sender: Player?, register_key: any, tbl: Table, changes: TableChanges) -> boolean
A function that validates changes.
Parameters:
sender: The player who sent the change (nil if from server)register_key: The register key being sent changes for.tbl: The old table.changes: Changes waiting to be applied.
Returns: true to accept, false to reject
FilterList
type FilterList = {
player_blacklist: {number}?,
player_whitelist: {number}?,
rate_limit: number?,
no_recieve: boolean?,
custom: Filter?,
}
Configuration object for get_filter().
Interface Types
RegistreeInterface
export type RegistreeInterface<T=Table, ReplicateArgs...=()> = {
set: (...any) -> (any) -> (),
get: (...any) -> any,
incr: (...any) -> (number) -> (),
replicate: (ReplicateArgs...) -> (),
full: () -> T,
}
The proxy interface returned by view_as_proxy().
Methods:
set(...path)(value): Set a value at pathget(...path): Get a value at pathincr(...path)(amount): Increment a number at pathreplicate(args...): Replicate changesfull(): Get the full table
Server ReplicateArgs: {Player}?, RemoteEvent?
Client ReplicateArgs: RemoteEvent?
Internal Types
Registree
type Registree<T=Table> = {
value: T,
copy: Table,
filter: Filter?,
onRecievedListeners: {(plr: Player?, tbl: Table, changes: TableChanges) -> ()},
}
Internal structure for registered tables.
Sender_Server
type Sender_Server = (plr: Player, register_key: any, changes: TableChanges) -> ()
Function signature for sending changes to a client.
Sender_Client
type Sender_Client = (register_key: any, changes: TableChanges) -> ()
Function signature for sending changes to the server.
RequestFull
type RequestFull = (register_key: any) -> TableChanges?
Function signature for requesting full table from server.
Callback Types
on_changes_recieved
(sender: Player?, register_key: any, changes: TableChanges) ->
("pass" | "return" | "disable", string?)
Returns:
"pass": Continue normal processing"return": Stop processing without applying changes"disable": Disable this key entirely- Optional second return: reason string (for "disable")
validate_full_request
(player: Player, key: any) -> boolean
Returns: true to allow request, false to deny
on_key_disabled
(register_key: any, reason: string) -> ()
Called when a key is disabled.
Usage Examples
-- Filter function
local myFilter: Filter = function(sender, path, value)
if path[1] == "coins" and type(value) ~= "number" then
return false
end
return true
end
-- Server proxy
local serverProxy: RegistreeInterface<any, ({Player}?, RemoteEvent?)> =
ReplicatedRegistry.server.view_as_proxy("key")
serverProxy.set("coins")(100)
serverProxy.replicate({player})
-- Client proxy
local clientProxy: RegistreeInterface<any, (RemoteEvent?)> =
ReplicatedRegistry.client.view_as_proxy("key")
clientProxy.incr("level")(1)
clientProxy.replicate()
-- Changes
local changes: TableChanges = ReplicatedRegistry.get_changes("key")
for _, change in changes do
print(`Path: {table.concat(change.p, ".")}`)
print(`Value: {change.v}`)
end
Example: Player Data
A complete example of synchronizing player data between server and client.
Server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
local server = ReplicatedRegistry.server
-- Setup remotes
server.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
-- Custom validation
server.callbacks.validate_full_request = function(player, key)
-- Only allow players to access their own data
return key == player.UserId
end
local playerDataFilter = ReplicatedRegistry.get_filter({
rate_limit = 10, -- Max 10 updates per second
custom = function(sender, register_key, tbl, changes)
-- Player can only access their own keys
if sender.UserId ~= register_key then
return false
end
for _, c in changes do
local value, path = c.v, c.p
-- Prevent negative coins
if path[1] == "coins" and type(value) == "number" and value < 0 then
return false
end
end
return true
end
})
Players.PlayerAdded:Connect(function(player)
local key = player.UserId
-- Create player data
local playerData = {
coins = 100,
level = 1,
inventory = {},
settings = {
sound = true,
music = true
}
}
-- Register for replication
server.register(key, playerData, playerDataFilter)
-- Listen for changes from client
ReplicatedRegistry.on_recieve(key, function(sender, tbl, changes)
print(`Player {sender.Name} updated their data:`)
for _, change in changes do
print(` {table.concat(change.p, ".")} = {change.v}`)
end
savePlayerData(sender, tbl)
end)
end)
Players.PlayerRemoving:Connect(function(player)
local key = player.UserId
local data = server.view(key)
-- Save before player leaves
savePlayerData(player, data)
end)
-- Example: Give coins to player
function giveCoins(player, amount)
local proxy = server.view_as_proxy(player.UserId)
proxy.incr({"coins"}, amount)
proxy.replicate({player}) -- Send to client
end
-- Example: Update player level
function levelUp(player)
local data = server.view(player.UserId)
data.level += 1
data.coins += data.level * 10 -- Bonus coins
server.to_clients(player.UserId, {player})
end
Client Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
local client = ReplicatedRegistry.client
-- Setup remotes
client.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
local player = Players.LocalPlayer
local key = player.UserId
-- Access player data
local playerData = client.view(key)
-- Listen for updates from server
ReplicatedRegistry.on_recieve(key, function(sender, tbl, changes)
print("Received update from server:")
for _, change in changes do
print(` {table.concat(change.p, ".")} = {change.v}`)
end
-- Update UI
updateCoinsDisplay(tbl.coins)
updateLevelDisplay(tbl.level)
end)
-- Example: Update settings
function updateSettings(soundEnabled, musicEnabled)
local proxy = client.view_as_proxy(key)
proxy.set({"settings", "sound"}, soundEnabled)
proxy.set({"settings", "music"}, musicEnabled)
proxy.replicate() -- Send to server
end
-- Example: Purchase item
function purchaseItem(itemName, cost)
local data = client.view(key)
if data.coins >= cost then
data.coins -= cost
data.inventory[itemName] = true
-- Send to server for validation
client.to_server(key)
else
warn("Not enough coins!")
end
end
Example: ProfileStore Integration
Integrating ReplicatedRegistry2 with ProfileService for persistent player data.
Server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Players = game:GetService("Players")
local ProfileService = require(ServerScriptService.ProfileService)
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
local server = ReplicatedRegistry.server
-- Setup remotes
ReplicatedRegistry.server.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
-- ProfileStore setup
local ProfileStore = ProfileService.GetProfileStore(
"PlayerData",
{
coins = 0,
level = 1,
inventory = {},
stats = {
kills = 0,
deaths = 0,
playtime = 0
}
}
)
local Profiles = {}
-- Filter for player profiles
local profileFilter = ReplicatedRegistry.get_filter({
rate_limit = 10,
custom = function(sender, register_key, tbl, changes)
-- Ensure player only modifies their own profile
return register_key == sender.UserId
end
})
Players.PlayerAdded:Connect(function(player)
local profile = ProfileStore:LoadProfileAsync(`Player_{player.UserId}`)
if not profile then
player:Kick("Failed to load data")
return
end
profile:AddUserId(player.UserId)
profile:Reconcile()
profile:ListenToRelease(function()
Profiles[player] = nil
server.deregister(player.UserId)
player:Kick("Profile released")
end)
if not player:IsDescendantOf(Players) then
profile:Release()
return
end
Profiles[player] = profile
-- Register profile data with ReplicatedRegistry
local key = player.UserId
server.register(key, profile.Data, profileFilter)
-- Listen for changes from client
ReplicatedRegistry.on_recieve(key, function(sender, tbl, changes)
print(`{sender.Name} updated profile:`)
for _, change in changes do
print(` {table.concat(change.p, ".")} = {change.v}`)
end
end)
-- Periodic sync to client (every 10 seconds)
task.spawn(function()
while Profiles[player] do
task.wait(10)
if Profiles[player] then
server.to_clients(key, {player})
end
end
end)
end)
Players.PlayerRemoving:Connect(function(player)
local profile = Profiles[player]
if profile then
profile:Release()
Profiles[player] = nil
server.deregister(player.UserId)
end
end)
-- Example: Give rewards
function giveReward(player, coins, xp)
local profile = Profiles[player]
if not profile then return end
local proxy = ReplicatedRegistry.server.view_as_proxy(player.UserId)
proxy.incr({"coins"}, coins)
proxy.incr({"level"}, xp)
-- Replicate to player
proxy.replicate({player})
end
-- Example: Add item to inventory
function giveItem(player, itemName)
local profile = Profiles[player]
if not profile then return end
local data = ReplicatedRegistry.server.view(player.UserId)
data.inventory[itemName] = true
ReplicatedRegistry.server.to_clients(player.UserId, {player})
end
-- Example: Update stats
function recordKill(player)
local profile = Profiles[player]
if not profile then return end
local proxy = ReplicatedRegistry.server.view_as_proxy(player.UserId)
proxy.incr({"stats", "kills"}, 1)
proxy.replicate({player})
end
Client Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
-- Setup remotes
ReplicatedRegistry.client.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
local player = Players.LocalPlayer
local key = player.UserId
-- Wait for data to be ready
local playerData = ReplicatedRegistry.client.view(key)
-- Listen for profile updates
ReplicatedRegistry.on_recieve(key, function(sender, tbl, changes)
for _, change in changes do
local path = table.concat(change.p, ".")
print(`Profile updated: {path} = {change.v}`)
-- Update UI based on what changed
if change.p[1] == "coins" then
updateCoinsUI(tbl.coins)
elseif change.p[1] == "level" then
updateLevelUI(tbl.level)
elseif change.p[1] == "stats" then
updateStatsUI(tbl.stats)
end
end
end)
-- Initial UI setup
updateCoinsUI(playerData.coins)
updateLevelUI(playerData.level)
updateStatsUI(playerData.stats)
-- Example: Purchase system
function tryPurchase(itemName, cost)
local data = ReplicatedRegistry.client.view(key)
if data.coins >= cost then
-- Optimistic update
data.coins -= cost
data.inventory[itemName] = true
-- Server will validate
ReplicatedRegistry.client.to_server(key)
return true
end
return false
end
Notes
- ProfileStore handles persistence
- ReplicatedRegistry handles real-time sync
- Server validates all client changes
- Client gets immediate feedback with optimistic updates
- Profile data automatically reconciles on load
Example: Shared Data
Examples of using ReplicatedRegistry2 for shared game state like leaderboards and settings.
Global Leaderboard
Server
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
-- Setup remotes
ReplicatedRegistry.server.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
-- Create global leaderboard
local leaderboard = {
topPlayers = {},
lastUpdate = os.time()
}
-- No filter = read-only for clients
ReplicatedRegistry.server.register("global_leaderboard", leaderboard)
-- Update leaderboard
function updateLeaderboard()
local proxy = ReplicatedRegistry.server.view_as_proxy("global_leaderboard")
-- Sort players by score
local players = {}
for _, player in Players:GetPlayers() do
table.insert(players, {
name = player.Name,
userId = player.UserId,
score = getPlayerScore(player)
})
end
table.sort(players, function(a, b)
return a.score > b.score
end)
-- Take top 10
local top10 = {}
for i = 1, math.min(10, #players) do
top10[i] = players[i]
end
proxy.set({"topPlayers"}, top10)
proxy.set({"lastUpdate"}, os.time())
-- Broadcast to all players
proxy.replicate()
end
-- Update every 30 seconds
task.spawn(function()
while true do
task.wait(30)
updateLeaderboard()
end
end)
Client
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
ReplicatedRegistry.client.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
-- Get leaderboard
local leaderboard = ReplicatedRegistry.client.view("global_leaderboard")
-- Listen for updates
ReplicatedRegistry.on_recieve("global_leaderboard", function(sender, tbl, changes)
print("Leaderboard updated!")
updateLeaderboardUI(tbl.topPlayers)
end)
-- Initial display
updateLeaderboardUI(leaderboard.topPlayers)
Game Settings
Server
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
-- Setup remotes
ReplicatedRegistry.server.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
-- Game settings
local gameSettings = {
maxPlayers = 16,
roundTime = 300,
gameMode = "FFA",
mapName = "Arena1",
friendly_fire = false
}
-- Only admins can modify
local adminFilter = ReplicatedRegistry.get_filter({
player_whitelist = {123456, 789012}, -- Admin user IDs
rate_limit = 5
})
ReplicatedRegistry.server.register("game_settings", gameSettings, adminFilter)
-- Listen for admin changes
ReplicatedRegistry.on_recieve("game_settings", function(sender, tbl, changes)
print(`Admin {sender.Name} changed settings:`)
for _, change in changes do
print(` {table.concat(change.p, ".")} = {change.v}`)
end
-- Broadcast to all players
ReplicatedRegistry.server.to_clients(
"game_settings",
Players:GetPlayers()
)
end)
-- Helper function to update settings
function updateGameSettings(newSettings)
local proxy = ReplicatedRegistry.server.view_as_proxy("game_settings")
for key, value in newSettings do
proxy.set({key}, value)
end
-- Broadcast to everyone
proxy.replicate()
end
Client
local ReplicatedRegistry = require(ReplicatedStorage.ReplicatedRegistry2)
ReplicatedRegistry.client.set_remote_instances(
ReplicatedStorage.RequestFullRemote,
ReplicatedStorage.SendChangesRemote
)
-- Get settings
local settings = ReplicatedRegistry.client.view("game_settings")
-- Listen for changes
ReplicatedRegistry.on_recieve("game_settings", function(sender, tbl, changes)
print("Game settings updated:")
for _, change in changes do
local setting = change.p[1]
local value = change.v
print(` {setting} = {value}`)
-- Update UI or game logic
if setting == "roundTime" then
updateTimerUI(value)
elseif setting == "gameMode" then
updateGameMode(value)
end
end
end)
-- Admin panel (only works if player is whitelisted)
function isAdmin(player)
-- Check if player is admin
return table.find({123456, 789012}, player.UserId) ~= nil
end
if isAdmin(Players.LocalPlayer) then
-- Admin can change settings
function changeRoundTime(newTime)
local proxy = ReplicatedRegistry.client.view_as_proxy("game_settings")
proxy.set({"roundTime"}, newTime)
proxy.replicate()
end
end
Team Data
Server
local teams = {
red = {
score = 0,
players = 0,
color = Color3.fromRGB(255, 0, 0)
},
blue = {
score = 0,
players = 0,
color = Color3.fromRGB(0, 0, 255)
}
}
-- Allow server updates only
local filter = ReplicatedRegistry.get_filter({
no_recieve = true -- No client updates
})
ReplicatedRegistry.server.register("team_data", teams, filter)
-- Update team scores
function addTeamScore(teamName, points)
local proxy = ReplicatedRegistry.server.view_as_proxy("team_data")
proxy.incr({teamName, "score"}, points)
proxy.replicate() -- Broadcast to all
end
-- Update player counts
Players.PlayerAdded:Connect(function(player)
local team = getPlayerTeam(player)
local proxy = ReplicatedRegistry.server.view_as_proxy("team_data")
proxy.incr({team, "players"}, 1)
proxy.replicate()
end)
Client
local teams = ReplicatedRegistry.client.view("team_data")
-- Update UI when teams change
ReplicatedRegistry.on_recieve("team_data", function(sender, tbl, changes)
updateTeamScoreUI(tbl.red.score, tbl.blue.score)
updateTeamCountUI(tbl.red.players, tbl.blue.players)
end)