view Lists.lua @ 9:daed0597deba

Pretty printing for lists A decision on how to index them up Bug about reverse list maintenance fixed. Next step noted.
author John@Doomsday
date Wed, 07 Mar 2012 14:56:25 -0500
parents b05fcb225c4a
children 433d31ea992a
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: rename player
-- TODO: list trimming

-- 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.

-- todo: list-of-lists must not use int indices. those will lead to peril.
bsk.lists = {}
bsk.persons = {}

local raidList = {}
local reserveList = {}
local activeListKey = 1 -- temporary
local personsReverse = {} 

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

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 .. " (" .. list.time .. ")")
    for i = 1,#list do
        bsk:Print("  " .. i .. " - " .. bsk.persons[list[i]].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
    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))
        else
            self:Print(depth .. i .. " - not sure how to print type: " .. type(v) )
        end
    end
end

--}}}

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

function bsk:CreateWorkingStateFromChanges(changes)
    local personsBase = self.db.profile.persons
    local listBase = self.db.profile.listBase

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

    bsk:tcopy(bsk.lists,listBase)
    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


-- 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 ...

function bsk:ProcessChange(change)
    if change.action == "AddPerson" then
        bsk:DoAddPerson(change)
    elseif change.action == "CreateList" then
        bsk:DoCreateList(change)
    elseif change.action == "AddPersonToList" then
        bsk:DoAddPersonToList(change)
    elseif change.action == "SuicidePerson" then
        bsk:DoSuicidePerson(change)
    else
        bsk:Print("Unknown message encountered")
        bsk:PrintTable(change)
        assert(false)
    end 
end

-- Action and DoAction defs {{{
--
-- 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.

-- persons list
function bsk:DoAddPerson(change)
    assert(change)
    assert(change.arg.id)
    local arg = change.arg
    -- require admin
    local persons = bsk.persons
    local name = arg.name
    local id = arg.id
    assert(persons[id]==nil)
    persons[id] = {main=name}
    persons.time=change.time
    personsReverse[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 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], 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}}
    if bsk:DoAddPerson(change) then
        bsk:CreateChange(change)
    end
end

function bsk:DoCreateList(change)
    -- TODO: this segment will probably be useful as bsk:SearchForListByName
    local lists = bsk.lists
    for i,v in pairs(lists) do
        if v.name == change.arg.name then
            self:Print(sformat("List %s already exists",v.name))
            return false
        end
    end
    tinsert(lists,{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)
    self:Print("Creating ... " .. name)
    if bsk:DoCreateList(change) then
        bsk:CommitChange(change)
    end
end

-- TODO: break into AddPersonToListEnd and AddPersonToListRandom
function bsk:DoAddPersonToList(change)
    local listIndex = change.arg.listIndex
    local slist = change.arg.slist
    local list = bsk.lists[listIndex]

    if #slist == 1 then -- end of list insertion - just one person
        tinsert(list,slist[1])
        list.time = change.time
    else
        self:Print("Adding to middle of list is not yet supported")
        return false
    end
    return true
end

function bsk:AddPersonToList(name,list)
    -- require admin
    local listIndex = bsk:GetListIndex(list)
    local id = personsReverse[name]
    bsk:Print(sformat("Adding %s (%s) to list %s (%s)", name, id, list, listIndex))
    local slist = {id} -- TODO: support adding to elsewhere besides the end
    local change = {action="AddPersonToList",arg={id=id,listIndex=listIndex,slist=slist}}
    bsk:StartChange(change)
    if bsk:DoAddPersonToList(change) then
        bsk:CommitChange(change)
    end
end

function bsk:DoRemovePerson(change)

    -- return true
end

function bsk:RemovePerson(name)
    -- from both persons and lists
end

function bsk:DoSuicidePerson(change)
    local listIndex = change.arg.listIndex
    local list = bsk.lists[listIndex]
    local slist = shallowCopy(change.arg.list)
    -- 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 stemp = shallowCopy(change.arg.list)
    local temp = table.remove(stemp,1) -- pop
    tinsert(stemp,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] == slist[1] then
            table.remove(slist,1)
            list[i] = stemp[1]
            table.remove(stemp,1)
        end
    end
    list.time=change.time
    --bsk:Print("After")
    --bsk:PrintTable(list)
    return true
end

function bsk:SuicidePerson(name,list)
    -- require admin
    bsk:PopulateRaidList()
    local listIndex = bsk:GetListIndex(list)
    local slist=bsk:GetSuicideList(name,bsk.lists[listIndex])
    local change = {action="SuicidePerson",arg={names=names,list=slist,listIndex=listIndex}}
    bsk:StartChange(change)
    if bsk:DoSuicidePerson(change) then
       bsk:CommitChange(change)
    end
end

function bsk:TrimLists(time)
    if not bsk:CheckListCausality() then
        self:Print("Unable to trim lists 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.listBase,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

--}}}
-- Higher order actions (ie calls other Doers){{{
function bsk:AddMissingPersons()
    bsk:PopulateRaidList() 
    local t = {}
    for _,v in pairs(bsk.persons) do
        t[v.main] = true
        -- TODO: also add alts here
    end
    for i,_ in pairs(raidList) do
        if t[i] == nil then
            bsk:Print(sformat("Person %s is missing from the persons list - adding",i))
            bsk:AddPerson(i)
        end
    end
    -- TODO: batch into a single op - no need to spam 25 messages in a row
end
function bsk:PopulateListRandom(index)
    -- difference (raid+reserve)^list, then random shuffle that, then add
    bsk:PopulateRaidList()
    local list = bsk.lists[index]

    local t = {}
    --for i = 1,#list do
    --    if not (raidList(list[i]) or reserveList(list[i])) then
    --        tinsert(t,)
    --    end
    --end
end
--}}}

-- "Soft" actions- ie things that cause nonpermanent state {{{

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

function bsk:RemoveReserve(name)
    reserveList[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 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()

    wipe(raidList)
    if inRaid > 0 then
        for i = 1, inRaid do
            raidList[UnitName(rID[i])]=true
        end
    elseif inParty > 0 then
        for i = 1, inParty do
            raidList[UnitName(pID[i])]=true
        end
        -- Now add yourself as the last party member
        raidList[UnitName("player")]=true
    else
        -- You're alone
        raidList[UnitName("player")]=true
    end
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(name,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] == name then
            pushing = true
        end
        if pushing and (raidList[list[i]] or reserveList[list[i]]) then
            tinsert(ret,list[i])
        end
    end
    return ret
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
    assert(false)
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