view Lists.lua @ 37:6362fe301d43

Storing class name in the persons so we can color them in later Exposing some data lists, but this will likely be retracted later in favor of interface functions
author John@Yosemite-PC
date Tue, 13 Mar 2012 23:17:18 -0400
parents ecd37523ae04
children ecef0cba2913
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: collaborative list trimming
-- 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
-- TODO: players list, drop the "main" sub-entry for people with no alts

-- 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 bsk.persons value which leads to a personName2id which
-- leads to a raidIdP

bsk.lists = {}
bsk.persons = {}

bsk.raidNameP = {} -- "name" is present in raid
bsk.raidIdP = {} -- "id" is present in raid
bsk.reserveIdP = {} -- "reserve id present"
local activeListKey = 1 -- temporary
local personName2id = {} -- given "name" get that person's id

local tinsert = table.insert
local sformat = string.format
local getn = table.getn

function bsk:SelfDestruct()
    bsk.lists = {}
    bsk.persons = {}
    bsk.db.profile.persons = {}
    bsk.db.profile.changes = {}
    bsk.db.profile.lists = {}
    bsk.raidNameP = {}
    bsk.raidIdP = {}
    bsk.reserveIdP = {}
    personName2id = {}
end
function bsk:tcopy(to, from)
  for k,v in pairs(from) do
    if(type(v)=="table") then
      to[k] = {}
      bsk:tcopy(to[k], v);
    else
      to[k] = v;
    end
  end
end
local shallowCopy = function(t)
  local u = { }
  for k, v in pairs(t) do u[k] = v end
  return setmetatable(u, getmetatable(t))
end

-- Debugging {{{
function bsk:PrettyPrintList(listIndex)
    local list = bsk.lists[listIndex]
    bsk:Print("List: " .. list.name .. " (" .. listIndex .. ") - last modified " .. date("%m/%d/%y %H:%M:%S", list.time) .. " (",list.time,")" )
    for i = 1,#list do
        bsk:Print("  " .. i .. " - " .. bsk.persons[list[i].id].main)
    end
end
function bsk:PrettyPrintLists()
    for i,_ in pairs(bsk.lists) do
        bsk:PrettyPrintList(i)
    end
end
function bsk:PrintLists()
    bsk:PrintTable(bsk.lists)
end
function bsk:PrintChanges()
    bsk:PrintTable(bsk.db.profile.changes)
end
function bsk:PrintPersons()
    bsk:PrintTable(bsk.persons)
end
function bsk:PrintTable(table, depth)
    depth = depth or ""
    if not table then return end
    if #depth > 3*5 then self:Print(depth.."Recursion too deep - stopping"); return end
    for i,v in pairs(table) do 
        if( type(v) == "string" ) then
            self:Print(depth .. i ..  " - " .. v) 
        elseif( type(v) == "number" ) then
            self:Print(depth .. i .. " - " .. tostring(v))
        elseif( type(v) == "table" ) then
            self:Print(depth .. i .." - ") 
            self:PrintTable(v,depth.."   ")
        elseif( type(v) == "boolean" ) then
            self:Print(depth .. i .. " - " .. tostring(v))
        elseif( type(v) == "function" ) then
            self:Print(depth .. "function " .. i .. "()")
        else
            self:Print(depth .. i .. " - not sure how to print type: " .. type(v) )
        end
    end
end

function bsk:PrintRaidAndReserve()
    bsk:Print("RaidNameP")
    bsk:PrintTable(bsk.raidNameP)
    bsk:Print("RaidIdP")
    bsk:PrintTable(bsk.raidIdP)
    bsk:Print("ReserveP")
    bsk:PrintTable(bsk.reserveIdP)
    bsk:Print("personName2id")
    bsk:PrintTable(personName2id)
end
--}}}

function bsk:UpdatePersonsReverse()
    for i,v in pairs(bsk.persons) do
        if i ~= "time" then
            personName2id[v.main] = i
        end
    end
end

-- Change processing {{{
function bsk:CreateWorkingStateFromChanges(changes)
    local personsBase = self.db.profile.persons
    local lists = self.db.profile.lists

    -- copy the base to the working state
    wipe(bsk.lists)
    wipe(bsk.persons)
    wipe(personName2id)

    bsk:tcopy(bsk.lists,lists)
    bsk:tcopy(bsk.persons,personsBase)

    -- now just go through the changes list applying each
    for i,v in ipairs(changes) do
        bsk:ProcessChange(v)
    end

    -- update the persons reverse list
    bsk:UpdatePersonsReverse()
end

function bsk:CreateChange(change)
    -- sanity
    assert(change)
    assert(change.action)
    assert(change.arg)

    bsk:StartChange(change)
    bsk:CommitChange(change)
end

function bsk:StartChange(change)
    local changes = self.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 bsk:CommitChange(change)
    local changes = self.db.profile.changes
    tinsert(changes,change)
    -- TODO: broadcast change
end

function bsk:ProcessChange(change)
    if change.action == "AddPerson" then
        bsk:DoAddPerson(change)
    elseif change.action == "RenameList" then
        bsk:DoRenameList(change)
    elseif change.action == "CreateList" then
        bsk:DoCreateList(change)
    elseif change.action == "DeleteList" then
        bsk:DoDeleteList(change)
    elseif change.action == "AddToListEnd" then
        bsk:DoAddPersonToListEnd(change)
    elseif change.action == "AddToListRand" then
        bsk:DoAddPersonToListRandom(change)
    elseif change.action == "RemovePerson" then
        bsk:DoRemovePerson(change)
    elseif change.action == "RemovePersonFromList" then
        bsk:DoRemovePersonFromList(change)
    elseif change.action == "SuicidePerson" then
        bsk:DoSuicidePerson(change)
    else
        bsk:Print("Unknown message encountered")
        bsk: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
-- 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 bsk: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 bsk: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 bsk:DoAddPerson(change)--{{{
    assert(change)
    assert(change.arg.id)
    -- require admin
    local persons = bsk.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 bsk:AddPerson(name)--{{{
    local persons = bsk.persons
    local guid = UnitGUID(name)
    -- TODO: check guid to be sure it's a player
    if not guid then
        self:Print(sformat("Could not add player %s - they must be in range or group",name))
        return
    end
    local _,englishClass = UnitClass(name)
    --bsk: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
        self:Print(sformat("Namechange detected for %s - new is %s, please rename the existing entry", persons[id].main, name))
        return
    end
    if persons[id] ~= nil then
        self:Print(sformat("%s is already in the persons list; disregarding", name))
        return
    end
    local change = {action="AddPerson",arg={name=name,id=id,class=englishClass}}
    if bsk:DoAddPerson(change) then
        bsk:CreateChange(change)
    end
end--}}}
function bsk:DoCreateList(change)--{{{
    --if bsk:GetListIndex(change.arg.name) then
    --    self:Print(sformat("List %s already exists",v.name))
    --    return false
    --end
    bsk.lists[change.arg.id]={name=change.arg.name,time=change.time}
    return true
end--}}}
function bsk:CreateList(name)--{{{
    -- require admin
    local change={action="CreateList",arg={name=name}}
    bsk:StartChange(change)
    change.arg.id=change.time -- use the creation timestamp as the list's index. it's as unique as anything...
    self:Print("Creating ... " .. name)
    if bsk:DoCreateList(change) then
        bsk:CommitChange(change)
    end
end--}}}
function bsk:DoAddPersonToListEnd(change)--{{{
    local list = bsk.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
end--}}}
function bsk:AddPersonToListEnd(name,listName)--{{{
    -- require admin
    local listIndex = bsk:GetListIndex(listName)
    local id = personName2id[name]
    if bsk:IdIsInList(id,bsk.lists[listIndex]) then
        bsk:Print(sformat("Person %s is already on the reqeuested list",name))
        return false
    end
    bsk:Print(sformat("Adding %s (%s) to list %s (%s)", name, id, listName, listIndex))
    local change = {action="AddToListEnd",arg={id=id,listIndex=listIndex}}
    bsk:StartChange(change)
    if bsk:DoAddPersonToListEnd(change) then
        bsk:CommitChange(change)
    end
end--}}}
function bsk:DoAddPersonToListRandom(change)--{{{
    local list = bsk.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
end--}}}
function bsk:AddPersonToListRandom(name,listName)--{{{
    -- require admin
    local listIndex = bsk:GetListIndex(listName)
    if bsk.lists[listIndex].closedRandom then
        self: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 bsk:IdIsInList(id,bsk.lists[listIndex]) then
        bsk:Print(sformat("Person %s is already on the reqeuested list",name))
        return false
    end
    local roll = math.random()
    bsk:Print(sformat("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}}
    bsk:StartChange(change)
    if bsk:DoAddPersonToListRandom(change) then
        bsk:CommitChange(change)
    end
end--}}}
function bsk:DoRemovePerson(change)--{{{
    local person = bsk.persons[change.arg.id]
    personName2id[person.main] = nil
    bsk.persons[change.arg.id] = nil
    bsk.persons.time = change.time
    return true
end--}}}
function bsk:RemovePerson(name)--{{{
    local id = personName2id[name]
    if not id then
        bsk:Print(sformat("%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(bsk.lists) do
        if bsk:IdIsInList(id,v) then
            tinsert(listsTheyreOn,v.name)
            break
        end
    end
    if getn(listsTheyreOn) > 0 then
        self:Print(sformat("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}}
    bsk:StartChange(change)
    if bsk:DoRemovePerson(change) then
        bsk:CommitChange(change)
    end
end--}}}
function bsk:DoSuicidePerson(change)--{{{
    local list = bsk.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
    --bsk:Print(sformat("Before suicide of %s on list %s",slist[1],list.name))
    --bsk: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
end--}}}
function bsk:SuicidePerson(name,listName)--{{{
    -- require admin
    bsk:PopulateRaidList()
    local listIndex = bsk:GetListIndex(listName)
    local id = personName2id[name]
    local affect=bsk:GetSuicideList(id,bsk.lists[listIndex])
    local change = {action="SuicidePerson",arg={affect=affect,listIndex=listIndex}}
    bsk:StartChange(change)
    if bsk:DoSuicidePerson(change) then
       bsk:CommitChange(change)
    end
end--}}}
function bsk:DoRenameList(change)--{{{
    bsk.lists[change.arg.listIndex].name = change.arg.name
    bsk.lists[change.arg.listIndex].time = change.time
    return true
end--}}}
function bsk:RenameList(listName,newListName)--{{{
    -- require admin
    local listIndex = bsk:GetListIndex(listName)
    local change = {action="RenameList",arg={listIndex=listIndex,name=newListName}}
    bsk:StartChange(change)
    if bsk:DoRenameList(change) then
       bsk:CommitChange(change)
    end
end--}}}
function bsk:DoDeleteList(change)--{{{
    bsk.lists[change.arg.listIndex] = nil
    return true
end--}}}
function bsk:DeleteList(listName)--{{{
    local listIndex = bsk:GetListIndex(listName)
    local change = {action="DeleteList",arg={listIndex=listIndex}}
    bsk:StartChange(change)
    if bsk:DoDeleteList(change) then
       bsk:CommitChange(change)
    end
end--}}}
function bsk:DoRemovePersonFromList(change)--{{{
    local list = bsk.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
end--}}}
function bsk:RemovePersonFromList(name,listName)--{{{
    local listIndex = bsk: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}}
    bsk:StartChange(change)
    if bsk:DoRemovePersonFromList(change) then
       bsk:CommitChange(change)
    end
end
--}}}
--}}}
-- Higher order actions (ie calls other standard actions){{{

function bsk:TrimLists(time)
    if not bsk:CheckListCausality() then
        self: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 = {}
    for i,v in ipairs(self.db.profile.changes) do
        if v.time <= time then
            tinsert(before,v)
        else
            break
        end
    end

    -- apply first half
    bsk:CreateWorkingStateFromChanges(before)

    -- save this state permanently; trim the changes permanently
    bsk:tcopy(bsk.db.profile.persons,bsk.persons)
    bsk:tcopy(bsk.db.profile.lists,bsk.lists)
    while bsk.db.profile.changes ~= nil and bsk.db.profile.changes[1] ~= nil and bsk.db.profile.changes[1].time <= time do
        table.remove(bsk.db.profile.changes,1)
    end

    -- using the trimmed list and the new bases, recreate the working state
    bsk:CreateWorkingStateFromChanges(bsk.db.profile.changes)
end

function bsk:AddMissingPersons()
    bsk:PopulateRaidList() 
    local t = {}
    for id,_ in pairs(bsk.persons) do
        t[id] = true
    end
    for name,_ in pairs(bsk.raidNameP) do
        if personName2id[name] == nil then
            bsk:Print(sformat("Person %s is missing from the persons list - adding",name))
            bsk:AddPerson(name)
        end
    end
    -- TODO: batch into a single op - no need to spam 25 messages in a row
end

function bsk:PopulateListRandom(listIndex)
    -- difference (raid+reserve)-list, then random shuffle that, then add
    bsk:PopulateRaidList()
    local list = bsk.lists[listIndex]

    local t = {} -- after loops, contains intersection of IDs present between raid and reserve
    for i,v in pairs(bsk.raidIdP) do
        if v then t[i] = true end 
    end
    for i,v in pairs(bsk.reserveIdP) do
        if v then t[i] = true end 
    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
            end
        end
    end

    -- add all remaining
    for i,v in pairs(t) do
        if v then
            bsk:AddPersonToListRandom(bsk.persons[i].main,list.name) -- TODO: APTLR keys off of string names. probably need to change this.
        end
    end
end

function bsk:NukePerson(name) -- delete from all lists and then from persons
    local pid = personName2id[name]
    for i,v in pairs(bsk.lists) do
        bsk:RemovePersonFromList(name,v.name)
    end
    bsk:RemovePerson(name)
end
--}}}
-- "Soft" actions- ie things that cause nonpermanent state {{{

-- reserves
function bsk:AddReserve(name)
    bsk.reserveIdP[personName2id[name]]=true
    -- TODO: communicate to others. don't store this in any way.
end

function bsk:RemoveReserve(name)
    bsk.reserveIdP[personName2id[name]]=false
    -- TODO: communicate to others. don't store this in any way.
end


--function bsk:GetActiveList()
--    return bsk.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] = format("party%d", i)
end
for i = 1, 40 do
    rID[i] = format("raid%d", i)
end
function bsk:PopulateRaidList()
    local inParty = GetNumPartyMembers()
    local inRaid = GetNumRaidMembers()
    local add = function(unitNameArg) 
        local name = UnitName(unitNameArg)
        bsk.raidNameP[name]=true
        if personName2id[name] ~= nil then
            bsk.raidIdP[personName2id[name]]=true 
        end
    end

    wipe(bsk.raidNameP)
    wipe(bsk.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
    --bsk:PrintTable(bsk.raidNameP)
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


function bsk:GetSuicideList(id,list)
    --self:Print("Calculating changeset for "..name.." from list -")
    --self: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 (bsk.raidIdP[list[i].id] or bsk.reserveIdP[list[i].id]) then
            tinsert(ret,list[i].id)
        end
    end
    --bsk:Print("GSL")
    --bsk:PrintTable(ret)
    --bsk:Print("GSL")
    return ret
end

function bsk: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 bsk:CheckListCausality()
    local t = nil
    for i,v in ipairs(bsk.db.profile.changes) do
        if t ~= nil then
            if v.time <= t then
                return false
            end
        end
        t = v.time
    end
    return true
end

-- Support functions

function bsk:GetListIndex(name)
    for i,v in pairs(bsk.lists) do
        if v.name == name then
            return i
        end
    end
    return nil
end

local shuffleArray = function(array)
    local arrayCount = #array
    for i = arrayCount, 2, -1 do
        local j = math.random(1, i)
        array[i], array[j] = array[j], array[i]
    end
    return array
end