view Lists.lua @ 0:47fac96968e1

First commit
author John@Yosemite-PC
date Fri, 02 Mar 2012 00:15:09 -0500
parents
children 21c58930f74e
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



bsk.lists = {}
bsk.players = {}

local RaidList = {}
local ReserveList = {}
local activeList = 0 -- temporary

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

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

-- Debugging {{{
function bsk:PrintLists()
    bsk:PrintTable(bsk.lists)
end
function bsk:PrintChanges()
    bsk:PrintTable(bsk.db.profile.changes)
end
function bsk:PrintPlayers()
    bsk:PrintTable(bsk.players)
end
--}}}

function bsk:CreateWorkingStateFromChanges()
    local playerBase = self.db.profile.players
    local listBase = self.db.profile.listBase
    local changes = self.db.profile.changes

    -- copy the base to the working state
    wipe(bsk.lists)
    wipe(bsk.players)
    bsk:tcopy(bsk.lists,listBase)
    bsk:tcopy(bsk.players,playerBase)

    -- now just go through the changes list applying each
    for i,v in pairs(changes) do
        bsk:ProcessChange(v)
    end
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 == "AddPlayer" then
        bsk:DoAddPlayer(change)
    elseif change.action == "CreateList" then
        bsk:DoCreateList(change)
    elseif change.action == "AddPlayerToList" then
        bsk:DoAddPlayerToList(change)
    elseif change.action == "SuicidePlayer" then
        bsk:DoSuicidePlayer(change)
    else
        bsk:Print("Unknown message encountered")
        bsk:PrintTable(change)
        assert(false)
    end 
end


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

-- Players list
function bsk:DoAddPlayer(change)
    assert(change)
    assert(change.arg.guid)
    local arg = change.arg
    -- require admin
    local players = bsk.players
    local name = arg.name
    local guid = arg.guid
    assert(players[guid]==nil)
    players[guid] = name
    players.time=change.time
    return true
end

function bsk:AddPlayer(name)
    local players = bsk.players
    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
    if players[guid] and players[guid] ~= name then
        self:Print(sformat("Namechange detected for %s - new is %s, please rename the existing entry", players[guid], name))
        return
    end
    if players[guid] ~= nil then
        self:Print(sformat("%s is already in the players list; disregarding", name))
        return
    end
    local change = {action="AddPlayer",arg={name=name,guid=guid}}
    if bsk:DoAddPlayer(change) then
        bsk:CreateChange(change)
    end
end

function bsk:CreateFakeLists()
    -- testing only
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

function bsk:DoAddPlayerToList(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:AddPlayerToList(name,list)
    -- require admin
    local listIndex = bsk:GetListIndex(list)
    local slist = {name} -- TODO: support adding to elsewhere besides the end
    local change = {action="AddPlayerToList",arg={name=name,listIndex=listIndex,slist=slist}}
    bsk:StartChange(change)
    if bsk:DoAddPlayerToList(change) then
        bsk:CommitChange(change)
    end
end

function bsk:DoRemovePlayer(change)

    -- return true
end

function bsk:RemovePlayer(name)
    -- from both players and lists
end

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

function bsk:GetActiveList()
    return bsk.lists[1] -- todo!
end

function bsk:DoSuicidePlayer(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:SuicidePlayer(name,list)
    -- require admin
    local l=bsk:GetActiveList()
    bsk:PopulateRaidList()
    local slist=bsk:GetSuicideList(name,l)
    local listIndex = bsk:GetListIndex(list)
    local change = {action="SuicidePlayer",arg={names=names,list=slist,listIndex=listIndex}}
    bsk:StartChange(change)
    if bsk:DoSuicidePlayer(change) then
       bsk:CommitChange(change)
    end
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




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









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