Mercurial > wow > breuesk
view Lists.lua @ 105:c6c748a5823b tip
Collaborative list trimming 80% functional
Updates for the zone-in problem
author | John@Yosemite-PC |
---|---|
date | Sun, 06 May 2012 08:33:34 -0400 |
parents | 25c127c4c1e8 |
children |
line wrap: on
line source
-- lists consist of three things -- 1) a base state - agreed on by one or more list holders -- 2) change sets - incremental list changes (can be rolled forwards or -- backwards) -- 3) working state - not saved because it can be so easily calculated -- -- A separate user list is held - lists index into this -- TODO: switch all action functions to use identifiers rather than names -- TODO: collapse slists into delimited strings for space (premature optimization?) -- TODO: organize working state data a little more carefully - hard to keep -- track of all the arrays that are floating out there -- holy crap long notes {{{ -- notes on list storage: -- Using names as keys as I do now is atrocious. -- It prevents insertions (twss) to the middle of the list because then it acts -- as a side effect onto all the others. ie ABCD -> AXBCD would be phrased as -- "insert X and shift down B,C,D" which sucks. BCD haven't really been affected -- (yet) because their relative positions to the others are still intact - ie -- they are still below A right where they belong. But really X hasn't done -- anything to affect their relative standing. -- -- Ok so we can't use names. -- -- We can't use monotonic integers either because it suffers the same problem. -- Also consider, randoming in someone to a list of ABCD. Say they roll spot 2. -- What if someone else runs a separate raid and also randoms someone into slot -- 2? How do you handle that conflict? Difficult. Also, consider this: -- List of ABCD on night 1. -- Admin 1 on night 2 rolls in 30 new people. ABCD's indexes are shuffled to be -- between 1-35. -- Admin 2 on night 3 rolls in 5 new ones and people ABCD and PQRST now all have -- indexes between 1-9. -- When these two are resolved against one another, do the 1-9 peopole end up on -- top of the list compared to those other 30? -- -- Solution: -- Need a huge random space with purposely left gaps to leave plenty of room for -- conflicts. -- So if ABCD had randomed on a space of say, 10,000 and then were sorted into -- order, then the next 30 could roll into that same space and have a proper -- ordering. Then the next 5, etc. -- -- Handling conflicts: -- -- Executive decision: random on a range of [0,1], ie math.random -- then on an add-to-end event just do last + .1 -- disallow random after any add-to-end event occurs -- because the list either elongates beyond 1 OR becomes -- ridiculously bottom heavy, thus meaning that randoms -- don't get an even distibution from then on (in fact -- they'll end up getting top favor) -- * if a stream contains a random-add after an add-to-end -- it is declared invalid. tough tits. it's just not a fair -- distribution at that point. -- * actually, fuck it. I'll give them an unlock command and -- let them screw over their lists :) --}}} -- there are some dep chains here. for instance, to have a raidIdP value, a -- person must have a persons value which leads to a personName2id which -- leads to a raidIdP local bsk = bsk local _G=_G local table=table local string=string local tinsert = table.insert local sformat = string.format local getn = table.getn local wipe = wipe local pairs=pairs local ipairs=ipairs local tonumber=tonumber local tostring=tostring local time=time local date=date local math=math local type=type local assert=assert local getmetatable=getmetatable local setmetatable=setmetatable setfenv(1,bsk) changeListener = nil -- todo: really should not be scoped like this timestamp = 0 ListEntry = { index = 0.0, id = 0, } ListEntry.__index = ListEntry function ListEntry:new(arg1,arg2) local n = {} setmetatable(n,ListEntry) if type(arg1) == "number" and type(arg2) == "string" then n.index, n.id = index,id elseif type(arg1) == "table" and type(arg2) == "nil" then n.index, n.id = arg1.roll, arg1.id else _G.error("Do not know how to construct a ListEntry from " .. type(arg1) .. " and " .. type(arg2)) end assert(n.index ~= nil) assert(n.id ~= nil) return n end function ListEntry:GetId() return self.id end function ListEntry:GetIndex() return self.index end function ListEntry:ReplaceId(newId) -- returns the old Id local temp = self.id self.id = newId return temp end function ListEntry:GetClass() -- todo: consider if this is ok return PersonList:Select(self.id):GetClass() end function ListEntry:GetName() -- todo: consider if this is ok return PersonList:Select(self.id):GetName() end List = { name = "", time = 0, -- "private" functions. only private thing about them is that my -- autocomplete won't pick them up when defined this way Sort = function(self) table.sort(self.data,function(a,b) return a:GetIndex() < b:GetIndex() end) end, GetLastIndex = function(self) if not self.data or getn(self.data) == 0 then return 0.0 else return self.data[#self.data]:GetIndex() end end, SetTime = function(self,time) if time == nil or time == 0 then assert("Dangerous things are afoot") else self.time = time end end } List.__index = List function List:new(arg1, arg2, arg3) local n = {data={}} setmetatable(n,List) if type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "nil" then n.name = arg1 n.time = arg2 elseif type(arg1) == "table" and type(arg2) == "nil" and type(arg3) == "nil" then n.name = arg1.name if arg1.data then for _,v in pairs(arg1.data) do local le = ListEntry:new(v) table.insert(n.data,entry) end n:Sort() end n:SetTime(arg1.time) else _G.error(sformat("Do not know how to construct list from: %s and %s and %s", type(arg1), type(arg2), type(arg3))) end return n end function List:HasId(id) for le in self:OrderedIdIter() do if id == le then return true end end return false end function List:GetTime() return self.time end function List:GetName() return self.name end function List:GetId() local listIndex = LootLists:IdOf(self) -- TODO: undo circular dep somehow return listIndex end function List:Rename(packet,time) self.name = packet.name self:SetTime(time) return packet end function List:RenameList(newName) local listIndex = self:GetId() return InitiateChange("Rename",self,{listIndex=listIndex,name=newName}) end function List:RemoveEntry(entry,time) local pos = self:Select(entry.id) if pos then --print("Removing id " .. entry.id .. " pos " .. pos) table.remove(self.data,pos) self:Sort() self:SetTime(time) return pos end --print("Failed removal of ...") --PrintTable(entry) end function List:Remove(id) -- TODO: check id local listIndex = self:GetId() return InitiateChange("RemoveEntry",self,{listIndex=listIndex,id=id}) end function List:InsertEndEntry(packet,time) if self:InsertEntry(packet,time) then self.closedRandom = true return packet end end function List:InsertEntry(packet,time) local le = ListEntry:new(packet) table.insert(self.data,le) self:Sort() self:SetTime(time) --printf("Inserting %s to %s", packet.id, packet.listIndex) return le end function List:InsertEnd(id) -- returns the LE created if self:Select(id) then printf("Person %s is already on the reqeuested list",id) -- todo: lookup name return false end local index = self:GetLastIndex() + 0.1 local le = ListEntry:new(index,id) local listIndex = self:GetId() return InitiateChange("InsertEndEntry",self,{listIndex=listIndex,id=id,roll=index}) end function List:InsertRandom(id) -- returns the LE created if self.closedRandom then print("Cannot add person to list by random roll because an add-to-end operation has already occurred") return false end if self:Select(id) then printf("Person %s is already on the reqeuested list",id) -- todo: lookup name return false end local index = math.random() local listIndex = self:GetId() return InitiateChange("InsertEntry",self,{listIndex=listIndex,id=id,roll=index}) end function List:GetLength() return #self.data end function List:OrderedListEntryIter() local i = 0 local n = #self.data return function() i = i+1 if i<=n then return self.data[i] end end end function List:OrderedIdIter() local i = 0 local n = #self.data return function() i = i+1 if i<=n then return self.data[i]:GetId() end end end function List:GetAllIds() local t = {} for id in self:OrderedIdIter() do table.insert(t,id) end return t end function List:Select(entry) -- returns the position and the entry if type(entry) == "string" then -- search by id local ids = self:GetAllIds() for i,v in ipairs(ids) do if v == entry then return i, self.data[i] end end return false, nil else assert("undone") return false, nil end end function List:Suicide(packet,time) -- the goal here is to rotate the suicide list by 1 -- then we can just mash it on top of the intersection between the original -- list and the working copy local affected = shallowCopy(packet.affect) local replacement = shallowCopy(packet.affect) local temp = table.remove(replacement,1) -- pop tinsert(replacement,temp) -- push_back --rintf(("Before suicide of %s on list %s",slist[1],list.name) --PrintTable(list) for le in self:OrderedListEntryIter() do if le:GetId() == affected[1] then le:ReplaceId(replacement[1]) table.remove(affected,1) table.remove(replacement,1) end end -- TODO: flag error if affected and replacement aren't both empty now self:SetTime(time) return packet end function List:SuicidePerson(id) -- first calculate the effect, then initiate the change PersonList:RefreshRaidList() local slist = {} local pushing = false for le in self:OrderedListEntryIter() do -- get all ids local lid = le:GetId() if lid == id then pushing = true end if pushing and PersonList:IsActive(lid) then -- TODO: decouple tinsert(slist,lid) end end local listIndex = self:GetId() return InitiateChange("Suicide",self,{listIndex=listIndex,affect=slist}) end LootLists = { --l = {} -- list of List objects, keyed by id } -- generate self, then generate sublists function LootLists:ConstructFromDB(db) self:Reset() local saved = db.profile.lists for i,v in pairs(saved) do -- upconvert saved to true list objects self.l[i] = List:new(v) end end function LootLists:SaveToDB(db) db.profile.lists = self.l end function LootLists:CreateList(packet,time) local le = List:new(packet.name,time) 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 self.l[packet.id] = le return le end function LootLists:Create(name) return InitiateChange("CreateList",self,{name=name}) end function LootLists:DeleteList(packet,time) self.l[packet.listIndex] = nil return id end function LootLists:Delete(index) -- TODO: is there anything to check first, or just fire away? return InitiateChange("DeleteList",self,{listIndex=index}) end function LootLists:Select(id) if type(id) == "number" then -- by id return self.l[id] elseif type(id) == "string" then -- name for i,v in pairs(self.l) do if v:GetName() == id then return v end end end return nil end function LootLists:Reset() self.l = {} end function LootLists:GetAllIds() local t = {} for i,v in pairs(self.l) do table.insert(t,i) end return t end function LootLists:IdOf(list) for i,v in pairs(self.l) do if v == list then return i end end end -- todo: iterator for lists ... it's pretty necessary Toon = { id=0, name="", class="" } Toon.__index = Toon function Toon:new(arg1,arg2,arg3) local t = {} setmetatable(t,Toon) if type(arg1) == "number" and type(arg2) == "string" and type(arg3) == "string" then t.id, t.name, t.class = arg1, arg2, arg3 elseif type(arg1) == "table" and arg2 == nil and arg3 == nil then t.id, t.name, t.class = arg1.id, arg1.name, arg1.class else error("Cannot construct a toon object from types " .. type(arg1) .. ", " .. type(arg2) .. ", " .. type(arg3)) end return t end function Toon:GetName() return self.name end function Toon:GetId() return self.id end function Toon:GetClass() return self.class end PersonList = { toons = {}, time = 0, active = { raid={}, reserve={} } } function PersonList:ConstructFromDB(db) self:Reset() local dbp = db.profile.persons self.time = dbp.time if dbp.toons == nil then return end for i,v in pairs(dbp.toons) do local te = Toon:new(v) table.insert(self.toons,te) end end function PersonList:SaveToDB(db) db.profile.persons = { toons=self.toons, time=self.time } -- if this changes, also check the comm functions that send persons end function PersonList:Reset() self.toons = {} self.time = 0 self.active = { raid={}, reserve={}, raidExtras={} } end function PersonList:Select(id) -- both id and name are strings, but there won't be clashes -- because an ID will contain either a number or all caps letters -- and names must be long enough to ensure that one of those is true if type(id) == "string" then for i,v in pairs(self.toons) do --print(i) --PrintTable(v) if v:GetName() == id or v:GetId() == id then return v end end end end function PersonList:AddToon(packet,time) local te = Toon:new(packet) table.insert(self.toons,te) return te end function PersonList:Add(name) local guid = _G.UnitGUID(name) -- TODO: check guid to be sure it's a player if not guid then printf("Could not add player %s - they must be in range or group",name) return end local _,englishClass = _G.UnitClass(name) --print("Person " .. name .. " is class " .. englishClass) local id = string.sub(guid,6) -- skip at least 0x0580 ... id = id:gsub("^0*(.*)","%1") -- nom all leading zeroes remaining local pe = self:Select(id) if pe and pe:GetName() ~= name then printf("Namechange detected for %s - new is %s, please rename the existing entry", pe:GetName(), name) return end if pe then printf("%s is already in the persons list; disregarding", name) return end return InitiateChange("AddToon", self, {name=name, id=id, class=englishClass}) end function PersonList:RemoveToon(packet,time) local id = packet.id for i,v in pairs(self.toons) do if v:GetId() == id then table.remove(self.toons,i) return v end end end function PersonList:Remove(ident) local le = PersonList:Select(ident) if not le then printf("%s is not in the persons list, please check your spelling", ident) return false end local id = le:GetId() local listsTheyreOn = {} -- check if they're active on any loot list local allListIds = LootLists:GetAllIds() for _,v in pairs(allListIds) do if LootLists:Select(v):HasId(id) then -- TODO: this is ineloquent tinsert(listsTheyreOn,LootLists:Select(v):GetName()) break end end if getn(listsTheyreOn) > 0 then printf("Cannot remove person %s because they are on one or more lists (%s)",ident,table.concat(listsTheyreOn,", ")) return false end return InitiateChange("RemoveToon", self, {id=id}) end function PersonList:IsRegistered(id) if self:Select(id) ~= nil then return true end end function PersonList:AddReserve(id) local le = self:Select(id) if le then -- todo: check that they're not already reserved self.active.reserve[le:GetId()] = true return true end end -- todo: remove reserve function PersonList:IsActive(id) -- todo: support LE as input - saves IsActive(le:GetId()) return self.active.raid[id] or self.active.reserve[id] end function PersonList:AddMissing() self:RefreshRaidList() for _,name in pairs(self.active.raidExtras) do printf("Person %s is missing from the persons list - adding",name) self:Add(name) end -- TODO: batch into a single op - no need to spam 25 messages in a row end function PersonList:GetAllActiveIds() self:RefreshRaidList() local t = {} for i,v in pairs(self.active.raid) do if v then table.insert(t,i) end end for i,v in pairs(self.active.reserve) do if v then table.insert(t,i) end end return t end -- The following (adapted) code is from Xinhuan (wowace forum member) -- Pre-create the unitId strings we will use local pId = {} local rId = {} for i = 1, 4 do pId[i] = sformat("party%d", i) end for i = 1, 40 do rId[i] = sformat("raid%d", i) end function PersonList:RefreshRaidList() local inParty = _G.GetNumPartyMembers() local inRaid = _G.GetNumRaidMembers() local add = function(unitNameArg) local name = _G.UnitName(unitNameArg) local te = self:Select(name) if te then self.active.raid[te:GetId()]=true else table.insert(self.active.raidExtras,name) end --if personName2id[name] ~= nil then -- raidIdP[personName2id[name]]=true --end end self.active.raid = {} self.active.raidExtras = {} if inRaid > 0 then for i = 1, inRaid do add(rId[i]) end elseif inParty > 0 then for i = 1, inParty do add(pId[i]) end -- Now add yourself as the last party member add("player") else -- You're alone add("player") end end function GetSafeTimestamp() local changes = db.profile.changes local ctime = time() local n = getn(changes) if n > 0 then if changes[n].time >= ctime then ctime = changes[n].time + 1 end end return ctime end function SetChangeListener(object) -- todo: holy tits this needs to go changeListener = object -- todo: needs correctness checking, at a minimum end function InitiateChange(finalizeAction,acceptor,arg) local change = {} change.time = GetSafeTimestamp() change.action = finalizeAction change.arg = arg if acceptor[finalizeAction](acceptor,arg,change.time) then table.insert(db.profile.changes,change) Comm:SendChange(change) return arg else return nil end end function ProcessChange(change) -- try list-o-lists and persons - if has matching function, call it local action = change.action if PersonList[action] then PersonList[action](PersonList,change.arg,change.time) timestamp = change.time return elseif LootLists[action] then LootLists[action](LootLists,change.arg,change.time) timestamp = change.time return else -- pray that the change has a listIndex in it ... if change.arg.listIndex then local l = LootLists:Select(change.arg.listIndex) if l and l[action] then l[action](l,change.arg,change.time) timestamp = change.time return end end end _G.error("Could not process change: " .. change.action) end function SelfDestruct() LootLists:Reset() PersonList:Reset() db.profile.time = 0 db.profile.persons = {} db.profile.changes = {} db.profile.lists = {} end -- Debugging {{{ function PrettyPrintList(listIndex) PersonList:RefreshRaidList() local le = LootLists:Select(listIndex) print("List: " .. le:GetName() .. " (" .. le:GetId() .. ") - last modified " .. date("%m/%d/%y %H:%M:%S", le:GetTime()) .. " ("..le:GetTime()..")" ) local pos = 1 for i in le:OrderedIdIter() do -- ordered iterator local s = "" if PersonList:IsActive(i) then s = "*" end print(" " .. pos .. " - " .. PersonList:Select(i):GetName() .. " ("..i..")",s) pos = pos + 1 end end function PrettyPrintLists() for _,i in pairs(LootLists:GetAllIds()) do PrettyPrintList(i) end end function PrintLists() PrintTable(LootLists) end function PrintChanges() PrintTable(db.profile.changes) end function PrintPersons() PrintTable(PersonList) end function PrintAPI(object) for i,v in pairs(object) do if type(v) == "function" then print("function "..i.."()") end end end --}}} -- Change processing {{{ function CreateWorkingStateFromChanges(changes) -- copy the base to the working state LootLists:ConstructFromDB(db) PersonList:ConstructFromDB(db) timestamp = db.profile.time -- now just go through the changes list applying each for i,v in ipairs(changes) do ProcessChange(v) end end --}}} -- holy crap long winded {{{ -- timestamp logic: -- use time() for comparisons - local clients use date() to make it pretty. only -- dowisde - we can't have a server timestamp. Which kind of sucks, but it turns -- out you can change timezones when you enter an instance server, so you really -- never know what time it is. -- There's unfortunately no hard-and-proven method for determining the true time -- difference between local time and server time. You can't just query the two -- and compare them because your server timezone can change (!) if you go into -- an instance server with a different timezone. This is apparently a big -- problem on Oceanic realms. -- -- Timestamp handling (brainstorming how to deal with drift): -- (not an issue) if someone sends you time in the future, update your offset so you won't -- send out events in the "past" to that person -- (not an issue - using local UTC now) on change-zone-event: check if you've changed timezones - might need update -- each time you add a change, check the tail of the change list; if this is -- less than that, you have a problem. Print a message. if this is equal, then -- that's ok, just bump it by 1 second. This could happen in the case of, say, -- spam-clicking the undo button or adding names to the list. The recipients -- should be ok with this since they'll follow the same algorithm. The only -- real chance for a problem is if two people click within the 1 second window? -- if someone sends you a past event, -- it's ok if it's newer than anything in the changes list -- otherwise ... causality has been violated. -- Whenever an admin signon event happens, have the admins each perform a -- timestamp check. Issue warnings for anyone with a clock that's more than -- X seconds out of sync with the others. Seriously, why isn't NTP a standard -- setting on all operating systems ... --}}} -- Action and DoAction defs {{{ -- Action Discussion {{{ -- The actual actions for changes start here -- -- Each action occurs as a pair of functions. The Action() function is from -- a list admin's point of view. Each will check for admin status, then create a -- change bundle, call the handler for that change (ie the DoAction func), and -- then record/transmist the bundle. These are simple and repetitive functions. -- -- The DoAction() function is tasked with executing the bundle and is what -- non-admins and admins alike will call to transform their working state via a -- change packet. Each Do() function will accept *only* a change packet, and -- it's assumed that the change has been vetted elsewhere. These are very blunt -- routines. -- -- Note that "undo" has no special voodoo to it. It's basically a change that -- reverses the prior change on the stack.--}}} function AddPerson(name)--{{{ print("Adding ... " .. name) PersonList:Add(name) end--}}} function CreateList(name)--{{{ -- require admin print("Creating ... " .. name) return LootLists:Create(name) end--}}} function AddPersonToListEnd(name,listName)--{{{ -- require admin local l = LootLists:Select(listName) local te = PersonList:Select(name) -- TODO: if not te ... printf("Adding %s (%s) to list %s", name, te:GetId(), listName) return l:InsertEnd(te:GetId()) end--}}} function AddPersonToListRandom(name,listName)--{{{ -- require admin local l = LootLists:Select(listName) local te = PersonList:Select(name) -- TODO: if not te ... printf("Adding %s (%s) to list %s - randomly!", name, te:GetId(), listName) return l:InsertRandom(te:GetId()) end--}}} function SuicidePerson(name,listName)--{{{ -- require admin PersonList:RefreshRaidList() local le = LootLists:Select(listName) local te = PersonList:Select(name) return le:SuicidePerson(te:GetId()) end--}}} function RenameList(listName,newListName)--{{{ -- require admin local le = LootLists:Select(listName) return le:RenameList(newListName) end--}}} function DeleteList(listName)--{{{ return LootLists:DeleteList(LootLists:Select(listName):GetId()) end--}}} function RemovePersonFromList(name,listName)--{{{ local le = LootLists:Select(listName) local te = PersonList:Select(name) return le:Remove(te:GetId()) end --}}} function RemovePerson(person) print("Removing " .. person) PersonList:Remove(person) end function ReservePerson(person) -- todo: move reserve state to ... State.lua print("Reserving " .. person) if PersonList:AddReserve(person) then -- todo: would be better if this were an ID ... Comm:AddReserve(person) end end --}}} -- Higher order actions (ie calls other standard actions){{{ function TrimLists(time) if not CheckListCausality() then print("Unable to trim changelist due to violated causality") return false end if type(time) ~= "number" then time = tonumber(time) end -- bisect the changes list by "time" local before = {} local lastTime = 0 for i,v in ipairs(db.profile.changes) do if v.time <= time then tinsert(before,v) lastTime = v.time else break end end print("before -",#before) --return --apply first half CreateWorkingStateFromChanges(before) -- save this state permanently; trim the changes permanently LootLists:SaveToDB(db) PersonList:SaveToDB(db) while db.profile.changes ~= nil and db.profile.changes[1] ~= nil and db.profile.changes[1].time <= time do table.remove(db.profile.changes,1) end print("reapplying ",_G.getn(db.profile.changes)) db.profile.time = lastTime -- using the trimmed list and the new bases, recreate the working state --CreateWorkingStateFromChanges(db.profile.changes) for i,v in ipairs(db.profile.changes) do ProcessChange(v) end if changelistener then changelistener:dataevent() end end function PopulateListRandom(listIndex) -- difference (raid+reserve)-list, then random shuffle that, then add local actives = PersonList:GetAllActiveIds() local list = LootLists:Select(listIndex) --swap keys on actives local t = {} for _,v in pairs(actives) do t[v] = true end -- now remove from t all of the people already present on the list if t then for id in list:OrderedIdIter() do -- id iterator if t[id] then t[id] = false end end end -- add all remaining for i,v in pairs(t) do if v then AddPersonToListRandom(i,list:GetId()) end end end function NukePerson(name) -- delete from all lists and then from persons for _,id in pairs(LootLists:GetAllIds()) do RemovePersonFromList(name,id) end RemovePerson(name) end --}}} -- undo rules! -- only the most recent event can be undone -- ^^^ on a given list? -- algorithm is easy, given "Suicide A B C" -- just find A,B,C in the list and replace in order from the s message -- while undo is allowed *per-list*, certain events in the stream will -- prevent proper undo, such as add/delete player or add/delete list -- returns true if the events in the list are in time order function CheckListCausality() local t = nil for i,v in ipairs(db.profile.changes) do if t ~= nil then if v.time <= t then return false end end t = v.time end return true end function CreateChangeDiff(remoteBase,remoteChanges) local t = remoteChanges if remoteBase == db.profile.time then local j = 1 -- index in foreign list local n = getn(t) local o = {} for i,v in ipairs(db.profile.changes) do -- for each timestamp in our list if t and t[j] < v.time then table.insert(o,v) end while j<n and t[j] <= v.time do j = j+1 end -- advance the foreign pointer past our current entry end -- j>=n ? add because the remote hit end of road. lt? add because it's a missing stamp --print("Received request at timebase",remoteBase,"and returning:") --PrintTable(o) return true, o else return false, {} end end function IntegrateChangeDiff(remoteBase,remoteChanges) local c = remoteChanges local old = db.profile.changes local new = {} local op = 1 local cp = 1 local no = getn(old) local worthRefresh = false -- only worth refreshing if we take changes from c -- if remotebase is older, don't integrate anything prior to db.profile.time. if they're newer, it's ok to merge in their changes if remoteBase < db.profile.time then while c[1] and c[1].time < db.profile.time do table.remove(c,1) end end local nc = getn(c) if no == 0 and nc > 0 then worthRefresh = true db.profile.changes = c else while op <= no or cp <= nc do -- lists are pre-sorted. insertion merge them if cp > nc then -- inelegant - edge cases first, then the normal logic table.insert(new,old[op]) op = op + 1 elseif op > no then worthRefresh=true table.insert(new,c[cp]) cp = cp + 1 elseif c[cp].time == old[op].time then -- todo: even though they're the same timestamp, still compare -- contents for sanity table.insert(new,old[cp]) cp = cp + 1 op = op + 1 elseif c[cp].time < old[op].time then worthRefresh=true table.insert(new,c[cp]) cp = cp + 1 elseif c[cp].time > old[op].time then table.insert(new,old[op]) op = op + 1 else error("Bad update received from ",sender) end end print("Updating changes - ",getn(new), "entries") db.profile.changes = new end if worthRefresh then CreateWorkingStateFromChanges(db.profile.changes) if changelistener then changelistener:dataevent() end end end