changeset 49:f52d472f0b0a

Sweeping refactor to compartmentalize the various lists and to give them nice interfaces. Disentangled dependency web significantly
author John@Yosemite-PC
date Wed, 21 Mar 2012 22:52:42 -0400
parents b3679847e292
children b155a875de42
files Core.lua Lists.lua
diffstat 2 files changed, 652 insertions(+), 527 deletions(-) [+]
line wrap: on
line diff
--- a/Core.lua	Wed Mar 21 22:51:34 2012 -0400
+++ b/Core.lua	Wed Mar 21 22:52:42 2012 -0400
@@ -63,121 +63,114 @@
         local MULTIBYTE_FIRST_CHAR = "^([\192-\255]?%a?[\128-\191]*)"
         return string.gsub(p, MULTIBYTE_FIRST_CHAR, string.upper, 1)  
     end
+    action,arg1,arg2,arg3 = bsk:GetArgs(paramIn,4)
 
-    if param[1] == nil or param[1] == "" then
+    if not action then
         print("need args")
         return
     end
-    if param[1] == "persons" then
+    if action == "persons" then
         PrintPersons()
-    elseif param[1] == "changes" then
+    elseif action == "changes" then
         PrintChanges()
-    elseif param[1] == "delete" then
-        if param[2] == nil or param[2] == "" or param[3] == nil or param[3] == "" then
+    elseif action == "selfdestruct" then
+        SelfDestruct()
+    elseif action == "delete" or action == "remove" then
+        if not arg1 or not arg2 then
             PrintTable(param)
             return
         end
-        if param[2] == "list" then
-            DeleteList(param[3])
-        elseif param[2] == "personfromlist" then
-            if param[4] == nil or param[4] == "" then
+        if arg1 == "list" then
+            DeleteList(arg2)
+        elseif arg1 == "personfromlist" or "fromlist" then
+            if not arg3 then
                 PrintTable(param)
                 return
             end
-            local person = FixPersonName(param[3])
-            RemovePersonFromList(person, param[4])
-        elseif param[2] == "person" then
-            local person = FixPersonName(param[3])
+            local person = FixPersonName(arg2)
+            RemovePersonFromList(person, arg3)
+        elseif arg1 == "person" then
+            local person = FixPersonName(arg2)
             RemovePerson(person)
         else
-            printf("Deleting anything of type %s is not supported",param[2])
+            printf("Deleting anything of type %s is not supported",arg1)
         end
-    elseif param[1] == "nuke" then
-        if param[2] == nil or param[2] == "" then
+    elseif action == "nuke" then
+        if not arg1 then
             PrintTable(param)
             return
         end
-        local person = FixPersonName(param[2])
+        local person = FixPersonName(arg1)
         NukePerson(person)
-    elseif param[1] == "add" then
-        if param[2] == nil or param[2] == "" then
+    elseif action == "add" or action == "create" then
+        if not arg1 or not arg2 then
             PrintTable(param)
             return
         end
-        if param[3] == nil or param[3] == "" then
-            PrintTable(param)
-            return
-        end
-        if param[2] == "person" then
-            if param[3] == "all" then
-                AddMissingPersons()
+        if arg1 == "person" then
+            if arg2 == "all" or arg2 == "missing" then
+                PersonList:AddMissing()
             else
-                local person = FixPersonName(param[3])
+                local person = FixPersonName(arg2)
                 AddPerson(person)
             end
-        elseif param[2] == "list" then
-            CreateList(param[3])
-        elseif param[2] == "tolist" then
-            if param[4] == nil or param[4] == "" then
+        elseif arg1 == "list" then
+            CreateList(arg2)
+        elseif arg1 == "tolist" then
+            if not arg3 then
                 PrintTable(param)
                 return
             end
-            local person = FixPersonName(param[3])
-            AddPersonToListEnd(person,param[4])
-        elseif param[2] == "tolistrandom" then
-            if param[4] == nil or param[4] == "" then
+            local person = FixPersonName(arg2)
+            AddPersonToListEnd(person,arg3)
+        elseif arg1 == "tolistrandom" then
+            if not arg3 then
                 PrintTable(param)
                 return
             end
-            local person = FixPersonName(param[3])
-            AddPersonToListRandom(person,param[4])
+            local person = FixPersonName(arg2)
+            AddPersonToListRandom(person,arg3)
         end
-    elseif param[1] == "populate" then
-        if param[2] == nil or param[2] == "" or param[3] == nil or param[3] == "" then
+    elseif action == "populate" then
+        if not arg1 then
             PrintTable(param)
             return
         end
         -- list = p2
-        local index = GetListIndex(param[2])
-        if param[3] == "random" then
-            PopulateListRandom(index)
-        end
-    elseif param[1] == "suicide" then
-        if param[2] == nil or param[2] == "" or param[3] == nil or param[3] == "" then
+        PopulateListRandom(arg1)
+    elseif action == "suicide" then
+        if not arg1 or not arg2 then
             PrintTable(param)
             return
         end
-        local person = FixPersonName(param[2])
-        SuicidePerson(person,param[3])
-    elseif param[1] == "lists" then
-        if param[2] == nil or param[2] == "" then
+        local person = FixPersonName(arg1)
+        SuicidePerson(person,arg2)
+    elseif action == "lists" then
+        if not arg1 then
             PrettyPrintLists()
             return
         else
-            local listIndex = GetListIndex(param[2])
-            PrettyPrintList(listIndex)
+            PrettyPrintList(arg1)
         end
-    elseif param[1] == "reserve" then
-        if param[2] == nil or param[2] == "" then
+    elseif action == "reserve" then
+        if not arg1 then
+            PrintTable(param)
+            return
+        end
+        local person = FixPersonName(arg1)
+        ReservePerson(person)
+    elseif action == "trim" then
+        if not arg1 then
             printtable(param)
             return
         end
-        local person = FixPersonName(param[2])
-        AddReserve(person)
-    elseif param[1] == "trim" then
-        if param[2] == nil or param[2] == "" then
-            printtable(param)
+        TrimLists(arg1)
+    elseif action == "rename" then
+        if not arg1 or not arg2 then
+            PrintTable(param)
             return
         end
-        TrimLists(param[2])
-    elseif param[1] == "rename" then
-        if param[2] == nil or param[2] == "" or param[3] == nil or param[3] == "" then
-            printtable(param)
-            return
-        end
-        RenameList(param[2],param[3])
-    elseif param[1] == "selfdestruct" then
-        SelfDestruct()
+        RenameList(arg1,arg2)
     else
         CreateGUI()
     end
--- a/Lists.lua	Wed Mar 21 22:51:34 2012 -0400
+++ b/Lists.lua	Wed Mar 21 22:52:42 2012 -0400
@@ -85,89 +85,597 @@
 local setmetatable=setmetatable
 setfenv(1,bsk)
 
-lists = {}
-persons = {}
-
-raidNameP = {} -- "name" is present in raid
-raidIdP = {} -- "id" is present in raid
-reserveIdP = {} -- "reserve id present"
-local personName2id = {} -- given "name" get that person's id
+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
 
 List = 
 {
-    id = 0,
     name = "",
-    Sort = function()
+    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
+
 }
-function List:new() -- TODO: take args, build list
-    n = {}
-    setmetatable(n,self)
-    self.__index = self
+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 i = 1,#self do
-        if id == self[i].id then
+function List:HasId(id)
+    for le in self:OrderedIdIter() do
+        if id == le then
             return true
         end
     end
     return false
 end
-function List:GetID()
+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: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
+
+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 List:RemoveEntry()
-end
-function List:InsertEntry()
-end
-function List:OrderedIter()
+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 }
+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
+    end
+end
+-- todo: remove reserve
+function PersonList:IsActive(id)
+    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 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)
+        -- TODO: broadcast
+        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)
+        return
+    elseif LootLists[action] then
+        LootLists[action](LootLists,change.arg,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)
+                return
+            end
+        end
+    end
+    _G.error("Could not process change: " .. change.action)
+end
 
 function SelfDestruct()
-    lists = {}
-    persons = {}
+    LootLists:Reset()
+    PersonList:Reset()
     db.profile.persons = {}
     db.profile.changes = {}
     db.profile.lists = {}
-    raidNameP = {}
-    raidIdP = {}
-    reserveIdP = {}
-    personName2id = {}
 end
 
 -- Debugging {{{
 function PrettyPrintList(listIndex)
-    local list = lists[listIndex]
-    print("List: " .. list.name .. " (" .. listIndex .. ") - last modified " .. date("%m/%d/%y %H:%M:%S", list.time) .. " (",list.time,")" )
-    for i = 1,#list do
-        print("  " .. i .. " - " .. persons[list[i].id].main)
+    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(lists) do
+    for _,i in pairs(LootLists:GetAllIds()) do
         PrettyPrintList(i)
     end
 end
 function PrintLists()
-    PrintTable(lists)
+    PrintTable(LootLists)
 end
 function PrintChanges()
     PrintTable(db.profile.changes)
 end
 function PrintPersons()
-    PrintTable(persons)
+    PrintTable(PersonList)
 end
 function PrintAPI(object)
     for i,v in pairs(object) do
@@ -176,103 +684,22 @@
         end
     end
 end
-function PrintRaidAndReserve()
-    print("RaidNameP")
-    PrintTable(raidNameP)
-    print("RaidIdP")
-    PrintTable(raidIdP)
-    print("ReserveP")
-    PrintTable(reserveIdP)
-    print("personName2id")
-    PrintTable(personName2id)
-end
 --}}}
 
-function UpdatePersonsReverse()
-    for i,v in pairs(persons) do
-        if i ~= "time" then
-            personName2id[v.main] = i
-        end
-    end
-end
-
 -- Change processing {{{
 function CreateWorkingStateFromChanges(changes)
-    local personsBase = db.profile.persons
-    local lists = db.profile.lists
-
     -- copy the base to the working state
-    wipe(lists)
-    wipe(persons)
-    wipe(personName2id)
-
-    tcopy(lists,lists)
-    tcopy(persons,personsBase)
+    LootLists:ConstructFromDB(db)
+    PersonList:ConstructFromDB(db)
 
     -- now just go through the changes list applying each
     for i,v in ipairs(changes) do
         ProcessChange(v)
     end
-
-    -- update the persons reverse list
-    UpdatePersonsReverse()
-end
-
-function CreateChange(change)
-    -- sanity
-    assert(change)
-    assert(change.action)
-    assert(change.arg)
-
-    StartChange(change)
-    CommitChange(change)
-end
-
-function StartChange(change)
-    local changes = db.profile.changes
-    change.time = time()
-    local n = getn(changes)
-    if n > 0 then
-        if changes[n].time >= change.time then
-            change.time = changes[n].time + 1
-        end
-    end
-end
-
-function CommitChange(change)
-    local changes = db.profile.changes
-    tinsert(changes,change)
-    -- TODO: broadcast change
-end
-
-function ProcessChange(change)
-    if change.action == "AddPerson" then
-        DoAddPerson(change)
-    elseif change.action == "RenameList" then
-        DoRenameList(change)
-    elseif change.action == "CreateList" then
-        DoCreateList(change)
-    elseif change.action == "DeleteList" then
-        DoDeleteList(change)
-    elseif change.action == "AddToListEnd" then
-        DoAddPersonToListEnd(change)
-    elseif change.action == "AddToListRand" then
-        DoAddPersonToListRandom(change)
-    elseif change.action == "RemovePerson" then
-        DoRemovePerson(change)
-    elseif change.action == "RemovePersonFromList" then
-        DoRemovePersonFromList(change)
-    elseif change.action == "SuicidePerson" then
-        DoSuicidePerson(change)
-    else
-        print("Unknown message encountered")
-        PrintTable(change)
-        assert(false)
-    end 
 end
 
 --}}}
---
+
 -- holy crap long winded {{{
 -- timestamp logic:
 -- use time() for comparisons - local clients use date() to make it pretty. only
@@ -321,239 +748,60 @@
 --
 -- Note that "undo" has no special voodoo to it. It's basically a change that
 -- reverses the prior change on the stack.--}}}
-function DoAddPerson(change)--{{{
-    assert(change)
-    assert(change.arg.id)
-    -- require admin
-    local persons = persons
-    local name = change.arg.name
-    local id = change.arg.id
-    assert(persons[id]==nil)
-    persons[id] = {main=name,class=change.arg.class}
-    persons.time=change.time
-    personName2id[name] = id
-    return true
-end--}}}
 function AddPerson(name)--{{{
-    local persons = persons
-    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
-    
-    if persons[id] and persons[id] ~= name then
-        printf("Namechange detected for %s - new is %s, please rename the existing entry", persons[id].main, name)
-        return
-    end
-    if persons[id] ~= nil then
-        printf("%s is already in the persons list; disregarding", name)
-        return
-    end
-    local change = {action="AddPerson",arg={name=name,id=id,class=englishClass}}
-    if DoAddPerson(change) then
-        CreateChange(change)
-    end
-end--}}}
-function DoCreateList(change)--{{{
-    --if GetListIndex(change.arg.name) then
-    --    rintf(("List %s already exists",v.name)
-    --    return false
-    --end
-    lists[change.arg.id]={name=change.arg.name,time=change.time}
-    return true
+    print("Adding ... " .. name)
+    PersonList:Add(name)
 end--}}}
 function CreateList(name)--{{{
     -- require admin
-    local change={action="CreateList",arg={name=name}}
-    StartChange(change)
-    change.arg.id=change.time -- use the creation timestamp as the list's index. it's as unique as anything...
     print("Creating ... " .. name)
-    if DoCreateList(change) then
-        CommitChange(change)
-    end
-end--}}}
-function DoAddPersonToListEnd(change)--{{{
-    local list = lists[change.arg.listIndex]
-    local index
-    if getn(list) > 0 then
-        index = list[#list].index + 0.1
-    else
-        index = 0.1
-    end
-    local entry = {index=index, id=change.arg.id}
-
-    tinsert(list,entry)
-    list.time = change.time
-    list.closedRandom = true
-
-    return true
+    return LootLists:Create(name)
 end--}}}
 function AddPersonToListEnd(name,listName)--{{{
     -- require admin
-    local listIndex = GetListIndex(listName)
-    local id = personName2id[name]
-    if IdIsInList(id,lists[listIndex]) then
-        printf("Person %s is already on the reqeuested list",name)
-        return false
-    end
-    printf("Adding %s (%s) to list %s (%s)", name, id, listName, listIndex)
-    local change = {action="AddToListEnd",arg={id=id,listIndex=listIndex}}
-    StartChange(change)
-    if DoAddPersonToListEnd(change) then
-        CommitChange(change)
-    end
-end--}}}
-function DoAddPersonToListRandom(change)--{{{
-    local list = lists[change.arg.listIndex]
-    local entry = {index=change.arg.roll, id=change.arg.id}
-
-    tinsert(list,entry)
-    table.sort(list,function(a,b) return a.index < b.index end)
-    list.time = change.time
-
-    return true
+    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 listIndex = GetListIndex(listName)
-    if lists[listIndex].closedRandom then
-        print("Cannot add person to list by random roll because an add-to-end operation has already occurred")
-        return false
-    end
-    local id = personName2id[name]
-    if IdIsInList(id,lists[listIndex]) then
-        printf("Person %s is already on the reqeuested list",name)
-        return false
-    end
-    local roll = math.random()
-    printf("Adding %s (%s) to list %s (%s) with roll (%f)", name, id, listName, listIndex, roll)
-    local change = {action="AddToListRand",arg={id=id,listIndex=listIndex,roll=roll}}
-    StartChange(change)
-    if DoAddPersonToListRandom(change) then
-        CommitChange(change)
-    end
-end--}}}
-function DoRemovePerson(change)--{{{
-    local person = persons[change.arg.id]
-    personName2id[person.main] = nil
-    persons[change.arg.id] = nil
-    persons.time = change.time
-    return true
-end--}}}
-function RemovePerson(name)--{{{
-    local id = personName2id[name]
-    if not id then
-        printf("%s is not in the persons list, please check your spelling", name)
-        return false
-    end
-    local listsTheyreOn = {}
-    -- check if they're active on any loot list
-    for i,v in pairs(lists) do
-        if IdIsInList(id,v) then
-            tinsert(listsTheyreOn,v.name)
-            break
-        end
-    end
-    if getn(listsTheyreOn) > 0 then
-        printf("Cannot remove person %s because they are on one or more lists (%s)",name,table.concat(listsTheyreOn,", "))
-        return false
-    end
-    local change = {action="RemovePerson",arg={id=id}}
-    StartChange(change)
-    if DoRemovePerson(change) then
-        CommitChange(change)
-    end
-end--}}}
-function DoSuicidePerson(change)--{{{
-    local list = lists[change.arg.listIndex]
-    local affected = shallowCopy(change.arg.affect)
-    -- 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 replacement = shallowCopy(change.arg.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 i = 1, #list do
-        if list[i].id == affected[1] then
-            table.remove(affected,1)
-            list[i].id = replacement[1]
-            table.remove(replacement,1)
-        end
-    end
-    list.time=change.time
-    return true
+    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
-    PopulateRaidList()
-    local listIndex = GetListIndex(listName)
-    local id = personName2id[name]
-    local affect=GetSuicideList(id,lists[listIndex])
-    local change = {action="SuicidePerson",arg={affect=affect,listIndex=listIndex}}
-    StartChange(change)
-    if DoSuicidePerson(change) then
-       CommitChange(change)
-    end
-end--}}}
-function DoRenameList(change)--{{{
-    lists[change.arg.listIndex].name = change.arg.name
-    lists[change.arg.listIndex].time = change.time
-    return true
+    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 listIndex = GetListIndex(listName)
-    local change = {action="RenameList",arg={listIndex=listIndex,name=newListName}}
-    StartChange(change)
-    if DoRenameList(change) then
-       CommitChange(change)
-    end
-end--}}}
-function DoDeleteList(change)--{{{
-    lists[change.arg.listIndex] = nil
-    return true
+    local le = LootLists:Select(listName)
+    return le:RenameList(newListName)
 end--}}}
 function DeleteList(listName)--{{{
-    local listIndex = GetListIndex(listName)
-    local change = {action="DeleteList",arg={listIndex=listIndex}}
-    StartChange(change)
-    if DoDeleteList(change) then
-       CommitChange(change)
-    end
-end--}}}
-function DoRemovePersonFromList(change)--{{{
-    local list = lists[change.arg.listIndex]
-
-    for i,v in ipairs(list) do
-        if v.id == change.arg.id then
-            table.remove(list,i)
-            break
-        end
-    end
-    table.sort(list,function(a,b) return a.index < b.index end)
-    list.time = change.time
-    return true
+    return LootLists:DeleteList(LootLists:Select(listName):GetId())
 end--}}}
 function RemovePersonFromList(name,listName)--{{{
-    local listIndex = GetListIndex(listName)
-    local pid = personName2id[name]
-    -- todo: check that they're on the list in the first place
-    local change = {action="RemovePersonFromList",arg={id=pid,listIndex=listIndex}}
-    StartChange(change)
-    if DoRemovePersonFromList(change) then
-       CommitChange(change)
-    end
+    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)
+    print("Reserving " .. person)
+    PersonList:AddReserve(person)
+end
 --}}}
 -- Higher order actions (ie calls other standard actions){{{
 
@@ -581,8 +829,8 @@
     CreateWorkingStateFromChanges(before)
 
     -- save this state permanently; trim the changes permanently
-    tcopy(db.profile.persons,persons)
-    tcopy(db.profile.lists,lists)
+    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
@@ -591,39 +839,21 @@
     CreateWorkingStateFromChanges(db.profile.changes)
 end
 
-function AddMissingPersons()
-    PopulateRaidList() 
-    local t = {}
-    for id,_ in pairs(persons) do
-        t[id] = true
-    end
-    for name,_ in pairs(raidNameP) do
-        if personName2id[name] == nil then
-            printf("Person %s is missing from the persons list - adding",name)
-            AddPerson(name)
-        end
-    end
-    -- TODO: batch into a single op - no need to spam 25 messages in a row
-end
 
 function PopulateListRandom(listIndex)
     -- difference (raid+reserve)-list, then random shuffle that, then add
-    PopulateRaidList()
-    local list = lists[listIndex]
+    local actives = PersonList:GetAllActiveIds()
+    local list = LootLists:Select(listIndex)
 
-    local t = {} -- after loops, contains intersection of IDs present between raid and reserve
-    for i,v in pairs(raidIdP) do
-        if v then t[i] = true end 
-    end
-    for i,v in pairs(reserveIdP) do
-        if v then t[i] = true end 
-    end
+    --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 list then
-        for i = 1,#list do
-            if t[list[i].id] then
-                t[list[i].id] = false
+    if t then
+        for id in list:OrderedIdIter() do -- id iterator
+            if t[id] then
+                t[id] = false
             end
         end
     end
@@ -631,79 +861,17 @@
     -- add all remaining
     for i,v in pairs(t) do
         if v then
-            AddPersonToListRandom(persons[i].main,list.name) -- TODO: APTLR keys off of string names. probably need to change this.
+            AddPersonToListRandom(i,list:GetId())
         end
     end
 end
-
 function NukePerson(name) -- delete from all lists and then from persons
-    local pid = personName2id[name]
-    for i,v in pairs(lists) do
-        RemovePersonFromList(name,v.name)
+    for _,id in pairs(LootLists:GetAllIds()) do
+        RemovePersonFromList(name,id)
     end
     RemovePerson(name)
 end
 --}}}
--- "Soft" actions- ie things that cause nonpermanent state {{{
-
--- reserves
-function AddReserve(name)
-    print("Reserving" .. name)
-    reserveIdP[personName2id[name]]=true
-    -- TODO: communicate to others. don't store this in any way.
-end
-
-function RemoveReserve(name)
-    reserveIdP[personName2id[name]]=false
-    -- TODO: communicate to others. don't store this in any way.
-end
-
-
---function GetActiveList()
---    return lists[1] -- todo!
---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 PopulateRaidList()
-    local inParty = _G.GetNumPartyMembers()
-    local inRaid = _G.GetNumRaidMembers()
-    local add = function(unitNameArg) 
-        local name = _G.UnitName(unitNameArg)
-        raidNameP[name]=true
-        if personName2id[name] ~= nil then
-            raidIdP[personName2id[name]]=true 
-        end
-    end
-
-    wipe(raidNameP)
-    wipe(raidIdP)
-    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
-    --PrintTable(raidNameP)
-end
 
 -- undo rules!
 -- only the most recent event can be undone
@@ -713,36 +881,6 @@
 -- while undo is allowed *per-list*, certain events in the stream will
 -- prevent proper undo, such as add/delete player or add/delete list
 
-
-function GetSuicideList(id,list)
-    --print("Calculating changeset for "..name.." from list -")
-    --PrintTable(list)
-    local t = {}
-    local ret = {}
-    local pushing = false
-    for i = 1, #list do
-        if list[i].id == id then
-            pushing = true
-        end
-        if pushing and (raidIdP[list[i].id] or reserveIdP[list[i].id]) then
-            tinsert(ret,list[i].id)
-        end
-    end
-    --print("GSL")
-    --PrintTable(ret)
-    --print("GSL")
-    return ret
-end
-
-function IdIsInList(id,listRef)
-    for i = 1,#listRef do
-        if id == listRef[i].id then
-            return true
-        end
-    end
-    return false
-end
-
 -- returns true if the events in the list are in time order
 function CheckListCausality()
     local t = nil
@@ -757,15 +895,3 @@
     return true
 end
 
--- Support functions
-
-function GetListIndex(name)
-    for i,v in pairs(lists) do
-        if v.name == name then
-            return i
-        end
-    end
-    return nil
-end
-
-