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@27: -- TODO: switch all action functions to use identifiers rather than names John@27: -- TODO: collaborative list trimming John@24: -- TODO: collapse slists into delimited strings for space (premature optimization?) John@18: -- TODO: organize working state data a little more carefully - hard to keep John@18: -- track of all the arrays that are floating out there John@18: John@17: -- holy crap long notes {{{ John@4: -- notes on list storage: John@7: -- Using names as keys as I do now is atrocious. John@4: -- It prevents insertions (twss) to the middle of the list because then it acts John@4: -- as a side effect onto all the others. ie ABCD -> AXBCD would be phrased as John@4: -- "insert X and shift down B,C,D" which sucks. BCD haven't really been affected John@4: -- (yet) because their relative positions to the others are still intact - ie John@4: -- they are still below A right where they belong. But really X hasn't done John@4: -- anything to affect their relative standing. John@4: -- John@4: -- Ok so we can't use names. John@4: -- John@4: -- We can't use monotonic integers either because it suffers the same problem. John@4: -- Also consider, randoming in someone to a list of ABCD. Say they roll spot 2. John@4: -- What if someone else runs a separate raid and also randoms someone into slot John@4: -- 2? How do you handle that conflict? Difficult. Also, consider this: John@4: -- List of ABCD on night 1. John@4: -- Admin 1 on night 2 rolls in 30 new people. ABCD's indexes are shuffled to be John@4: -- between 1-35. John@4: -- Admin 2 on night 3 rolls in 5 new ones and people ABCD and PQRST now all have John@4: -- indexes between 1-9. John@4: -- When these two are resolved against one another, do the 1-9 peopole end up on John@4: -- top of the list compared to those other 30? John@4: -- John@4: -- Solution: John@4: -- Need a huge random space with purposely left gaps to leave plenty of room for John@4: -- conflicts. John@4: -- So if ABCD had randomed on a space of say, 10,000 and then were sorted into John@4: -- order, then the next 30 could roll into that same space and have a proper John@4: -- ordering. Then the next 5, etc. John@4: -- John@4: -- Handling conflicts: John@4: -- John@9: -- Executive decision: random on a range of [0,1], ie math.random John@9: -- then on an add-to-end event just do last + .1 John@9: -- disallow random after any add-to-end event occurs John@9: -- because the list either elongates beyond 1 OR becomes John@9: -- ridiculously bottom heavy, thus meaning that randoms John@9: -- don't get an even distibution from then on (in fact John@9: -- they'll end up getting top favor) John@9: -- * if a stream contains a random-add after an add-to-end John@9: -- it is declared invalid. tough tits. it's just not a fair John@9: -- distribution at that point. John@10: -- * actually, fuck it. I'll give them an unlock command and John@10: -- let them screw over their lists :) John@17: --}}} John@18: John@17: -- there are some dep chains here. for instance, to have a raidIdP value, a John@42: -- person must have a persons value which leads to a personName2id which John@17: -- leads to a raidIdP John@42: local bsk = bsk John@42: local _G=_G John@42: local table=table John@42: local string=string John@0: local tinsert = table.insert John@0: local sformat = string.format John@0: local getn = table.getn John@42: local wipe = wipe John@42: local pairs=pairs John@42: local ipairs=ipairs John@42: local tonumber=tonumber John@42: local tostring=tostring John@42: local time=time John@42: local date=date John@42: local math=math John@42: local type=type John@42: local assert=assert John@42: local getmetatable=getmetatable John@42: local setmetatable=setmetatable John@42: setfenv(1,bsk) John@0: John@70: changeListener = nil -- todo: really should not be scoped like this John@91: timestamp = 0 John@68: John@49: ListEntry = John@49: { John@49: index = 0.0, John@49: id = 0, John@49: } John@49: ListEntry.__index = ListEntry John@49: function ListEntry:new(arg1,arg2) John@49: local n = {} John@49: setmetatable(n,ListEntry) John@49: if type(arg1) == "number" and type(arg2) == "string" then John@49: n.index, n.id = index,id John@49: elseif type(arg1) == "table" and type(arg2) == "nil" then John@49: n.index, n.id = arg1.roll, arg1.id John@49: else John@49: _G.error("Do not know how to construct a ListEntry from " .. type(arg1) .. " and " .. type(arg2)) John@49: end John@49: assert(n.index ~= nil) John@49: assert(n.id ~= nil) John@49: return n John@49: end John@49: function ListEntry:GetId() John@49: return self.id John@49: end John@49: function ListEntry:GetIndex() John@49: return self.index John@49: end John@49: function ListEntry:ReplaceId(newId) -- returns the old Id John@49: local temp = self.id John@49: self.id = newId John@49: return temp John@49: end John@50: function ListEntry:GetClass() -- todo: consider if this is ok John@50: return PersonList:Select(self.id):GetClass() John@50: end John@50: function ListEntry:GetName() -- todo: consider if this is ok John@50: return PersonList:Select(self.id):GetName() John@50: end John@42: John@45: List = John@45: { John@46: name = "", John@49: time = 0, John@49: John@49: -- "private" functions. only private thing about them is that my John@49: -- autocomplete won't pick them up when defined this way John@49: Sort = function(self) John@49: table.sort(self.data,function(a,b) return a:GetIndex() < b:GetIndex() end) John@49: end, John@49: GetLastIndex = function(self) John@49: if not self.data or getn(self.data) == 0 then return 0.0 John@49: else return self.data[#self.data]:GetIndex() end John@49: end, John@49: SetTime = function(self,time) John@49: if time == nil or time == 0 then John@49: assert("Dangerous things are afoot") John@49: else John@49: self.time = time John@49: end John@45: end John@49: John@45: } John@49: List.__index = List John@49: function List:new(arg1, arg2, arg3) John@49: local n = {data={}} John@49: setmetatable(n,List) John@49: if type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "nil" then John@49: n.name = arg1 John@49: n.time = arg2 John@49: elseif type(arg1) == "table" and type(arg2) == "nil" and type(arg3) == "nil" then John@49: n.name = arg1.name John@49: if arg1.data then John@49: for _,v in pairs(arg1.data) do John@49: local le = ListEntry:new(v) John@49: table.insert(n.data,entry) John@49: end John@49: n:Sort() John@49: end John@49: n:SetTime(arg1.time) John@49: else John@49: _G.error(sformat("Do not know how to construct list from: %s and %s and %s", type(arg1), type(arg2), type(arg3))) John@49: end John@45: return n John@45: end John@49: function List:HasId(id) John@49: for le in self:OrderedIdIter() do John@49: if id == le then John@45: return true John@45: end John@45: end John@45: return false John@45: end John@49: function List:GetTime() John@49: return self.time John@49: end John@49: function List:GetName() John@49: return self.name John@49: end John@49: function List:GetId() John@49: local listIndex = LootLists:IdOf(self) -- TODO: undo circular dep somehow John@49: return listIndex John@49: end John@49: function List:Rename(packet,time) John@49: self.name = packet.name John@49: self:SetTime(time) John@49: return packet John@49: end John@49: function List:RenameList(newName) John@49: local listIndex = self:GetId() John@49: return InitiateChange("Rename",self,{listIndex=listIndex,name=newName}) John@49: end John@49: function List:RemoveEntry(entry,time) John@49: local pos = self:Select(entry.id) John@49: if pos then John@49: --print("Removing id " .. entry.id .. " pos " .. pos) John@49: table.remove(self.data,pos) John@49: self:Sort() John@49: self:SetTime(time) John@49: return pos John@49: end John@49: --print("Failed removal of ...") John@49: --PrintTable(entry) John@49: end John@49: function List:Remove(id) John@49: -- TODO: check id John@49: local listIndex = self:GetId() John@49: return InitiateChange("RemoveEntry",self,{listIndex=listIndex,id=id}) John@49: end John@49: function List:InsertEndEntry(packet,time) John@49: if self:InsertEntry(packet,time) then John@49: self.closedRandom = true John@49: return packet John@49: end John@49: end John@49: function List:InsertEntry(packet,time) John@49: local le = ListEntry:new(packet) John@49: table.insert(self.data,le) John@49: self:Sort() John@49: self:SetTime(time) John@49: --printf("Inserting %s to %s", packet.id, packet.listIndex) John@49: return le John@49: end John@49: function List:InsertEnd(id) -- returns the LE created John@49: if self:Select(id) then John@49: printf("Person %s is already on the reqeuested list",id) -- todo: lookup name John@49: return false John@49: end John@49: local index = self:GetLastIndex() + 0.1 John@49: local le = ListEntry:new(index,id) John@49: local listIndex = self:GetId() John@49: return InitiateChange("InsertEndEntry",self,{listIndex=listIndex,id=id,roll=index}) John@49: end John@49: function List:InsertRandom(id) -- returns the LE created John@49: if self.closedRandom then John@49: print("Cannot add person to list by random roll because an add-to-end operation has already occurred") John@49: return false John@49: end John@49: if self:Select(id) then John@49: printf("Person %s is already on the reqeuested list",id) -- todo: lookup name John@49: return false John@49: end John@49: local index = math.random() John@49: local listIndex = self:GetId() John@49: return InitiateChange("InsertEntry",self,{listIndex=listIndex,id=id,roll=index}) John@49: end John@50: function List:GetLength() John@50: return #self.data John@50: end John@49: function List:OrderedListEntryIter() John@49: local i = 0 John@49: local n = #self.data John@49: John@49: return function() John@49: i = i+1 John@49: if i<=n then return self.data[i] end John@49: end John@49: end John@49: function List:OrderedIdIter() John@49: local i = 0 John@49: local n = #self.data John@49: return function() John@49: i = i+1 John@49: if i<=n then return self.data[i]:GetId() end John@49: end John@49: end John@49: function List:GetAllIds() John@49: local t = {} John@49: for id in self:OrderedIdIter() do John@49: table.insert(t,id) John@49: end John@49: return t John@49: end John@49: function List:Select(entry) -- returns the position and the entry John@49: if type(entry) == "string" then -- search by id John@49: local ids = self:GetAllIds() John@49: for i,v in ipairs(ids) do John@49: if v == entry then John@49: return i, self.data[i] John@49: end John@49: end John@49: return false, nil John@49: else John@49: assert("undone") John@49: return false, nil John@49: end John@49: end John@49: function List:Suicide(packet,time) John@49: -- the goal here is to rotate the suicide list by 1 John@49: -- then we can just mash it on top of the intersection between the original John@49: -- list and the working copy John@49: local affected = shallowCopy(packet.affect) John@49: local replacement = shallowCopy(packet.affect) John@49: local temp = table.remove(replacement,1) -- pop John@49: tinsert(replacement,temp) -- push_back John@49: --rintf(("Before suicide of %s on list %s",slist[1],list.name) John@49: --PrintTable(list) John@49: for le in self:OrderedListEntryIter() do John@49: if le:GetId() == affected[1] then John@49: le:ReplaceId(replacement[1]) John@49: table.remove(affected,1) John@49: table.remove(replacement,1) John@49: end John@49: end John@49: -- TODO: flag error if affected and replacement aren't both empty now John@49: self:SetTime(time) John@49: return packet John@49: end John@49: function List:SuicidePerson(id) John@49: -- first calculate the effect, then initiate the change John@49: PersonList:RefreshRaidList() John@49: local slist = {} John@49: local pushing = false John@49: for le in self:OrderedListEntryIter() do -- get all ids John@49: local lid = le:GetId() John@49: if lid == id then John@49: pushing = true John@49: end John@49: if pushing and PersonList:IsActive(lid) then -- TODO: decouple John@49: tinsert(slist,lid) John@49: end John@49: end John@49: local listIndex = self:GetId() John@49: return InitiateChange("Suicide",self,{listIndex=listIndex,affect=slist}) John@49: end John@49: John@49: LootLists = John@49: { John@49: --l = {} -- list of List objects, keyed by id John@49: } John@49: John@49: -- generate self, then generate sublists John@49: function LootLists:ConstructFromDB(db) John@49: self:Reset() John@49: local saved = db.profile.lists John@49: for i,v in pairs(saved) do -- upconvert saved to true list objects John@49: self.l[i] = List:new(v) John@49: end John@49: end John@49: function LootLists:SaveToDB(db) John@49: db.profile.lists = self.l John@49: end John@49: function LootLists:CreateList(packet,time) John@49: local le = List:new(packet.name,time) John@49: if not packet.id then packet.id = time end -- uses the timestamp for the index - it's unique, unlike anything else I can think of John@49: self.l[packet.id] = le John@49: return le John@49: end John@49: function LootLists:Create(name) John@49: return InitiateChange("CreateList",self,{name=name}) John@49: end John@49: function LootLists:DeleteList(packet,time) John@49: self.l[packet.listIndex] = nil John@49: return id John@49: end John@49: function LootLists:Delete(index) John@49: -- TODO: is there anything to check first, or just fire away? John@49: return InitiateChange("DeleteList",self,{listIndex=index}) John@49: end John@49: function LootLists:Select(id) John@49: if type(id) == "number" then -- by id John@49: return self.l[id] John@49: elseif type(id) == "string" then -- name John@49: for i,v in pairs(self.l) do John@49: if v:GetName() == id then John@49: return v John@49: end John@49: end John@49: end John@49: return nil John@49: end John@49: function LootLists:Reset() John@49: self.l = {} John@49: end John@49: function LootLists:GetAllIds() John@49: local t = {} John@49: for i,v in pairs(self.l) do John@49: table.insert(t,i) John@49: end John@49: return t John@49: end John@49: function LootLists:IdOf(list) John@49: for i,v in pairs(self.l) do John@49: if v == list then John@49: return i John@49: end John@49: end John@49: end John@50: -- todo: iterator for lists ... it's pretty necessary John@49: John@49: Toon = John@49: { John@49: id=0, John@49: name="", John@49: class="" John@49: } John@49: Toon.__index = Toon John@49: function Toon:new(arg1,arg2,arg3) John@49: local t = {} John@49: setmetatable(t,Toon) John@49: if type(arg1) == "number" and type(arg2) == "string" and type(arg3) == "string" then John@49: t.id, t.name, t.class = arg1, arg2, arg3 John@49: elseif type(arg1) == "table" and arg2 == nil and arg3 == nil then John@49: t.id, t.name, t.class = arg1.id, arg1.name, arg1.class John@49: else John@49: error("Cannot construct a toon object from types " .. type(arg1) .. ", " .. type(arg2) .. ", " .. type(arg3)) John@49: end John@49: return t John@49: end John@49: function Toon:GetName() John@49: return self.name John@49: end John@49: function Toon:GetId() John@45: return self.id John@45: end John@49: function Toon:GetClass() John@49: return self.class John@45: end John@45: John@49: PersonList = John@49: { John@49: toons = {}, John@49: time = 0, John@49: active = { raid={}, reserve={} } John@49: } John@45: John@49: function PersonList:ConstructFromDB(db) John@49: self:Reset() John@49: local dbp = db.profile.persons John@49: self.time = dbp.time John@49: if dbp.toons == nil then return end John@49: for i,v in pairs(dbp.toons) do John@49: local te = Toon:new(v) John@49: table.insert(self.toons,te) John@49: end John@49: end John@49: function PersonList:SaveToDB(db) John@91: db.profile.persons = { toons=self.toons, time=self.time } -- if this changes, also check the comm functions that send persons John@49: end John@49: function PersonList:Reset() John@49: self.toons = {} John@49: self.time = 0 John@49: self.active = { raid={}, reserve={}, raidExtras={} } John@49: end John@49: function PersonList:Select(id) John@49: -- both id and name are strings, but there won't be clashes John@49: -- because an ID will contain either a number or all caps letters John@49: -- and names must be long enough to ensure that one of those is true John@49: if type(id) == "string" then John@49: for i,v in pairs(self.toons) do John@49: --print(i) John@49: --PrintTable(v) John@49: if v:GetName() == id or v:GetId() == id then John@49: return v John@49: end John@49: end John@49: end John@49: end John@49: function PersonList:AddToon(packet,time) John@49: local te = Toon:new(packet) John@49: table.insert(self.toons,te) John@49: return te John@49: end John@49: function PersonList:Add(name) John@49: local guid = _G.UnitGUID(name) John@49: -- TODO: check guid to be sure it's a player John@49: if not guid then John@49: printf("Could not add player %s - they must be in range or group",name) John@49: return John@49: end John@49: local _,englishClass = _G.UnitClass(name) John@49: --print("Person " .. name .. " is class " .. englishClass) John@49: local id = string.sub(guid,6) -- skip at least 0x0580 ... John@49: id = id:gsub("^0*(.*)","%1") -- nom all leading zeroes remaining John@49: John@49: local pe = self:Select(id) John@49: if pe and pe:GetName() ~= name then John@49: printf("Namechange detected for %s - new is %s, please rename the existing entry", pe:GetName(), name) John@49: return John@49: end John@49: if pe then John@49: printf("%s is already in the persons list; disregarding", name) John@49: return John@49: end John@45: John@49: return InitiateChange("AddToon", self, {name=name, id=id, class=englishClass}) John@49: end John@49: function PersonList:RemoveToon(packet,time) John@49: local id = packet.id John@49: for i,v in pairs(self.toons) do John@49: if v:GetId() == id then John@49: table.remove(self.toons,i) John@49: return v John@49: end John@49: end John@49: end John@49: function PersonList:Remove(ident) John@49: local le = PersonList:Select(ident) John@49: if not le then John@49: printf("%s is not in the persons list, please check your spelling", ident) John@49: return false John@49: end John@49: local id = le:GetId() John@49: local listsTheyreOn = {} John@45: John@49: -- check if they're active on any loot list John@49: local allListIds = LootLists:GetAllIds() John@49: for _,v in pairs(allListIds) do John@49: if LootLists:Select(v):HasId(id) then -- TODO: this is ineloquent John@49: tinsert(listsTheyreOn,LootLists:Select(v):GetName()) John@49: break John@49: end John@49: end John@49: if getn(listsTheyreOn) > 0 then John@49: printf("Cannot remove person %s because they are on one or more lists (%s)",ident,table.concat(listsTheyreOn,", ")) John@49: return false John@49: end John@49: return InitiateChange("RemoveToon", self, {id=id}) John@49: end John@49: function PersonList:IsRegistered(id) John@49: if self:Select(id) ~= nil then return true end John@49: end John@49: function PersonList:AddReserve(id) John@49: local le = self:Select(id) John@49: if le then John@49: -- todo: check that they're not already reserved John@49: self.active.reserve[le:GetId()] = true John@70: return true John@49: end John@49: end John@49: -- todo: remove reserve John@50: function PersonList:IsActive(id) -- todo: support LE as input - saves IsActive(le:GetId()) John@49: return self.active.raid[id] or self.active.reserve[id] John@49: end John@49: function PersonList:AddMissing() John@49: self:RefreshRaidList() John@49: for _,name in pairs(self.active.raidExtras) do John@49: printf("Person %s is missing from the persons list - adding",name) John@49: self:Add(name) John@49: end John@49: -- TODO: batch into a single op - no need to spam 25 messages in a row John@49: end John@49: function PersonList:GetAllActiveIds() John@49: self:RefreshRaidList() John@49: local t = {} John@49: for i,v in pairs(self.active.raid) do John@49: if v then table.insert(t,i) end John@49: end John@49: for i,v in pairs(self.active.reserve) do John@49: if v then table.insert(t,i) end John@49: end John@49: return t John@49: end John@45: John@49: -- The following (adapted) code is from Xinhuan (wowace forum member) John@49: -- Pre-create the unitId strings we will use John@49: local pId = {} John@49: local rId = {} John@49: for i = 1, 4 do John@49: pId[i] = sformat("party%d", i) John@49: end John@49: for i = 1, 40 do John@49: rId[i] = sformat("raid%d", i) John@49: end John@49: function PersonList:RefreshRaidList() John@49: local inParty = _G.GetNumPartyMembers() John@49: local inRaid = _G.GetNumRaidMembers() John@49: local add = function(unitNameArg) John@49: local name = _G.UnitName(unitNameArg) John@49: local te = self:Select(name) John@49: if te then John@49: self.active.raid[te:GetId()]=true John@49: else John@49: table.insert(self.active.raidExtras,name) John@49: end John@49: --if personName2id[name] ~= nil then John@49: -- raidIdP[personName2id[name]]=true John@49: --end John@49: end John@45: John@49: self.active.raid = {} John@49: self.active.raidExtras = {} John@49: if inRaid > 0 then John@49: for i = 1, inRaid do John@49: add(rId[i]) John@49: end John@49: elseif inParty > 0 then John@49: for i = 1, inParty do John@49: add(pId[i]) John@49: end John@49: -- Now add yourself as the last party member John@49: add("player") John@49: else John@49: -- You're alone John@49: add("player") John@49: end John@49: end John@45: John@45: John@49: function GetSafeTimestamp() John@49: local changes = db.profile.changes John@49: local ctime = time() John@49: local n = getn(changes) John@49: if n > 0 then John@49: if changes[n].time >= ctime then John@49: ctime = changes[n].time + 1 John@49: end John@49: end John@49: return ctime John@49: end John@45: John@72: function SetChangeListener(object) -- todo: holy tits this needs to go John@68: changeListener = object -- todo: needs correctness checking, at a minimum John@68: end John@49: function InitiateChange(finalizeAction,acceptor,arg) John@49: local change = {} John@49: change.time = GetSafeTimestamp() John@49: change.action = finalizeAction John@49: change.arg = arg John@45: John@49: if acceptor[finalizeAction](acceptor,arg,change.time) then John@49: table.insert(db.profile.changes,change) John@70: Comm:SendChange(change) John@49: return arg John@49: else John@49: return nil John@49: end John@49: end John@49: function ProcessChange(change) John@49: -- try list-o-lists and persons - if has matching function, call it John@49: local action = change.action John@49: if PersonList[action] then John@49: PersonList[action](PersonList,change.arg,change.time) John@91: timestamp = change.time John@49: return John@49: elseif LootLists[action] then John@49: LootLists[action](LootLists,change.arg,change.time) John@91: timestamp = change.time John@49: return John@49: else John@49: -- pray that the change has a listIndex in it ... John@49: if change.arg.listIndex then John@49: local l = LootLists:Select(change.arg.listIndex) John@49: if l and l[action] then John@49: l[action](l,change.arg,change.time) John@91: timestamp = change.time John@49: return John@49: end John@49: end John@49: end John@49: _G.error("Could not process change: " .. change.action) John@49: end John@42: John@42: function SelfDestruct() John@49: LootLists:Reset() John@49: PersonList:Reset() John@91: db.profile.time = 0 John@42: db.profile.persons = {} John@42: db.profile.changes = {} John@42: db.profile.lists = {} John@17: end John@0: John@1: -- Debugging {{{ John@42: function PrettyPrintList(listIndex) John@49: PersonList:RefreshRaidList() John@49: local le = LootLists:Select(listIndex) John@49: print("List: " .. le:GetName() .. " (" .. le:GetId() .. ") - last modified " .. date("%m/%d/%y %H:%M:%S", le:GetTime()) .. " ("..le:GetTime()..")" ) John@49: local pos = 1 John@49: for i in le:OrderedIdIter() do -- ordered iterator John@49: local s = "" John@49: if PersonList:IsActive(i) then John@49: s = "*" John@49: end John@49: John@49: print(" " .. pos .. " - " .. PersonList:Select(i):GetName() .. " ("..i..")",s) John@49: pos = pos + 1 John@9: end John@9: end John@42: function PrettyPrintLists() John@49: for _,i in pairs(LootLists:GetAllIds()) do John@42: PrettyPrintList(i) John@9: end John@9: end John@42: function PrintLists() John@49: PrintTable(LootLists) John@1: end John@42: function PrintChanges() John@42: PrintTable(db.profile.changes) John@1: end John@42: function PrintPersons() John@49: PrintTable(PersonList) John@1: end John@42: function PrintAPI(object) John@39: for i,v in pairs(object) do John@39: if type(v) == "function" then John@43: print("function "..i.."()") John@39: end John@39: end John@39: end John@0: --}}} John@0: John@43: -- Change processing {{{ John@42: function CreateWorkingStateFromChanges(changes) John@0: -- copy the base to the working state John@49: LootLists:ConstructFromDB(db) John@49: PersonList:ConstructFromDB(db) John@91: timestamp = db.profile.time John@0: John@0: -- now just go through the changes list applying each John@5: for i,v in ipairs(changes) do John@42: ProcessChange(v) John@0: end John@16: end John@16: John@16: --}}} John@49: John@27: -- holy crap long winded {{{ 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@27: --}}} John@0: John@1: -- Action and DoAction defs {{{ John@27: -- Action Discussion {{{ John@0: -- The actual actions for changes start here John@0: -- John@42: -- Each action occurs as a pair of functions. The 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@42: -- The 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@27: -- reverses the prior change on the stack.--}}} John@42: function AddPerson(name)--{{{ John@49: print("Adding ... " .. name) John@49: PersonList:Add(name) John@26: end--}}} John@42: function CreateList(name)--{{{ John@0: -- require admin John@43: print("Creating ... " .. name) John@49: return LootLists:Create(name) John@26: end--}}} John@42: function AddPersonToListEnd(name,listName)--{{{ John@0: -- require admin John@49: local l = LootLists:Select(listName) John@49: local te = PersonList:Select(name) John@49: -- TODO: if not te ... John@49: printf("Adding %s (%s) to list %s", name, te:GetId(), listName) John@49: return l:InsertEnd(te:GetId()) John@26: end--}}} John@42: function AddPersonToListRandom(name,listName)--{{{ John@10: -- require admin John@49: local l = LootLists:Select(listName) John@49: local te = PersonList:Select(name) John@49: -- TODO: if not te ... John@49: printf("Adding %s (%s) to list %s - randomly!", name, te:GetId(), listName) John@49: return l:InsertRandom(te:GetId()) John@26: end--}}} John@42: function SuicidePerson(name,listName)--{{{ John@0: -- require admin John@49: PersonList:RefreshRaidList() John@49: local le = LootLists:Select(listName) John@49: local te = PersonList:Select(name) John@49: return le:SuicidePerson(te:GetId()) John@26: end--}}} John@42: function RenameList(listName,newListName)--{{{ John@20: -- require admin John@49: local le = LootLists:Select(listName) John@49: return le:RenameList(newListName) John@26: end--}}} John@42: function DeleteList(listName)--{{{ John@49: return LootLists:DeleteList(LootLists:Select(listName):GetId()) John@26: end--}}} John@42: function RemovePersonFromList(name,listName)--{{{ John@49: local le = LootLists:Select(listName) John@49: local te = PersonList:Select(name) John@49: return le:Remove(te:GetId()) John@22: end John@20: --}}} John@49: function RemovePerson(person) John@49: print("Removing " .. person) John@49: PersonList:Remove(person) John@49: end John@76: function ReservePerson(person) -- todo: move reserve state to ... State.lua John@49: print("Reserving " .. person) John@70: if PersonList:AddReserve(person) then -- todo: would be better if this were an ID ... John@70: Comm:AddReserve(person) John@70: end John@49: end John@26: --}}} John@20: -- Higher order actions (ie calls other standard actions){{{ John@20: John@42: function TrimLists(time) John@42: if not CheckListCausality() then John@43: print("Unable to trim changelist due to violated causality") John@5: return false John@5: end John@5: John@5: if type(time) ~= "number" then John@5: time = tonumber(time) John@5: end John@5: John@5: -- bisect the changes list by "time" John@5: local before = {} John@91: local lastTime = 0 John@42: for i,v in ipairs(db.profile.changes) do John@5: if v.time <= time then John@5: tinsert(before,v) John@91: lastTime = v.time John@5: else John@5: break John@5: end John@5: end John@5: John@5: -- apply first half John@42: CreateWorkingStateFromChanges(before) John@5: John@5: -- save this state permanently; trim the changes permanently John@49: LootLists:SaveToDB(db) John@49: PersonList:SaveToDB(db) John@42: while db.profile.changes ~= nil and db.profile.changes[1] ~= nil and db.profile.changes[1].time <= time do John@42: table.remove(db.profile.changes,1) John@5: end John@91: db.profile.time = lastTime John@5: John@5: -- using the trimmed list and the new bases, recreate the working state John@42: CreateWorkingStateFromChanges(db.profile.changes) John@5: end John@5: John@29: John@42: function PopulateListRandom(listIndex) John@17: -- difference (raid+reserve)-list, then random shuffle that, then add John@49: local actives = PersonList:GetAllActiveIds() John@49: local list = LootLists:Select(listIndex) John@3: John@49: --swap keys on actives John@49: local t = {} John@49: for _,v in pairs(actives) do t[v] = true end John@17: John@17: -- now remove from t all of the people already present on the list John@49: if t then John@49: for id in list:OrderedIdIter() do -- id iterator John@49: if t[id] then John@49: t[id] = false John@21: end John@17: end John@17: end John@17: John@17: -- add all remaining John@17: for i,v in pairs(t) do John@17: if v then John@49: AddPersonToListRandom(i,list:GetId()) John@17: end John@17: end John@3: end John@42: function NukePerson(name) -- delete from all lists and then from persons John@49: for _,id in pairs(LootLists:GetAllIds()) do John@49: RemovePersonFromList(name,id) John@30: end John@42: RemovePerson(name) John@30: end John@1: --}}} 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@5: -- returns true if the events in the list are in time order John@42: function CheckListCausality() John@5: local t = nil John@42: for i,v in ipairs(db.profile.changes) do John@5: if t ~= nil then John@5: if v.time <= t then John@5: return false John@5: end John@5: end John@5: t = v.time John@5: end John@5: return true John@5: end John@0: