Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement client channel overrides #16

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased Changes

- add `override` function to allow clients to push temporary values onto channels ([#16](https://github.com/seaofvoices/crosswalk-channels/pull/16))
- rewrite data syncing logic ([#15](https://github.com/seaofvoices/crosswalk-channels/pull/15))

## 0.1.3
Expand Down
2 changes: 1 addition & 1 deletion foreman.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tools]
rojo = { github = "rojo-rbx/rojo", version = "=7.4.4"}
selene = { github = "Kampfkarren/selene", version = "=0.27.1"}
selene = { github = "Kampfkarren/selene", version = "=0.28.0"}
stylua = { github = "JohnnyMorganz/StyLua", version = "=0.20.0"}
darklua = { github = "seaofvoices/darklua", version = "=0.14.0"}
luau-lsp = { github = "JohnnyMorganz/luau-lsp", version = "=1.35.0"}
Expand Down
85 changes: 83 additions & 2 deletions src/impl/ClientReplication.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local Signal = require('@pkg/luau-signal')
local Teardown = require('@pkg/luau-teardown')

local Constants = require('./Constants')
local compareData = require('./compareData')
local waitForeverForChild = require('./waitForeverForChild')

type Signal<T...> = Signal.Signal<T...>
Expand All @@ -13,21 +14,36 @@ export type ClientReplication = {
setOptions: (self: ClientReplication, options: ClientReplicationOptions) -> (),
setup: (self: ClientReplication, parent: Instance, player: Player) -> (),
bind: <T>(self: ClientReplication, name: string, fn: Fn<T>) -> () -> (),
override: <T>(self: ClientReplication, name: string, value: T, expiration: number?) -> (),
}

type Private = {
_isSetup: boolean,
_race: boolean,
_defaultExpiration: number,
_channelSignals: { [string]: Signal<unknown> },
_lastTimeStamps: { [string]: number },
_lastData: { [string]: unknown },
_overrides: {
[string]: {
lifeTime: number,
minimumOverrideTime: number,
value: unknown,
},
},
_getMinimumOverrideDuration: () -> number,
_timeFn: () -> number,
}

export type ClientReplicationOptions = {
race: boolean?,
defaultExpiration: number?,
getMinimumOverrideDuration: (() -> number)?,
}

local DEFAULT_RACE_OPTIONS = false
local DEFAULT_EXPIRATION = 1.5
local DEFAULT_STATIC_MINIMUM_OVERRIDE = 0.1

type ClientReplicationStatic = ClientReplication & Private & {
new: (options: ClientReplicationOptions?) -> ClientReplication,
Expand All @@ -44,9 +60,15 @@ function ClientReplication.new(options: ClientReplicationOptions?): ClientReplic
local self: Private = {
_isSetup = false,
_race = if options.race == nil then DEFAULT_RACE_OPTIONS else options.race,
_defaultExpiration = options.defaultExpiration or DEFAULT_EXPIRATION,
_channelSignals = {},
_lastTimeStamps = {},
_lastData = {},
_overrides = {},
_getMinimumOverrideDuration = function()
return DEFAULT_STATIC_MINIMUM_OVERRIDE
end,
_timeFn = os.clock,
}

return setmetatable(self, ClientReplicationMetatable) :: any
Expand All @@ -65,6 +87,12 @@ function ClientReplication:setOptions(options: ClientReplicationOptions)
if options.race ~= nil then
self._race = options.race
end
if options.getMinimumOverrideDuration ~= nil then
self._getMinimumOverrideDuration = options.getMinimumOverrideDuration
end
if options.defaultExpiration ~= nil then
self._defaultExpiration = options.defaultExpiration
end
end

function ClientReplication:setup(parent: Instance, _player: Player): Teardown
Expand All @@ -83,8 +111,33 @@ function ClientReplication:setup(parent: Instance, _player: Player): Teardown
return
end

self._lastTimeStamps[channelName] = timeStamp
self._lastData[channelName] = data
local lastData = self._lastData[channelName]
local isEqualToLastData = compareData(lastData, data)

if not isEqualToLastData then
self._lastTimeStamps[channelName] = timeStamp
self._lastData[channelName] = data
end

local now = self._timeFn()

local override = self._overrides[channelName]

if override ~= nil then
local passedOverrideMinimum = now >= override.minimumOverrideTime

if passedOverrideMinimum then
self._overrides[channelName] = nil
end

if compareData(override.value, data) then
return
elseif not passedOverrideMinimum then
return
end
elseif isEqualToLastData then
return
end

local signal = self._channelSignals[channelName]

Expand Down Expand Up @@ -135,4 +188,32 @@ function ClientReplication:bind<T>(name: string, fn: Fn<T>): () -> ()
return disconnect
end

function ClientReplication:override<T>(name: string, value: T, expiration: number?)
local self: Private & ClientReplication = self :: any

local override = {
lifeTime = expiration or self._defaultExpiration,
minimumOverrideTime = self._timeFn() + self._getMinimumOverrideDuration(),
value = value,
}

self._overrides[name] = override

local signal = self._channelSignals[name]
if signal then
signal:fire(value)
end

task.delay(expiration, function()
if self._overrides[name] == override then
self._overrides[name] = nil

local signal = self._channelSignals[name]
if signal then
signal:fire(self._lastData[name])
end
end
end)
end

return ClientReplication
13 changes: 7 additions & 6 deletions src/impl/ServerReplication.lua
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,14 @@ function ServerReplication:setup(parent: Instance): () -> ()
self._remote = remote
end

if self._unreliableRemote == nil then
local unreliableRemote = Instance.new('UnreliableRemoteEvent')
unreliableRemote.Name = Constants.FastEventName
unreliableRemote.Parent = parent
self._unreliableRemote = unreliableRemote
end

if self._race then
if self._unreliableRemote == nil then
local unreliableRemote = Instance.new('UnreliableRemoteEvent')
unreliableRemote.Name = Constants.FastEventName
unreliableRemote.Parent = parent
self._unreliableRemote = unreliableRemote
end
local unreliableRemote = self._unreliableRemote :: UnreliableRemoteEvent

local function onDataReceived(player: Player, channel: string, timeStamp: number)
Expand Down
86 changes: 86 additions & 0 deletions src/impl/__tests__/compareData.test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local expect = jestGlobals.expect
local it = jestGlobals.it

local compareData = require('../compareData')

it('returns true for equal numbers', function()
expect(compareData(42, 42)).toEqual(true)
end)

it('returns true for equal strings', function()
expect(compareData('hello', 'hello')).toEqual(true)
end)

it('returns true for equal Vector3 values', function()
expect(compareData(Vector3.new(1, 2, 3), Vector3.new(1, 2, 3))).toEqual(true)
end)

it('returns true for equal Color3 values', function()
expect(compareData(Color3.new(1, 0, 0), Color3.new(1, 0, 0))).toEqual(true)
end)

it('returns true for equal Enum values', function()
expect(compareData(Enum.KeyCode.A, Enum.KeyCode.A)).toEqual(true)
end)

it('returns true for two nil values', function()
expect(compareData(nil, nil)).toEqual(true)
end)

it('returns false for unequal numbers', function()
expect(compareData(42, 43)).toEqual(false)
end)

it('returns false for unequal strings', function()
expect(compareData('hello', 'world')).toEqual(false)
end)

it('returns false for unequal Vector3 values', function()
expect(compareData(Vector3.new(1, 2, 3), Vector3.new(4, 5, 6))).toEqual(false)
end)

it('returns false for unequal Color3 values', function()
expect(compareData(Color3.new(1, 0, 0), Color3.new(0, 1, 0))).toEqual(false)
end)

it('returns false for unequal Enum values', function()
expect(compareData(Enum.KeyCode.A, Enum.KeyCode.B)).toEqual(false)
end)

it('returns false for a number and a string', function()
expect(compareData(42, '42')).toEqual(false)
end)

it('returns false for Vector3 and Color3', function()
expect(compareData(Vector3.new(1, 2, 3), Color3.new(1, 0, 0))).toEqual(false)
end)

it('returns false for Enum and number', function()
expect(compareData(Enum.KeyCode.A, 1)).toEqual(false)
end)

it('returns false for tables', function()
expect(compareData({ 1, 2, 3 }, { 1, 2, 3 })).toEqual(false)
end)

it('returns false for functions', function()
expect(compareData(function() end, function() end)).toEqual(false)
end)

it('returns false for nil and a number', function()
expect(compareData(nil, 42)).toEqual(false)
end)

it('returns false for a number and nil', function()
expect(compareData(42, nil)).toEqual(false)
end)

it('returns false for mismatched comparable types', function()
expect(compareData(42, '42')).toEqual(false)
end)

it('returns false for Color3 and Vector3', function()
expect(compareData(Color3.new(1, 0, 0), Vector3.new(1, 0, 0))).toEqual(false)
end)
28 changes: 28 additions & 0 deletions src/impl/compareData.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
local COMPARABLE_TYPES = {
['nil'] = true,
number = true,
string = true,
CFrame = true,
Vector2 = true,
Vector2int16 = true,
Vector3 = true,
Vector3int16 = true,
UDim = true,
UDim2 = true,
Color3 = true,
Enum = true,
EnumItem = true,
Rect = true,
Ray = true,
}

local function compareData(value1: unknown, value2: unknown): boolean
local valueType = typeof(value1)
if valueType ~= typeof(value2) then
return false
end

return COMPARABLE_TYPES[valueType] == true and value1 == value2
end

return compareData
19 changes: 18 additions & 1 deletion src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ return function(_SharedModules, Services, isServer)
race: boolean?,
syncInterval: number?,
timeFn: (() -> number)?,
defaultExpiration: number?,
getMinimumOverrideDuration: (() -> number)?,
}

if isServer then
Expand Down Expand Up @@ -133,7 +135,11 @@ return function(_SharedModules, Services, isServer)
else
local ClientReplication = require('./impl/ClientReplication')

local clientReplication = ClientReplication.new()
local clientReplication = ClientReplication.new({
getMinimumOverrideDuration = function()
return Services.Players.LocalPlayer:GetNetworkPing()
end,
})

function module.configure(config: Configuration)
clientReplication:setOptions(config)
Expand All @@ -156,6 +162,17 @@ return function(_SharedModules, Services, isServer)
return clientReplication:bind(name, fn)
end
module.bind = module.Bind

function module.override(name: string, value: unknown, lifeTime: number?)
if _G.DEV then
assert(
type(name) == 'string',
string.format('expected argument #1 to be a string, received %s', type(name))
)
end
clientReplication:override(name, value, lifeTime)
end
module.Override = module.override
end

return module
Expand Down
Loading