Skip to content

Commit

Permalink
add more advanced broadcasting logic
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensayshi committed Jul 3, 2019
1 parent 533ee07 commit d59f53c
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 60 deletions.
1 change: 1 addition & 0 deletions TheClassicRace.toc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ src\networking\network.lua
src\core\event-bus.lua
src\core\core.lua
src\core\tracker.lua
src\core\broadcaster.lua
src\core\updater.lua
src\core\scan.lua
src\core\chat-notifier.lua
Expand Down
2 changes: 2 additions & 0 deletions src/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ local TheClassicRaceConfig = {
Id = nil, -- will be set at runtime to channel ID if joined
},
Events = {
-- PlayerInfo({name, level, dingedAt, [broadcast]})
-- argument is a list, not a dict, for efficiency
PlayerInfo = "TCRACE_NET_PLAYER_INFO",
RequestUpdate = "TCRACE_NET_REQUEST_UPDATE",
},
Expand Down
87 changes: 87 additions & 0 deletions src/core/broadcaster.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
-- Addon global
local TheClassicRace = _G.TheClassicRace

-- WoW API
local C_Timer, IsInGuild = _G.C_Timer, _G.IsInGuild

--[[
TheClassicRaceBroadcasterdoes a left-most binary search
to find the lower bound level in our /who query which gives > 0 results but < 50 (because we only get 50 from 1 query)
]]--
---@class TheClassicRaceBroadcaster
---@field Config TheClassicRaceConfig
---@field Core TheClassicRaceCore
---@field DB table<string, table>
---@field EventBus TheClassicRaceEventBus
local TheClassicRaceBroadcaster = {}
TheClassicRaceBroadcaster.__index = TheClassicRaceBroadcaster
TheClassicRace.Broadcaster = TheClassicRaceBroadcaster
setmetatable(TheClassicRaceBroadcaster, {
__call = function(cls, ...)
return cls.new(...)
end,
})

function TheClassicRaceBroadcaster.new(Config, Core, DB, Network)
local self = setmetatable({}, TheClassicRaceBroadcaster)

self.Config = Config
self.Core = Core
self.DB = DB
self.Network = Network

self.timer = nil
self.ticker = nil
self.done = false

return self
end

function TheClassicRaceBroadcaster:IsDone()
return self.done
end

function TheClassicRaceBroadcaster:Start()
if self.timer ~= nil or self.ticker ~= nil then
return
end

-- to avoid everyone else who also received RequestUpdate from being in sync with us
-- we'll add a random sleep offset
-- @TODO: maybe we can do something better?
-- like attempt to observe other broadcasters who are in sync with us offset to cover that
local randomOffset = math.random(1, 10)

self.timer = C_Timer.NewTimer(randomOffset, function()
self.ticker = C_Timer.NewTicker(10, function()
self:Broadcast()
end)
end)
end

function TheClassicRaceBroadcaster:Broadcast()
-- iterate over leaderboard descending
for _, playerInfo in ipairs(self.DB.realm.leaderboard) do
-- if a player hasn't been "observed" yet since our last received RequestUpdate
-- then we can broadcast his info
if playerInfo.observedAt < self.DB.realm.lastRequestUpdate then
self.Network:SendObject(self.Config.Network.Events.PlayerInfo,
{ playerInfo.name, playerInfo.level, playerInfo.dingedAt }, "CHANNEL")
if IsInGuild() then
self.Network:SendObject(self.Config.Network.Events.PlayerInfo,
{ playerInfo.name, playerInfo.level, playerInfo.dingedAt }, "GUILD")
end

-- update observedAt
playerInfo.observedAt = self.Core:Now()

-- return out of this function completely
return
end
end

-- all players on our leaderboard were "observed" since our last received RequestUpdate
-- this means we can stop broadcasting
self.ticker:Cancel()
self.done = true
end
39 changes: 12 additions & 27 deletions src/core/tracker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ to us through the EventBus.
---@field Core TheClassicRaceCore
---@field EventBus TheClassicRaceEventBus
---@field Network TheClassicRaceNetwork
---@field broadcaster TheClassicRaceBroadcaster
local TheClassicRaceTracker = {}
TheClassicRaceTracker.__index = TheClassicRaceTracker
TheClassicRace.Tracker = TheClassicRaceTracker
Expand All @@ -32,7 +33,7 @@ function TheClassicRaceTracker.new(Config, Core, DB, EventBus, Network)
self.EventBus = EventBus
self.Network = Network

self.throttles = {}
self.broadcaster = nil

-- subscribe to network events
EventBus:RegisterCallback(self.Config.Network.Events.PlayerInfo, self, self.OnPlayerInfo)
Expand Down Expand Up @@ -61,11 +62,12 @@ function TheClassicRaceTracker:RequestUpdate()
end
end

function TheClassicRaceTracker:OnRequestUpdate(_, sender)
function TheClassicRaceTracker:OnRequestUpdate()
-- don't respond to update requests when we've disabled networking
if not self.DB.profile.options.networking then
return
end

TheClassicRace:DebugPrint("Update Requested")

-- if we don't know a leader yet then we can't respond
Expand All @@ -74,33 +76,14 @@ function TheClassicRaceTracker:OnRequestUpdate(_, sender)
return
end

-- cleanup throttle list
local now = self.Core:Now()
for _, throttle in ipairs(self.throttles) do
if throttle.time + TheClassicRace.Config.Throttle <= now then
table.remove(self.throttles, 1)
else
break
end
end
-- update last RequestUpdate timestamp
self.DB.realm.lastRequestUpdate = self.Core:Now()

-- check if sender is still in throttle window
for _, throttle in ipairs(self.throttles) do
if throttle.sender == sender then
TheClassicRace:TracePrint("throttled " .. sender)
return
end
-- (re)start our broadcaster if neccesary
if self.broadcaster == nil or self.broadcaster:IsDone() then
self.broadcaster = TheClassicRace.Broadcaster(self.Config, self.Core, self.DB, self.Network)
self.broadcaster:Start()
end

-- add sender to throttle list
table.insert(self.throttles, {sender = sender, time = now})

-- respond with leader
self.Network:SendObject(self.Config.Network.Events.PlayerInfo, {
self.DB.realm.leaderboard[1].name,
self.DB.realm.leaderboard[1].level,
self.DB.realm.leaderboard[1].dingedAt
}, "WHISPER", sender)
end

function TheClassicRaceTracker:OnScanFinished(complete)
Expand Down Expand Up @@ -196,6 +179,8 @@ function TheClassicRaceTracker:HandlePlayerInfo(playerInfo, shouldBroadcast)
name = playerInfo.name,
level = playerInfo.level,
dingedAt = dingedAt,
-- track last time this player was "observed"
observedAt = now,
})

-- truncate when leaderboard reached max size
Expand Down
1 change: 1 addition & 0 deletions src/defaultdb.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ local TheClassicRaceDefaultDB = {
finished = false,
levelThreshold = 2,
highestLevel = 1,
lastRequestUpdate = 0,
leaderboard = {},
},
}
Expand Down
131 changes: 131 additions & 0 deletions tests/broadcaster.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
-- load test base
local TheClassicRace = require("testbase")

-- WoW API stubs
local SetIsInGuild, C_Timer = _G.SetIsInGuild, _G.C_Timer

describe("Broadcaster", function()
---@type TheClassicRaceConfig
local config
local db
---@type TheClassicRaceCore
local core
---@type TheClassicRaceNetwork
local network
---@type TheClassicRaceBroadcaster
local broadcaster
local time = 1000000000

before_each(function()
SetIsInGuild(false)

config = TheClassicRace.Config
db = LibStub("AceDB-3.0"):New("TheClassicRace_DB", TheClassicRace.DefaultDB, true)
db:ResetDB()
core = TheClassicRace.Core("Nub", "NubVille")
-- mock core:Now() to return our mocked time
function core:Now() return time end
network = {SendObject = function() end}
broadcaster = TheClassicRace.Broadcaster(config, core, db, network)
broadcaster.ticker = C_Timer.NewTicker()
end)

after_each(function()
SetIsInGuild(nil)
end)

it("basic broadcast", function()
local networkSpy = spy.on(network, "SendObject")

-- insert some data into leaderboard
db.realm.leaderboard = {
{ name = "Leader", level = 13, observedAt = time - 1 },
{ name = "Nub1", level = 13, observedAt = time - 1 },
}

-- set lastRequestUpdate
db.realm.lastRequestUpdate = time

broadcaster:Broadcast()

assert.spy(networkSpy).was_called_with(match.is_ref(network), config.Network.Events.PlayerInfo,
match.is_same({ db.realm.leaderboard[1].name, db.realm.leaderboard[1].level, nil }), "CHANNEL")

broadcaster:Broadcast()

assert.spy(networkSpy).was_called_with(match.is_ref(network), config.Network.Events.PlayerInfo,
match.is_same({ db.realm.leaderboard[2].name, db.realm.leaderboard[2].level, nil }), "CHANNEL")

broadcaster:Broadcast()

assert.spy(networkSpy).called_at_most(2)
assert.equals(true, broadcaster:IsDone())
end)

it("external observed, won't broadcast", function()
local networkSpy = spy.on(network, "SendObject")

-- insert some data into leaderboard
db.realm.leaderboard = {
{ name = "Leader", level = 13, observedAt = time - 1 },
{ name = "Nub1", level = 13, observedAt = time - 1 },
}

-- set lastRequestUpdate
db.realm.lastRequestUpdate = time

broadcaster:Broadcast()

assert.spy(networkSpy).was_called_with(match.is_ref(network), config.Network.Events.PlayerInfo,
match.is_same({ db.realm.leaderboard[1].name, db.realm.leaderboard[1].level, nil }), "CHANNEL")

-- something external caused observedAt to be bumped for player 2
db.realm.leaderboard[2].observedAt = time

broadcaster:Broadcast()

assert.spy(networkSpy).called_at_most(2)
assert.equals(true, broadcaster:IsDone())
end)

it("request update mid broadcast", function()
local networkSpy = spy.on(network, "SendObject")

-- insert some data into leaderboard
db.realm.leaderboard = {
{ name = "Leader", level = 13, observedAt = time - 1 },
{ name = "Nub1", level = 13, observedAt = time - 1 },
}

-- set lastRequestUpdate
db.realm.lastRequestUpdate = time

broadcaster:Broadcast()

assert.spy(networkSpy).was_called_with(match.is_ref(network), config.Network.Events.PlayerInfo,
match.is_same({ db.realm.leaderboard[1].name, db.realm.leaderboard[1].level, nil }), "CHANNEL")

-- something external caused observedAt to be bumped for player 2
-- which would cause the broadcast to be done
db.realm.leaderboard[2].observedAt = time

-- but lastRequestUpdate is also bumped, so we restart the broadcasting sequence from first player
time = time + 1
db.realm.lastRequestUpdate = time

broadcaster:Broadcast()

assert.spy(networkSpy).was_called_with(match.is_ref(network), config.Network.Events.PlayerInfo,
match.is_same({ db.realm.leaderboard[1].name, db.realm.leaderboard[1].level, nil }), "CHANNEL")

broadcaster:Broadcast()

assert.spy(networkSpy).was_called_with(match.is_ref(network), config.Network.Events.PlayerInfo,
match.is_same({ db.realm.leaderboard[2].name, db.realm.leaderboard[2].level, nil }), "CHANNEL")

broadcaster:Broadcast()

assert.spy(networkSpy).called_at_most(3)
assert.equals(true, broadcaster:IsDone())
end)
end)
16 changes: 16 additions & 0 deletions tests/stubs/misc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,20 @@ end

function _G.hooksecurefunc()

end

_G.C_Timer = {}

function _G.C_Timer.NewTicker()
local ticker = {}
function ticker:Cancel() end

return ticker
end

function _G.C_Timer.NewTimer()
local timer = {}
function timer:Cancel() end

return timer
end
1 change: 1 addition & 0 deletions tests/testbase.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require("core.core")
require("core.event-bus")
require("core.scan")
require("core.tracker")
require("core.broadcaster")
require("networking.network")

return TheClassicRace
Loading

0 comments on commit d59f53c

Please sign in to comment.