view Lists.lua @ 41:dc9bfacca238

Lint
author John@Yosemite-PC
date Wed, 14 Mar 2012 22:06:19 -0400
parents ecef0cba2913
children 72055fc7e115
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 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:PrintAPI(object)
    for i,v in pairs(object) do
        if type(v) == "function" then
            bsk:Print("function "..i.."()")
        end
    end
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