John@0: -- lists consist of three things John@0: -- 1) a base state - agreed on by one or more list holders John@0: -- 2) change sets - incremental list changes (can be rolled forwards or John@0: -- backwards) John@0: -- 3) working state - not saved because it can be so easily calculated John@0: -- John@0: -- A separate user list is held - lists index into this John@0: John@0: John@0: -- TODO: rename player John@0: John@0: John@0: John@0: bsk.lists = {} John@0: bsk.players = {} John@0: John@0: local RaidList = {} John@0: local ReserveList = {} John@0: local activeList = 0 -- temporary John@0: John@0: local tinsert = table.insert John@0: local sformat = string.format John@0: local getn = table.getn John@0: John@0: function bsk:tcopy(to, from) John@0: for k,v in pairs(from) do John@0: if(type(v)=="table") then John@0: to[k] = {} John@0: bsk:tcopy(to[k], v); John@0: else John@0: to[k] = v; John@0: end John@0: end John@0: end John@0: local shallowCopy = function(t) John@0: local u = { } John@0: for k, v in pairs(t) do u[k] = v end John@0: return setmetatable(u, getmetatable(t)) John@0: end John@0: John@1: -- Debugging {{{ John@1: function bsk:PrintLists() John@1: bsk:PrintTable(bsk.lists) John@1: end John@1: function bsk:PrintChanges() John@1: bsk:PrintTable(bsk.db.profile.changes) John@1: end John@1: function bsk:PrintPlayers() John@1: bsk:PrintTable(bsk.players) John@1: end John@0: function bsk:PrintTable(table, depth) John@0: depth = depth or "" John@0: if not table then return end John@0: for i,v in pairs(table) do John@0: if( type(v) == "string" ) then John@0: self:Print(depth .. i .. " - " .. v) John@0: elseif( type(v) == "number" ) then John@0: self:Print(depth .. i .. " - " .. tostring(v)) John@0: elseif( type(v) == "table" ) then John@0: self:Print(depth .. i .." - ") John@0: self:PrintTable(v,depth.." ") John@0: elseif( type(v) == "boolean" ) then John@0: self:Print(depth .. i .. " - " .. tostring(v)) John@0: else John@0: self:Print(depth .. i .. " - not sure how to print type: " .. type(v) ) John@0: end John@0: end John@0: end John@0: John@0: --}}} John@0: John@0: function bsk:CreateWorkingStateFromChanges() John@0: local playerBase = self.db.profile.players John@0: local listBase = self.db.profile.listBase John@0: local changes = self.db.profile.changes John@0: John@0: -- copy the base to the working state John@0: wipe(bsk.lists) John@0: wipe(bsk.players) John@0: bsk:tcopy(bsk.lists,listBase) John@0: bsk:tcopy(bsk.players,playerBase) John@0: John@0: -- now just go through the changes list applying each John@0: for i,v in pairs(changes) do John@0: bsk:ProcessChange(v) John@0: end John@0: end John@0: John@0: function bsk:CreateChange(change) John@0: -- sanity John@0: assert(change) John@0: assert(change.action) John@0: assert(change.arg) John@0: John@0: bsk:StartChange(change) John@0: bsk:CommitChange(change) John@0: end John@0: John@0: function bsk:StartChange(change) John@0: local changes = self.db.profile.changes John@0: change.time = time() John@0: local n = getn(changes) John@0: if n > 0 then John@0: if changes[n].time >= change.time then John@0: change.time = changes[n].time + 1 John@0: end John@0: end John@0: end John@0: John@0: function bsk:CommitChange(change) John@0: local changes = self.db.profile.changes John@0: tinsert(changes,change) John@0: -- TODO: broadcast change John@0: end John@0: John@0: John@0: -- timestamp logic: John@0: -- use time() for comparisons - local clients use date() to make it pretty. only John@0: -- dowisde - we can't have a server timestamp. Which kind of sucks, but it turns John@0: -- out you can change timezones when you enter an instance server, so you really John@0: -- never know what time it is. John@0: -- There's unfortunately no hard-and-proven method for determining the true time John@0: -- difference between local time and server time. You can't just query the two John@0: -- and compare them because your server timezone can change (!) if you go into John@0: -- an instance server with a different timezone. This is apparently a big John@0: -- problem on Oceanic realms. John@0: -- John@0: -- Timestamp handling (brainstorming how to deal with drift): John@0: -- (not an issue) if someone sends you time in the future, update your offset so you won't John@0: -- send out events in the "past" to that person John@0: -- (not an issue - using local UTC now) on change-zone-event: check if you've changed timezones - might need update John@0: -- each time you add a change, check the tail of the change list; if this is John@0: -- less than that, you have a problem. Print a message. if this is equal, then John@0: -- that's ok, just bump it by 1 second. This could happen in the case of, say, John@0: -- spam-clicking the undo button or adding names to the list. The recipients John@0: -- should be ok with this since they'll follow the same algorithm. The only John@0: -- real chance for a problem is if two people click within the 1 second window? John@0: -- if someone sends you a past event, John@0: -- it's ok if it's newer than anything in the changes list John@0: -- otherwise ... causality has been violated. John@0: -- Whenever an admin signon event happens, have the admins each perform a John@0: -- timestamp check. Issue warnings for anyone with a clock that's more than John@0: -- X seconds out of sync with the others. Seriously, why isn't NTP a standard John@0: -- setting on all operating systems ... John@0: John@0: function bsk:ProcessChange(change) John@0: if change.action == "AddPlayer" then John@0: bsk:DoAddPlayer(change) John@0: elseif change.action == "CreateList" then John@0: bsk:DoCreateList(change) John@0: elseif change.action == "AddPlayerToList" then John@0: bsk:DoAddPlayerToList(change) John@0: elseif change.action == "SuicidePlayer" then John@0: bsk:DoSuicidePlayer(change) John@0: else John@0: bsk:Print("Unknown message encountered") John@0: bsk:PrintTable(change) John@0: assert(false) John@0: end John@0: end John@0: John@1: -- Action and DoAction defs {{{ John@0: -- John@0: -- The actual actions for changes start here John@0: -- John@0: -- Each action occurs as a pair of functions. The bsk:Action() function is from John@0: -- a list admin's point of view. Each will check for admin status, then create a John@0: -- change bundle, call the handler for that change (ie the DoAction func), and John@0: -- then record/transmist the bundle. These are simple and repetitive functions. John@0: -- John@0: -- The bsk:DoAction() function is tasked with executing the bundle and is what John@0: -- non-admins and admins alike will call to transform their working state via a John@0: -- change packet. Each Do() function will accept *only* a change packet, and John@0: -- it's assumed that the change has been vetted elsewhere. These are very blunt John@0: -- routines. John@0: -- John@0: -- Note that "undo" has no special voodoo to it. It's basically a change that John@0: -- reverses the prior change on the stack. John@0: John@0: -- Players list John@0: function bsk:DoAddPlayer(change) John@0: assert(change) John@0: assert(change.arg.guid) John@0: local arg = change.arg John@0: -- require admin John@0: local players = bsk.players John@0: local name = arg.name John@0: local guid = arg.guid John@0: assert(players[guid]==nil) John@0: players[guid] = name John@0: players.time=change.time John@0: return true John@0: end John@0: John@0: function bsk:AddPlayer(name) John@0: local players = bsk.players John@0: local guid = UnitGUID(name) John@0: -- TODO: check guid to be sure it's a player John@0: if not guid then John@0: self:Print(sformat("Could not add player %s - they must be in range or group",name)) John@0: return John@0: end John@0: if players[guid] and players[guid] ~= name then John@0: self:Print(sformat("Namechange detected for %s - new is %s, please rename the existing entry", players[guid], name)) John@0: return John@0: end John@0: if players[guid] ~= nil then John@0: self:Print(sformat("%s is already in the players list; disregarding", name)) John@0: return John@0: end John@0: local change = {action="AddPlayer",arg={name=name,guid=guid}} John@0: if bsk:DoAddPlayer(change) then John@0: bsk:CreateChange(change) John@0: end John@0: end John@0: John@0: function bsk:DoCreateList(change) John@0: -- TODO: this segment will probably be useful as bsk:SearchForListByName John@0: local lists = bsk.lists John@0: for i,v in pairs(lists) do John@0: if v.name == change.arg.name then John@0: self:Print(sformat("List %s already exists",v.name)) John@0: return false John@0: end John@0: end John@0: tinsert(lists,{name=change.arg.name,time=change.time}) John@0: return true John@0: end John@0: John@0: function bsk:CreateList(name) John@0: -- require admin John@0: local change={action="CreateList",arg={name=name}} John@0: bsk:StartChange(change) John@0: self:Print("Creating ... " .. name) John@0: if bsk:DoCreateList(change) then John@0: bsk:CommitChange(change) John@0: end John@0: end John@0: John@0: function bsk:DoAddPlayerToList(change) John@0: local listIndex = change.arg.listIndex John@0: local slist = change.arg.slist John@0: local list = bsk.lists[listIndex] John@0: John@0: if #slist == 1 then -- end of list insertion - just one person John@0: tinsert(list,slist[1]) John@0: list.time = change.time John@0: else John@0: self:Print("Adding to middle of list is not yet supported") John@0: return false John@0: end John@0: return true John@0: end John@0: John@0: function bsk:AddPlayerToList(name,list) John@0: -- require admin John@0: local listIndex = bsk:GetListIndex(list) John@0: local slist = {name} -- TODO: support adding to elsewhere besides the end John@0: local change = {action="AddPlayerToList",arg={name=name,listIndex=listIndex,slist=slist}} John@0: bsk:StartChange(change) John@0: if bsk:DoAddPlayerToList(change) then John@0: bsk:CommitChange(change) John@0: end John@0: end John@0: John@0: function bsk:DoRemovePlayer(change) John@0: John@0: -- return true John@0: end John@0: John@0: function bsk:RemovePlayer(name) John@0: -- from both players and lists John@0: end John@0: John@0: function bsk:DoSuicidePlayer(change) John@0: local listIndex = change.arg.listIndex John@0: local list = bsk.lists[listIndex] John@0: local slist = shallowCopy(change.arg.list) John@0: -- the goal here is to rotate the suicide list by 1 John@0: -- then we can just mash it on top of the intersection between the original John@0: -- list and the working copy John@0: local stemp = shallowCopy(change.arg.list) John@0: local temp = table.remove(stemp,1) -- pop John@0: tinsert(stemp,temp) -- push_back John@0: --bsk:Print(sformat("Before suicide of %s on list %s",slist[1],list.name)) John@0: --bsk:PrintTable(list) John@0: for i = 1, #list do John@0: if list[i] == slist[1] then John@0: table.remove(slist,1) John@0: list[i] = stemp[1] John@0: table.remove(stemp,1) John@0: end John@0: end John@0: list.time=change.time John@0: --bsk:Print("After") John@0: --bsk:PrintTable(list) John@0: return true John@0: end John@0: John@0: function bsk:SuicidePlayer(name,list) John@0: -- require admin John@0: bsk:PopulateRaidList() John@0: local listIndex = bsk:GetListIndex(list) John@1: local slist=bsk:GetSuicideList(name,bsk.lists[listIndex]) John@0: local change = {action="SuicidePlayer",arg={names=names,list=slist,listIndex=listIndex}} John@0: bsk:StartChange(change) John@0: if bsk:DoSuicidePlayer(change) then John@0: bsk:CommitChange(change) John@0: end John@0: end John@1: --}}} John@1: -- Higher order actions (ie calls other Doers){{{ John@1: function bsk:AddMissingPlayers() John@1: bsk:PopulateRaidList() John@1: local t = {} John@1: for i,v in pairs(bsk.players) do John@1: t[v] = true John@1: end John@1: for i,v in pairs(RaidList) do John@2: if t[i] == nil then John@2: bsk:Print(sformat("Player %s is missing from the players list - adding",i)) John@2: bsk:AddPlayer(i) John@1: end John@1: end John@1: -- TODO: batch into a single op - no need to spam 25 messages in a row John@1: end John@1: --}}} John@1: John@1: -- "Soft" actions- ie things that cause nonpermanent state {{{ John@1: John@1: -- reserves John@1: function bsk:AddReserve(name) John@1: ReserveList[name]=true John@1: -- TODO: communicate to others. don't store this in any way. John@1: end John@1: John@1: function bsk:RemoveReserve(name) John@1: ReserveList[name]=false John@1: -- TODO: communicate to others. don't store this in any way. John@1: end John@1: John@1: John@1: --function bsk:GetActiveList() John@1: -- return bsk.lists[1] -- todo! John@1: --end John@1: John@1: --}}} John@0: John@0: -- The following code is from Xinhuan (wowace forum member) John@0: -- Pre-create the unitID strings we will use John@0: local pID = {} John@0: local rID = {} John@0: for i = 1, 4 do John@0: pID[i] = format("party%d", i) John@0: end John@0: for i = 1, 40 do John@0: rID[i] = format("raid%d", i) John@0: end John@0: function bsk:PopulateRaidList() John@0: local inParty = GetNumPartyMembers() John@0: local inRaid = GetNumRaidMembers() John@0: John@0: wipe(RaidList) John@0: if inRaid > 0 then John@0: for i = 1, inRaid do John@0: RaidList[UnitName(rID[i])]=true John@0: end John@0: elseif inParty > 0 then John@0: for i = 1, inParty do John@0: RaidList[UnitName(pID[i])]=true John@0: end John@0: -- Now add yourself as the last party member John@0: RaidList[UnitName("player")]=true John@0: else John@0: -- You're alone John@0: RaidList[UnitName("player")]=true John@0: end John@0: end John@0: John@0: -- undo rules! John@0: -- only the most recent event can be undone John@0: -- ^^^ on a given list? John@0: -- algorithm is easy, given "Suicide A B C" John@0: -- just find A,B,C in the list and replace in order from the s message John@0: -- while undo is allowed *per-list*, certain events in the stream will John@0: -- prevent proper undo, such as add/delete player or add/delete list John@0: John@0: John@1: function bsk:GetSuicideList(name,list) John@1: --self:Print("Calculating changeset for "..name.." from list -") John@1: --self:PrintTable(list) John@1: local t = {} John@1: local ret = {} John@1: local pushing = false John@1: for i = 1, #list do John@1: if list[i] == name then John@1: pushing = true John@1: end John@1: if pushing and (RaidList[list[i]] or ReserveList[list[i]]) then John@1: tinsert(ret,list[i]) John@1: end John@1: end John@1: return ret John@0: end John@0: John@0: John@0: John@0: -- Support functions John@0: John@0: function bsk:GetListIndex(name) John@0: for i,v in pairs(bsk.lists) do John@0: if v.name == name then John@0: return i John@0: end John@0: end John@0: assert(false) John@0: end John@1: