Skip to content

Commit

Permalink
scan using libwho.
Browse files Browse the repository at this point in the history
mock libwho and test scan.
use left most binary search to find leader in `O(log n + 1)`.
  • Loading branch information
rubensayshi committed Jun 25, 2019
1 parent b4c0213 commit d0bfa97
Show file tree
Hide file tree
Showing 16 changed files with 571 additions and 212 deletions.
4 changes: 3 additions & 1 deletion TheClassicRace.toc
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ src\util\table-helpers.lua
src\util\chat.lua
src\networking\network.lua
src\core\event-bus.lua
src\core\interaction-tracker.lua
src\core\core.lua
src\core\tracker.lua
src\core\updater.lua
src\core\scan.lua
13 changes: 4 additions & 9 deletions src/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@ TheClassicRace.Colors = TheClassicRaceColors
---@class TheClassicRaceConfig
local TheClassicRaceConfig = {
Debug = true,
Trace = false,

PollInterval = 60,
SlashWHoDelay = 5,
Trace = true,

MaxLevel = 60,
MaxSlashWhoResults = 50,


Network = {
Prefix = "TCRace",
Expand All @@ -26,12 +21,12 @@ local TheClassicRaceConfig = {
Id = 1, -- will be set at runtime to channel ID when joined
},
Events = {
SetScore = "TCRACE_NE_SET_SCORE",
RequestScores = "TCRACE_NE_REQUEST_SCORES",
Ding = "TCRACE_DING",
RequestUpdate = "TCRACE_REQUEST_UPDATE",
},
},
Events = {
SetPlayerInfo = "TCRACE_SET_PLAYER_INFO",
PlayerInfo = "TCRACE_PLAYER_INFO",
},
}
TheClassicRace.Config = TheClassicRaceConfig
4 changes: 2 additions & 2 deletions src/core/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
local TheClassicRace = _G.TheClassicRace

-- WoW API
local time = _G.time
local GetServerTime = _G.GetServerTime

---@class TheClassicRaceCore
local TheClassicRaceCore = {}
Expand Down Expand Up @@ -42,5 +42,5 @@ function TheClassicRaceCore:RealMe()
end

function TheClassicRaceCore:Now()
return time()
return GetServerTime()
end
133 changes: 133 additions & 0 deletions src/core/scan.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
-- Addon global
local TheClassicRace = _G.TheClassicRace

--[[
TheClassicRaceScan does 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 TheClassicRaceScan
---@field DB table<string, table>
---@field Core TheClassicRaceCore
---@field EventBus TheClassicRaceEventBus
local TheClassicRaceScan = {}
TheClassicRaceScan.__index = TheClassicRaceScan
TheClassicRace.Scan = TheClassicRaceScan
setmetatable(TheClassicRaceScan, {
__call = function(cls, ...)
return cls.new(...)
end,
})

function TheClassicRaceScan.new(Core, DB, EventBus, who, min, max)
local self = setmetatable({}, TheClassicRaceScan)

self.Core = Core
self.DB = DB
self.EventBus = EventBus
self.who = who

self.min = min
self.max = max

self.done = false
self.started = false

return self
end

function TheClassicRaceScan:SetMin(min)
-- normally we'd err but can't in WoW addons?
if self.started then
return
end

self.min = min
end

function TheClassicRaceScan:IsDone()
return self.done
end

function TheClassicRaceScan:HandleResult(_, result, complete)
-- if we have 0 results then we need to decrease lower bound
if #result == 0 then
-- value > target -> right = m
self.right = self.m

-- if we have > 0 results and not more than we can query in 1 /who then we are done
elseif complete then
-- value == target -> right = m
-- we can exit early here because we don't need to find the exact m
self.done = true
return

-- if we have too many results for 1 /who query then we need to increase lower bound to refine the result
else
-- value < target -> left = m + 1

-- too many people at highest level, we can exit early
if self.m == self.max then
self.done = true
return
end

self.left = self.m + 1
end

-- do next scan
self:Next()
end

function TheClassicRaceScan:Start()
if self.started then
return
end

self.started = true

-- instead of starting with our binary search we start with a shortcut to search for (min, max)
-- and then lead into the binary search
self:Shortcut()
end

function TheClassicRaceScan:Shortcut()
local function cb(query, result, complete)
TheClassicRace:DebugPrint("who '" .. query .. "' result: " .. #result .. ", complete: " .. tostring(complete))

if complete and #result > 0 then
for _, player in ipairs(result) do
TheClassicRace:DebugPrint(" - " .. player.Name .. " lvl" .. player.Level)
end
else
self:BinarySearch()
end
end

self.who(self.min, self.max, cb)
end

function TheClassicRaceScan:BinarySearch()
-- binary search state
self.left = self.min
self.right = self.max

self:Next()
end

function TheClassicRaceScan:Next()
self.m = math.floor((self.left + self.right) / 2)

local function cb(query, result, complete)
TheClassicRace:DebugPrint("who '" .. query .. "' result: " .. #result .. ", complete: " .. tostring(complete))

if complete and #result > 0 then
for _, player in ipairs(result) do
TheClassicRace:DebugPrint(" - " .. player.Name .. " lvl" .. player.Level)
end
end

self:HandleResult(query, result, complete)
end

self.who(self.m, self.max, cb)
end
130 changes: 130 additions & 0 deletions src/core/tracker.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
-- Addon global
local TheClassicRace = _G.TheClassicRace

-- WoW API
local CreateFrame = _G.CreateFrame

---@class TheClassicRaceTracker
---@field DB table<string, table>
---@field Core TheClassicRaceCore
---@field EventBus TheClassicRaceEventBus
---@field Network TheClassicRaceNetwork
local TheClassicRaceTracker = {}
TheClassicRaceTracker.__index = TheClassicRaceTracker
TheClassicRace.Tracker = TheClassicRaceTracker
setmetatable(TheClassicRaceTracker, {
__call = function(cls, ...)
return cls.new(...)
end,
})

function TheClassicRaceTracker.new(Core, DB, EventBus, Network)
local self = setmetatable({}, TheClassicRaceTracker)

self.Core = Core
self.DB = DB
self.EventBus = EventBus
self.Network = Network

-- @TODO: do we need the frame?
self.Frame = CreateFrame("Frame")
self.Frame:SetScript("OnEvent", function()
end)

-- subscribe to network events
EventBus:RegisterCallback(TheClassicRace.Config.Network.Events.Ding, self, self.OnDing)
EventBus:RegisterCallback(TheClassicRace.Config.Network.Events.RequestUpdate, self, self.OnRequestUpdate)
-- subscribe to local events
EventBus:RegisterCallback(TheClassicRace.Config.Events.PlayerInfo, self, self.OnPlayerInfo)

return self
end

function TheClassicRaceTracker:RequestUpdate()
self.Network:SendObject(TheClassicRace.Config.Network.Events.RequestUpdate, {}, "CHANNEL")
end

function TheClassicRaceTracker:OnRequestUpdate(_, sender)
TheClassicRace:DebugPrint("Update Requested")
-- if we don't know a leader yet then we can't respond
if self.DB.realm.leader == nil then
TheClassicRace:DebugPrint("Update Requested, but no leader")
return
end

-- respond with leader
self.Network:SendObject(TheClassicRace.Config.Network.Events.Ding, self.DB.realm.leader, "WHISPER", sender)
end

function TheClassicRaceTracker:OnDing(playerInfo)
self:HandlePlayerInfo({
Name = playerInfo[1],
Level = playerInfo[2],
DingedAt = playerInfo[3],
}, false)
end

function TheClassicRaceTracker:OnPlayerInfo(playerInfo)
self:HandlePlayerInfo(playerInfo, true)
end

function TheClassicRaceTracker:OnMaxLevelBump()
-- we only care about >= highest level - 10
self.DB.realm.levelThreshold = math.max(
self.DB.realm.levelThreshold,
self.DB.realm.highestLevel - 10
)

-- clean our DB of lower level records
for playerName, playerInfo in pairs(self.DB.realm.players) do
if playerInfo.level < self.DB.realm.levelThreshold then
self.DB.realm.players[playerName] = nil
end
end
end

function TheClassicRaceTracker:HandlePlayerInfo(playerInfo, shouldBroadcast)
TheClassicRace:DebugPrint("HandlePlayerInfo: " .. playerInfo.Name .. " lvl" .. playerInfo.Level)
-- ignore players below our lower bound threshold
if playerInfo.Level < self.DB.realm.levelThreshold then
TheClassicRace:DebugPrint("Ignored player info < lvl" .. self.DB.realm.levelThreshold)
return
end

local now = self.Core:Now()
local dingedAt = playerInfo.DingedAt
if dingedAt == nil then
dingedAt = now
end
local isNew = self.DB.realm.players[playerInfo.Name].level == nil
local isDing = not isNew and playerInfo.Level > self.DB.realm.players[playerInfo.Name].level

-- store player info
self.DB.realm.players[playerInfo.Name].level = playerInfo.Level
self.DB.realm.players[playerInfo.Name].lastseenAt = now
if isDing or isNew then
self.DB.realm.players[playerInfo.Name].dingedAt = dingedAt

-- broadcast new/ding
if shouldBroadcast then
self.Network:SendObject(TheClassicRace.Config.Network.Events.Ding,
{ playerInfo.Name, playerInfo.Level, dingedAt }, "CHANNEL")
end
end

-- new highest level! implies ding
if playerInfo.Level > self.DB.realm.highestLevel then
self.DB.realm.highestLevel = playerInfo.Level
self.DB.realm.leader = { playerInfo.Name, playerInfo.Level, dingedAt }

if playerInfo.Level == TheClassicRace.Config.MaxLevel then
TheClassicRace:PPrint("The race is over! Gratz to " .. playerInfo.Name .. ", first to reach max level!!")
else
TheClassicRace:PPrint("Gratz to " .. TheClassicRace:PlayerChatLink(playerInfo.Name) .. ", first to reach level " .. playerInfo.Level .. "!")
end

self:OnMaxLevelBump()
elseif isDing then
TheClassicRace:PPrint("Gratz to " .. playerInfo.Name .. ", reached level " .. playerInfo.Level .. "!")
end
end
Loading

0 comments on commit d0bfa97

Please sign in to comment.