annotate Lists.lua @ 52:7c7e80f63b51

First pass. Taking stuff from TreeGroup that I like/need/want in order to make a standalone selection list
author John@Yosemite-PC
date Sat, 24 Mar 2012 13:37:29 -0400
parents b155a875de42
children 93acdcd6ace5
rev   line source
John@0 1 -- lists consist of three things
John@0 2 -- 1) a base state - agreed on by one or more list holders
John@0 3 -- 2) change sets - incremental list changes (can be rolled forwards or
John@0 4 -- backwards)
John@0 5 -- 3) working state - not saved because it can be so easily calculated
John@0 6 --
John@0 7 -- A separate user list is held - lists index into this
John@0 8
John@0 9
John@27 10 -- TODO: switch all action functions to use identifiers rather than names
John@27 11 -- TODO: collaborative list trimming
John@24 12 -- TODO: collapse slists into delimited strings for space (premature optimization?)
John@18 13 -- TODO: organize working state data a little more carefully - hard to keep
John@18 14 -- track of all the arrays that are floating out there
John@35 15 -- TODO: players list, drop the "main" sub-entry for people with no alts
John@18 16
John@17 17 -- holy crap long notes {{{
John@4 18 -- notes on list storage:
John@7 19 -- Using names as keys as I do now is atrocious.
John@4 20 -- It prevents insertions (twss) to the middle of the list because then it acts
John@4 21 -- as a side effect onto all the others. ie ABCD -> AXBCD would be phrased as
John@4 22 -- "insert X and shift down B,C,D" which sucks. BCD haven't really been affected
John@4 23 -- (yet) because their relative positions to the others are still intact - ie
John@4 24 -- they are still below A right where they belong. But really X hasn't done
John@4 25 -- anything to affect their relative standing.
John@4 26 --
John@4 27 -- Ok so we can't use names.
John@4 28 --
John@4 29 -- We can't use monotonic integers either because it suffers the same problem.
John@4 30 -- Also consider, randoming in someone to a list of ABCD. Say they roll spot 2.
John@4 31 -- What if someone else runs a separate raid and also randoms someone into slot
John@4 32 -- 2? How do you handle that conflict? Difficult. Also, consider this:
John@4 33 -- List of ABCD on night 1.
John@4 34 -- Admin 1 on night 2 rolls in 30 new people. ABCD's indexes are shuffled to be
John@4 35 -- between 1-35.
John@4 36 -- Admin 2 on night 3 rolls in 5 new ones and people ABCD and PQRST now all have
John@4 37 -- indexes between 1-9.
John@4 38 -- When these two are resolved against one another, do the 1-9 peopole end up on
John@4 39 -- top of the list compared to those other 30?
John@4 40 --
John@4 41 -- Solution:
John@4 42 -- Need a huge random space with purposely left gaps to leave plenty of room for
John@4 43 -- conflicts.
John@4 44 -- So if ABCD had randomed on a space of say, 10,000 and then were sorted into
John@4 45 -- order, then the next 30 could roll into that same space and have a proper
John@4 46 -- ordering. Then the next 5, etc.
John@4 47 --
John@4 48 -- Handling conflicts:
John@4 49 --
John@9 50 -- Executive decision: random on a range of [0,1], ie math.random
John@9 51 -- then on an add-to-end event just do last + .1
John@9 52 -- disallow random after any add-to-end event occurs
John@9 53 -- because the list either elongates beyond 1 OR becomes
John@9 54 -- ridiculously bottom heavy, thus meaning that randoms
John@9 55 -- don't get an even distibution from then on (in fact
John@9 56 -- they'll end up getting top favor)
John@9 57 -- * if a stream contains a random-add after an add-to-end
John@9 58 -- it is declared invalid. tough tits. it's just not a fair
John@9 59 -- distribution at that point.
John@10 60 -- * actually, fuck it. I'll give them an unlock command and
John@10 61 -- let them screw over their lists :)
John@17 62 --}}}
John@18 63
John@17 64 -- there are some dep chains here. for instance, to have a raidIdP value, a
John@42 65 -- person must have a persons value which leads to a personName2id which
John@17 66 -- leads to a raidIdP
John@42 67 local bsk = bsk
John@42 68 local _G=_G
John@42 69 local table=table
John@42 70 local string=string
John@0 71 local tinsert = table.insert
John@0 72 local sformat = string.format
John@0 73 local getn = table.getn
John@42 74 local wipe = wipe
John@42 75 local pairs=pairs
John@42 76 local ipairs=ipairs
John@42 77 local tonumber=tonumber
John@42 78 local tostring=tostring
John@42 79 local time=time
John@42 80 local date=date
John@42 81 local math=math
John@42 82 local type=type
John@42 83 local assert=assert
John@42 84 local getmetatable=getmetatable
John@42 85 local setmetatable=setmetatable
John@42 86 setfenv(1,bsk)
John@0 87
John@49 88 ListEntry =
John@49 89 {
John@49 90 index = 0.0,
John@49 91 id = 0,
John@49 92 }
John@49 93 ListEntry.__index = ListEntry
John@49 94 function ListEntry:new(arg1,arg2)
John@49 95 local n = {}
John@49 96 setmetatable(n,ListEntry)
John@49 97 if type(arg1) == "number" and type(arg2) == "string" then
John@49 98 n.index, n.id = index,id
John@49 99 elseif type(arg1) == "table" and type(arg2) == "nil" then
John@49 100 n.index, n.id = arg1.roll, arg1.id
John@49 101 else
John@49 102 _G.error("Do not know how to construct a ListEntry from " .. type(arg1) .. " and " .. type(arg2))
John@49 103 end
John@49 104 assert(n.index ~= nil)
John@49 105 assert(n.id ~= nil)
John@49 106 return n
John@49 107 end
John@49 108 function ListEntry:GetId()
John@49 109 return self.id
John@49 110 end
John@49 111 function ListEntry:GetIndex()
John@49 112 return self.index
John@49 113 end
John@49 114 function ListEntry:ReplaceId(newId) -- returns the old Id
John@49 115 local temp = self.id
John@49 116 self.id = newId
John@49 117 return temp
John@49 118 end
John@50 119 function ListEntry:GetClass() -- todo: consider if this is ok
John@50 120 return PersonList:Select(self.id):GetClass()
John@50 121 end
John@50 122 function ListEntry:GetName() -- todo: consider if this is ok
John@50 123 return PersonList:Select(self.id):GetName()
John@50 124 end
John@42 125
John@45 126 List =
John@45 127 {
John@46 128 name = "",
John@49 129 time = 0,
John@49 130
John@49 131 -- "private" functions. only private thing about them is that my
John@49 132 -- autocomplete won't pick them up when defined this way
John@49 133 Sort = function(self)
John@49 134 table.sort(self.data,function(a,b) return a:GetIndex() < b:GetIndex() end)
John@49 135 end,
John@49 136 GetLastIndex = function(self)
John@49 137 if not self.data or getn(self.data) == 0 then return 0.0
John@49 138 else return self.data[#self.data]:GetIndex() end
John@49 139 end,
John@49 140 SetTime = function(self,time)
John@49 141 if time == nil or time == 0 then
John@49 142 assert("Dangerous things are afoot")
John@49 143 else
John@49 144 self.time = time
John@49 145 end
John@45 146 end
John@49 147
John@45 148 }
John@49 149 List.__index = List
John@49 150 function List:new(arg1, arg2, arg3)
John@49 151 local n = {data={}}
John@49 152 setmetatable(n,List)
John@49 153 if type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "nil" then
John@49 154 n.name = arg1
John@49 155 n.time = arg2
John@49 156 elseif type(arg1) == "table" and type(arg2) == "nil" and type(arg3) == "nil" then
John@49 157 n.name = arg1.name
John@49 158 if arg1.data then
John@49 159 for _,v in pairs(arg1.data) do
John@49 160 local le = ListEntry:new(v)
John@49 161 table.insert(n.data,entry)
John@49 162 end
John@49 163 n:Sort()
John@49 164 end
John@49 165 n:SetTime(arg1.time)
John@49 166 else
John@49 167 _G.error(sformat("Do not know how to construct list from: %s and %s and %s", type(arg1), type(arg2), type(arg3)))
John@49 168 end
John@45 169 return n
John@45 170 end
John@49 171 function List:HasId(id)
John@49 172 for le in self:OrderedIdIter() do
John@49 173 if id == le then
John@45 174 return true
John@45 175 end
John@45 176 end
John@45 177 return false
John@45 178 end
John@49 179 function List:GetTime()
John@49 180 return self.time
John@49 181 end
John@49 182 function List:GetName()
John@49 183 return self.name
John@49 184 end
John@49 185 function List:GetId()
John@49 186 local listIndex = LootLists:IdOf(self) -- TODO: undo circular dep somehow
John@49 187 return listIndex
John@49 188 end
John@49 189 function List:Rename(packet,time)
John@49 190 self.name = packet.name
John@49 191 self:SetTime(time)
John@49 192 return packet
John@49 193 end
John@49 194 function List:RenameList(newName)
John@49 195 local listIndex = self:GetId()
John@49 196 return InitiateChange("Rename",self,{listIndex=listIndex,name=newName})
John@49 197 end
John@49 198 function List:RemoveEntry(entry,time)
John@49 199 local pos = self:Select(entry.id)
John@49 200 if pos then
John@49 201 --print("Removing id " .. entry.id .. " pos " .. pos)
John@49 202 table.remove(self.data,pos)
John@49 203 self:Sort()
John@49 204 self:SetTime(time)
John@49 205 return pos
John@49 206 end
John@49 207 --print("Failed removal of ...")
John@49 208 --PrintTable(entry)
John@49 209 end
John@49 210 function List:Remove(id)
John@49 211 -- TODO: check id
John@49 212 local listIndex = self:GetId()
John@49 213 return InitiateChange("RemoveEntry",self,{listIndex=listIndex,id=id})
John@49 214 end
John@49 215 function List:InsertEndEntry(packet,time)
John@49 216 if self:InsertEntry(packet,time) then
John@49 217 self.closedRandom = true
John@49 218 return packet
John@49 219 end
John@49 220 end
John@49 221 function List:InsertEntry(packet,time)
John@49 222 local le = ListEntry:new(packet)
John@49 223 table.insert(self.data,le)
John@49 224 self:Sort()
John@49 225 self:SetTime(time)
John@49 226 --printf("Inserting %s to %s", packet.id, packet.listIndex)
John@49 227 return le
John@49 228 end
John@49 229 function List:InsertEnd(id) -- returns the LE created
John@49 230 if self:Select(id) then
John@49 231 printf("Person %s is already on the reqeuested list",id) -- todo: lookup name
John@49 232 return false
John@49 233 end
John@49 234 local index = self:GetLastIndex() + 0.1
John@49 235 local le = ListEntry:new(index,id)
John@49 236 local listIndex = self:GetId()
John@49 237 return InitiateChange("InsertEndEntry",self,{listIndex=listIndex,id=id,roll=index})
John@49 238 end
John@49 239 function List:InsertRandom(id) -- returns the LE created
John@49 240 if self.closedRandom then
John@49 241 print("Cannot add person to list by random roll because an add-to-end operation has already occurred")
John@49 242 return false
John@49 243 end
John@49 244 if self:Select(id) then
John@49 245 printf("Person %s is already on the reqeuested list",id) -- todo: lookup name
John@49 246 return false
John@49 247 end
John@49 248 local index = math.random()
John@49 249 local listIndex = self:GetId()
John@49 250 return InitiateChange("InsertEntry",self,{listIndex=listIndex,id=id,roll=index})
John@49 251 end
John@50 252 function List:GetLength()
John@50 253 return #self.data
John@50 254 end
John@49 255 function List:OrderedListEntryIter()
John@49 256 local i = 0
John@49 257 local n = #self.data
John@49 258
John@49 259 return function()
John@49 260 i = i+1
John@49 261 if i<=n then return self.data[i] end
John@49 262 end
John@49 263 end
John@49 264 function List:OrderedIdIter()
John@49 265 local i = 0
John@49 266 local n = #self.data
John@49 267 return function()
John@49 268 i = i+1
John@49 269 if i<=n then return self.data[i]:GetId() end
John@49 270 end
John@49 271 end
John@49 272 function List:GetAllIds()
John@49 273 local t = {}
John@49 274 for id in self:OrderedIdIter() do
John@49 275 table.insert(t,id)
John@49 276 end
John@49 277 return t
John@49 278 end
John@49 279 function List:Select(entry) -- returns the position and the entry
John@49 280 if type(entry) == "string" then -- search by id
John@49 281 local ids = self:GetAllIds()
John@49 282 for i,v in ipairs(ids) do
John@49 283 if v == entry then
John@49 284 return i, self.data[i]
John@49 285 end
John@49 286 end
John@49 287 return false, nil
John@49 288 else
John@49 289 assert("undone")
John@49 290 return false, nil
John@49 291 end
John@49 292 end
John@49 293 function List:Suicide(packet,time)
John@49 294 -- the goal here is to rotate the suicide list by 1
John@49 295 -- then we can just mash it on top of the intersection between the original
John@49 296 -- list and the working copy
John@49 297 local affected = shallowCopy(packet.affect)
John@49 298 local replacement = shallowCopy(packet.affect)
John@49 299 local temp = table.remove(replacement,1) -- pop
John@49 300 tinsert(replacement,temp) -- push_back
John@49 301 --rintf(("Before suicide of %s on list %s",slist[1],list.name)
John@49 302 --PrintTable(list)
John@49 303 for le in self:OrderedListEntryIter() do
John@49 304 if le:GetId() == affected[1] then
John@49 305 le:ReplaceId(replacement[1])
John@49 306 table.remove(affected,1)
John@49 307 table.remove(replacement,1)
John@49 308 end
John@49 309 end
John@49 310 -- TODO: flag error if affected and replacement aren't both empty now
John@49 311 self:SetTime(time)
John@49 312 return packet
John@49 313 end
John@49 314 function List:SuicidePerson(id)
John@49 315 -- first calculate the effect, then initiate the change
John@49 316 PersonList:RefreshRaidList()
John@49 317 local slist = {}
John@49 318 local pushing = false
John@49 319 for le in self:OrderedListEntryIter() do -- get all ids
John@49 320 local lid = le:GetId()
John@49 321 if lid == id then
John@49 322 pushing = true
John@49 323 end
John@49 324 if pushing and PersonList:IsActive(lid) then -- TODO: decouple
John@49 325 tinsert(slist,lid)
John@49 326 end
John@49 327 end
John@49 328 local listIndex = self:GetId()
John@49 329 return InitiateChange("Suicide",self,{listIndex=listIndex,affect=slist})
John@49 330 end
John@49 331
John@49 332 LootLists =
John@49 333 {
John@49 334 --l = {} -- list of List objects, keyed by id
John@49 335 }
John@49 336
John@49 337 -- generate self, then generate sublists
John@49 338 function LootLists:ConstructFromDB(db)
John@49 339 self:Reset()
John@49 340 local saved = db.profile.lists
John@49 341 for i,v in pairs(saved) do -- upconvert saved to true list objects
John@49 342 self.l[i] = List:new(v)
John@49 343 end
John@49 344 end
John@49 345 function LootLists:SaveToDB(db)
John@49 346 db.profile.lists = self.l
John@49 347 end
John@49 348 function LootLists:CreateList(packet,time)
John@49 349 local le = List:new(packet.name,time)
John@49 350 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
John@49 351 self.l[packet.id] = le
John@49 352 return le
John@49 353 end
John@49 354 function LootLists:Create(name)
John@49 355 return InitiateChange("CreateList",self,{name=name})
John@49 356 end
John@49 357 function LootLists:DeleteList(packet,time)
John@49 358 self.l[packet.listIndex] = nil
John@49 359 return id
John@49 360 end
John@49 361 function LootLists:Delete(index)
John@49 362 -- TODO: is there anything to check first, or just fire away?
John@49 363 return InitiateChange("DeleteList",self,{listIndex=index})
John@49 364 end
John@49 365 function LootLists:Select(id)
John@49 366 if type(id) == "number" then -- by id
John@49 367 return self.l[id]
John@49 368 elseif type(id) == "string" then -- name
John@49 369 for i,v in pairs(self.l) do
John@49 370 if v:GetName() == id then
John@49 371 return v
John@49 372 end
John@49 373 end
John@49 374 end
John@49 375 return nil
John@49 376 end
John@49 377 function LootLists:Reset()
John@49 378 self.l = {}
John@49 379 end
John@49 380 function LootLists:GetAllIds()
John@49 381 local t = {}
John@49 382 for i,v in pairs(self.l) do
John@49 383 table.insert(t,i)
John@49 384 end
John@49 385 return t
John@49 386 end
John@49 387 function LootLists:IdOf(list)
John@49 388 for i,v in pairs(self.l) do
John@49 389 if v == list then
John@49 390 return i
John@49 391 end
John@49 392 end
John@49 393 end
John@50 394 -- todo: iterator for lists ... it's pretty necessary
John@49 395
John@49 396 Toon =
John@49 397 {
John@49 398 id=0,
John@49 399 name="",
John@49 400 class=""
John@49 401 }
John@49 402 Toon.__index = Toon
John@49 403 function Toon:new(arg1,arg2,arg3)
John@49 404 local t = {}
John@49 405 setmetatable(t,Toon)
John@49 406 if type(arg1) == "number" and type(arg2) == "string" and type(arg3) == "string" then
John@49 407 t.id, t.name, t.class = arg1, arg2, arg3
John@49 408 elseif type(arg1) == "table" and arg2 == nil and arg3 == nil then
John@49 409 t.id, t.name, t.class = arg1.id, arg1.name, arg1.class
John@49 410 else
John@49 411 error("Cannot construct a toon object from types " .. type(arg1) .. ", " .. type(arg2) .. ", " .. type(arg3))
John@49 412 end
John@49 413 return t
John@49 414 end
John@49 415 function Toon:GetName()
John@49 416 return self.name
John@49 417 end
John@49 418 function Toon:GetId()
John@45 419 return self.id
John@45 420 end
John@49 421 function Toon:GetClass()
John@49 422 return self.class
John@45 423 end
John@45 424
John@49 425 PersonList =
John@49 426 {
John@49 427 toons = {},
John@49 428 time = 0,
John@49 429 active = { raid={}, reserve={} }
John@49 430 }
John@45 431
John@49 432 function PersonList:ConstructFromDB(db)
John@49 433 self:Reset()
John@49 434 local dbp = db.profile.persons
John@49 435 self.time = dbp.time
John@49 436 if dbp.toons == nil then return end
John@49 437 for i,v in pairs(dbp.toons) do
John@49 438 local te = Toon:new(v)
John@49 439 table.insert(self.toons,te)
John@49 440 end
John@49 441 end
John@49 442 function PersonList:SaveToDB(db)
John@49 443 db.profile.persons = { toons=self.toons, time=self.time }
John@49 444 end
John@49 445 function PersonList:Reset()
John@49 446 self.toons = {}
John@49 447 self.time = 0
John@49 448 self.active = { raid={}, reserve={}, raidExtras={} }
John@49 449 end
John@49 450 function PersonList:Select(id)
John@49 451 -- both id and name are strings, but there won't be clashes
John@49 452 -- because an ID will contain either a number or all caps letters
John@49 453 -- and names must be long enough to ensure that one of those is true
John@49 454 if type(id) == "string" then
John@49 455 for i,v in pairs(self.toons) do
John@49 456 --print(i)
John@49 457 --PrintTable(v)
John@49 458 if v:GetName() == id or v:GetId() == id then
John@49 459 return v
John@49 460 end
John@49 461 end
John@49 462 end
John@49 463 end
John@49 464 function PersonList:AddToon(packet,time)
John@49 465 local te = Toon:new(packet)
John@49 466 table.insert(self.toons,te)
John@49 467 return te
John@49 468 end
John@49 469 function PersonList:Add(name)
John@49 470 local guid = _G.UnitGUID(name)
John@49 471 -- TODO: check guid to be sure it's a player
John@49 472 if not guid then
John@49 473 printf("Could not add player %s - they must be in range or group",name)
John@49 474 return
John@49 475 end
John@49 476 local _,englishClass = _G.UnitClass(name)
John@49 477 --print("Person " .. name .. " is class " .. englishClass)
John@49 478 local id = string.sub(guid,6) -- skip at least 0x0580 ...
John@49 479 id = id:gsub("^0*(.*)","%1") -- nom all leading zeroes remaining
John@49 480
John@49 481 local pe = self:Select(id)
John@49 482 if pe and pe:GetName() ~= name then
John@49 483 printf("Namechange detected for %s - new is %s, please rename the existing entry", pe:GetName(), name)
John@49 484 return
John@49 485 end
John@49 486 if pe then
John@49 487 printf("%s is already in the persons list; disregarding", name)
John@49 488 return
John@49 489 end
John@45 490
John@49 491 return InitiateChange("AddToon", self, {name=name, id=id, class=englishClass})
John@49 492 end
John@49 493 function PersonList:RemoveToon(packet,time)
John@49 494 local id = packet.id
John@49 495 for i,v in pairs(self.toons) do
John@49 496 if v:GetId() == id then
John@49 497 table.remove(self.toons,i)
John@49 498 return v
John@49 499 end
John@49 500 end
John@49 501 end
John@49 502 function PersonList:Remove(ident)
John@49 503 local le = PersonList:Select(ident)
John@49 504 if not le then
John@49 505 printf("%s is not in the persons list, please check your spelling", ident)
John@49 506 return false
John@49 507 end
John@49 508 local id = le:GetId()
John@49 509 local listsTheyreOn = {}
John@45 510
John@49 511 -- check if they're active on any loot list
John@49 512 local allListIds = LootLists:GetAllIds()
John@49 513 for _,v in pairs(allListIds) do
John@49 514 if LootLists:Select(v):HasId(id) then -- TODO: this is ineloquent
John@49 515 tinsert(listsTheyreOn,LootLists:Select(v):GetName())
John@49 516 break
John@49 517 end
John@49 518 end
John@49 519 if getn(listsTheyreOn) > 0 then
John@49 520 printf("Cannot remove person %s because they are on one or more lists (%s)",ident,table.concat(listsTheyreOn,", "))
John@49 521 return false
John@49 522 end
John@49 523 return InitiateChange("RemoveToon", self, {id=id})
John@49 524 end
John@49 525 function PersonList:IsRegistered(id)
John@49 526 if self:Select(id) ~= nil then return true end
John@49 527 end
John@49 528 function PersonList:AddReserve(id)
John@49 529 local le = self:Select(id)
John@49 530 if le then
John@49 531 -- todo: check that they're not already reserved
John@49 532 self.active.reserve[le:GetId()] = true
John@49 533 end
John@49 534 end
John@49 535 -- todo: remove reserve
John@50 536 function PersonList:IsActive(id) -- todo: support LE as input - saves IsActive(le:GetId())
John@49 537 return self.active.raid[id] or self.active.reserve[id]
John@49 538 end
John@49 539 function PersonList:AddMissing()
John@49 540 self:RefreshRaidList()
John@49 541 for _,name in pairs(self.active.raidExtras) do
John@49 542 printf("Person %s is missing from the persons list - adding",name)
John@49 543 self:Add(name)
John@49 544 end
John@49 545 -- TODO: batch into a single op - no need to spam 25 messages in a row
John@49 546 end
John@49 547 function PersonList:GetAllActiveIds()
John@49 548 self:RefreshRaidList()
John@49 549 local t = {}
John@49 550 for i,v in pairs(self.active.raid) do
John@49 551 if v then table.insert(t,i) end
John@49 552 end
John@49 553 for i,v in pairs(self.active.reserve) do
John@49 554 if v then table.insert(t,i) end
John@49 555 end
John@49 556 return t
John@49 557 end
John@45 558
John@49 559 -- The following (adapted) code is from Xinhuan (wowace forum member)
John@49 560 -- Pre-create the unitId strings we will use
John@49 561 local pId = {}
John@49 562 local rId = {}
John@49 563 for i = 1, 4 do
John@49 564 pId[i] = sformat("party%d", i)
John@49 565 end
John@49 566 for i = 1, 40 do
John@49 567 rId[i] = sformat("raid%d", i)
John@49 568 end
John@49 569 function PersonList:RefreshRaidList()
John@49 570 local inParty = _G.GetNumPartyMembers()
John@49 571 local inRaid = _G.GetNumRaidMembers()
John@49 572 local add = function(unitNameArg)
John@49 573 local name = _G.UnitName(unitNameArg)
John@49 574 local te = self:Select(name)
John@49 575 if te then
John@49 576 self.active.raid[te:GetId()]=true
John@49 577 else
John@49 578 table.insert(self.active.raidExtras,name)
John@49 579 end
John@49 580 --if personName2id[name] ~= nil then
John@49 581 -- raidIdP[personName2id[name]]=true
John@49 582 --end
John@49 583 end
John@45 584
John@49 585 self.active.raid = {}
John@49 586 self.active.raidExtras = {}
John@49 587 if inRaid > 0 then
John@49 588 for i = 1, inRaid do
John@49 589 add(rId[i])
John@49 590 end
John@49 591 elseif inParty > 0 then
John@49 592 for i = 1, inParty do
John@49 593 add(pId[i])
John@49 594 end
John@49 595 -- Now add yourself as the last party member
John@49 596 add("player")
John@49 597 else
John@49 598 -- You're alone
John@49 599 add("player")
John@49 600 end
John@49 601 end
John@45 602
John@45 603
John@49 604 function GetSafeTimestamp()
John@49 605 local changes = db.profile.changes
John@49 606 local ctime = time()
John@49 607 local n = getn(changes)
John@49 608 if n > 0 then
John@49 609 if changes[n].time >= ctime then
John@49 610 ctime = changes[n].time + 1
John@49 611 end
John@49 612 end
John@49 613 return ctime
John@49 614 end
John@45 615
John@49 616 function InitiateChange(finalizeAction,acceptor,arg)
John@49 617 local change = {}
John@49 618 change.time = GetSafeTimestamp()
John@49 619 change.action = finalizeAction
John@49 620 change.arg = arg
John@45 621
John@49 622 if acceptor[finalizeAction](acceptor,arg,change.time) then
John@49 623 table.insert(db.profile.changes,change)
John@49 624 -- TODO: broadcast
John@49 625 return arg
John@49 626 else
John@49 627 return nil
John@49 628 end
John@49 629 end
John@49 630 function ProcessChange(change)
John@49 631 -- try list-o-lists and persons - if has matching function, call it
John@49 632 local action = change.action
John@49 633 if PersonList[action] then
John@49 634 PersonList[action](PersonList,change.arg,change.time)
John@49 635 return
John@49 636 elseif LootLists[action] then
John@49 637 LootLists[action](LootLists,change.arg,change.time)
John@49 638 return
John@49 639 else
John@49 640 -- pray that the change has a listIndex in it ...
John@49 641 if change.arg.listIndex then
John@49 642 local l = LootLists:Select(change.arg.listIndex)
John@49 643 if l and l[action] then
John@49 644 l[action](l,change.arg,change.time)
John@49 645 return
John@49 646 end
John@49 647 end
John@49 648 end
John@49 649 _G.error("Could not process change: " .. change.action)
John@49 650 end
John@42 651
John@42 652 function SelfDestruct()
John@49 653 LootLists:Reset()
John@49 654 PersonList:Reset()
John@42 655 db.profile.persons = {}
John@42 656 db.profile.changes = {}
John@42 657 db.profile.lists = {}
John@17 658 end
John@0 659
John@1 660 -- Debugging {{{
John@42 661 function PrettyPrintList(listIndex)
John@49 662 PersonList:RefreshRaidList()
John@49 663 local le = LootLists:Select(listIndex)
John@49 664 print("List: " .. le:GetName() .. " (" .. le:GetId() .. ") - last modified " .. date("%m/%d/%y %H:%M:%S", le:GetTime()) .. " ("..le:GetTime()..")" )
John@49 665 local pos = 1
John@49 666 for i in le:OrderedIdIter() do -- ordered iterator
John@49 667 local s = ""
John@49 668 if PersonList:IsActive(i) then
John@49 669 s = "*"
John@49 670 end
John@49 671
John@49 672 print(" " .. pos .. " - " .. PersonList:Select(i):GetName() .. " ("..i..")",s)
John@49 673 pos = pos + 1
John@9 674 end
John@9 675 end
John@42 676 function PrettyPrintLists()
John@49 677 for _,i in pairs(LootLists:GetAllIds()) do
John@42 678 PrettyPrintList(i)
John@9 679 end
John@9 680 end
John@42 681 function PrintLists()
John@49 682 PrintTable(LootLists)
John@1 683 end
John@42 684 function PrintChanges()
John@42 685 PrintTable(db.profile.changes)
John@1 686 end
John@42 687 function PrintPersons()
John@49 688 PrintTable(PersonList)
John@1 689 end
John@42 690 function PrintAPI(object)
John@39 691 for i,v in pairs(object) do
John@39 692 if type(v) == "function" then
John@43 693 print("function "..i.."()")
John@39 694 end
John@39 695 end
John@39 696 end
John@0 697 --}}}
John@0 698
John@43 699 -- Change processing {{{
John@42 700 function CreateWorkingStateFromChanges(changes)
John@0 701 -- copy the base to the working state
John@49 702 LootLists:ConstructFromDB(db)
John@49 703 PersonList:ConstructFromDB(db)
John@0 704
John@0 705 -- now just go through the changes list applying each
John@5 706 for i,v in ipairs(changes) do
John@42 707 ProcessChange(v)
John@0 708 end
John@16 709 end
John@16 710
John@16 711 --}}}
John@49 712
John@27 713 -- holy crap long winded {{{
John@0 714 -- timestamp logic:
John@0 715 -- use time() for comparisons - local clients use date() to make it pretty. only
John@0 716 -- dowisde - we can't have a server timestamp. Which kind of sucks, but it turns
John@0 717 -- out you can change timezones when you enter an instance server, so you really
John@0 718 -- never know what time it is.
John@0 719 -- There's unfortunately no hard-and-proven method for determining the true time
John@0 720 -- difference between local time and server time. You can't just query the two
John@0 721 -- and compare them because your server timezone can change (!) if you go into
John@0 722 -- an instance server with a different timezone. This is apparently a big
John@0 723 -- problem on Oceanic realms.
John@0 724 --
John@0 725 -- Timestamp handling (brainstorming how to deal with drift):
John@0 726 -- (not an issue) if someone sends you time in the future, update your offset so you won't
John@0 727 -- send out events in the "past" to that person
John@0 728 -- (not an issue - using local UTC now) on change-zone-event: check if you've changed timezones - might need update
John@0 729 -- each time you add a change, check the tail of the change list; if this is
John@0 730 -- less than that, you have a problem. Print a message. if this is equal, then
John@0 731 -- that's ok, just bump it by 1 second. This could happen in the case of, say,
John@0 732 -- spam-clicking the undo button or adding names to the list. The recipients
John@0 733 -- should be ok with this since they'll follow the same algorithm. The only
John@0 734 -- real chance for a problem is if two people click within the 1 second window?
John@0 735 -- if someone sends you a past event,
John@0 736 -- it's ok if it's newer than anything in the changes list
John@0 737 -- otherwise ... causality has been violated.
John@0 738 -- Whenever an admin signon event happens, have the admins each perform a
John@0 739 -- timestamp check. Issue warnings for anyone with a clock that's more than
John@0 740 -- X seconds out of sync with the others. Seriously, why isn't NTP a standard
John@0 741 -- setting on all operating systems ...
John@27 742 --}}}
John@0 743
John@1 744 -- Action and DoAction defs {{{
John@27 745 -- Action Discussion {{{
John@0 746 -- The actual actions for changes start here
John@0 747 --
John@42 748 -- Each action occurs as a pair of functions. The Action() function is from
John@0 749 -- a list admin's point of view. Each will check for admin status, then create a
John@0 750 -- change bundle, call the handler for that change (ie the DoAction func), and
John@0 751 -- then record/transmist the bundle. These are simple and repetitive functions.
John@0 752 --
John@42 753 -- The DoAction() function is tasked with executing the bundle and is what
John@0 754 -- non-admins and admins alike will call to transform their working state via a
John@0 755 -- change packet. Each Do() function will accept *only* a change packet, and
John@0 756 -- it's assumed that the change has been vetted elsewhere. These are very blunt
John@0 757 -- routines.
John@0 758 --
John@0 759 -- Note that "undo" has no special voodoo to it. It's basically a change that
John@27 760 -- reverses the prior change on the stack.--}}}
John@42 761 function AddPerson(name)--{{{
John@49 762 print("Adding ... " .. name)
John@49 763 PersonList:Add(name)
John@26 764 end--}}}
John@42 765 function CreateList(name)--{{{
John@0 766 -- require admin
John@43 767 print("Creating ... " .. name)
John@49 768 return LootLists:Create(name)
John@26 769 end--}}}
John@42 770 function AddPersonToListEnd(name,listName)--{{{
John@0 771 -- require admin
John@49 772 local l = LootLists:Select(listName)
John@49 773 local te = PersonList:Select(name)
John@49 774 -- TODO: if not te ...
John@49 775 printf("Adding %s (%s) to list %s", name, te:GetId(), listName)
John@49 776 return l:InsertEnd(te:GetId())
John@26 777 end--}}}
John@42 778 function AddPersonToListRandom(name,listName)--{{{
John@10 779 -- require admin
John@49 780 local l = LootLists:Select(listName)
John@49 781 local te = PersonList:Select(name)
John@49 782 -- TODO: if not te ...
John@49 783 printf("Adding %s (%s) to list %s - randomly!", name, te:GetId(), listName)
John@49 784 return l:InsertRandom(te:GetId())
John@26 785 end--}}}
John@42 786 function SuicidePerson(name,listName)--{{{
John@0 787 -- require admin
John@49 788 PersonList:RefreshRaidList()
John@49 789 local le = LootLists:Select(listName)
John@49 790 local te = PersonList:Select(name)
John@49 791 return le:SuicidePerson(te:GetId())
John@26 792 end--}}}
John@42 793 function RenameList(listName,newListName)--{{{
John@20 794 -- require admin
John@49 795 local le = LootLists:Select(listName)
John@49 796 return le:RenameList(newListName)
John@26 797 end--}}}
John@42 798 function DeleteList(listName)--{{{
John@49 799 return LootLists:DeleteList(LootLists:Select(listName):GetId())
John@26 800 end--}}}
John@42 801 function RemovePersonFromList(name,listName)--{{{
John@49 802 local le = LootLists:Select(listName)
John@49 803 local te = PersonList:Select(name)
John@49 804 return le:Remove(te:GetId())
John@22 805 end
John@20 806 --}}}
John@49 807 function RemovePerson(person)
John@49 808 print("Removing " .. person)
John@49 809 PersonList:Remove(person)
John@49 810 end
John@49 811 function ReservePerson(person)
John@49 812 print("Reserving " .. person)
John@49 813 PersonList:AddReserve(person)
John@49 814 end
John@26 815 --}}}
John@20 816 -- Higher order actions (ie calls other standard actions){{{
John@20 817
John@42 818 function TrimLists(time)
John@42 819 if not CheckListCausality() then
John@43 820 print("Unable to trim changelist due to violated causality")
John@5 821 return false
John@5 822 end
John@5 823
John@5 824 if type(time) ~= "number" then
John@5 825 time = tonumber(time)
John@5 826 end
John@5 827
John@5 828 -- bisect the changes list by "time"
John@5 829 local before = {}
John@42 830 for i,v in ipairs(db.profile.changes) do
John@5 831 if v.time <= time then
John@5 832 tinsert(before,v)
John@5 833 else
John@5 834 break
John@5 835 end
John@5 836 end
John@5 837
John@5 838 -- apply first half
John@42 839 CreateWorkingStateFromChanges(before)
John@5 840
John@5 841 -- save this state permanently; trim the changes permanently
John@49 842 LootLists:SaveToDB(db)
John@49 843 PersonList:SaveToDB(db)
John@42 844 while db.profile.changes ~= nil and db.profile.changes[1] ~= nil and db.profile.changes[1].time <= time do
John@42 845 table.remove(db.profile.changes,1)
John@5 846 end
John@5 847
John@5 848 -- using the trimmed list and the new bases, recreate the working state
John@42 849 CreateWorkingStateFromChanges(db.profile.changes)
John@5 850 end
John@5 851
John@29 852
John@42 853 function PopulateListRandom(listIndex)
John@17 854 -- difference (raid+reserve)-list, then random shuffle that, then add
John@49 855 local actives = PersonList:GetAllActiveIds()
John@49 856 local list = LootLists:Select(listIndex)
John@3 857
John@49 858 --swap keys on actives
John@49 859 local t = {}
John@49 860 for _,v in pairs(actives) do t[v] = true end
John@17 861
John@17 862 -- now remove from t all of the people already present on the list
John@49 863 if t then
John@49 864 for id in list:OrderedIdIter() do -- id iterator
John@49 865 if t[id] then
John@49 866 t[id] = false
John@21 867 end
John@17 868 end
John@17 869 end
John@17 870
John@17 871 -- add all remaining
John@17 872 for i,v in pairs(t) do
John@17 873 if v then
John@49 874 AddPersonToListRandom(i,list:GetId())
John@17 875 end
John@17 876 end
John@3 877 end
John@42 878 function NukePerson(name) -- delete from all lists and then from persons
John@49 879 for _,id in pairs(LootLists:GetAllIds()) do
John@49 880 RemovePersonFromList(name,id)
John@30 881 end
John@42 882 RemovePerson(name)
John@30 883 end
John@1 884 --}}}
John@0 885
John@0 886 -- undo rules!
John@0 887 -- only the most recent event can be undone
John@0 888 -- ^^^ on a given list?
John@0 889 -- algorithm is easy, given "Suicide A B C"
John@0 890 -- just find A,B,C in the list and replace in order from the s message
John@0 891 -- while undo is allowed *per-list*, certain events in the stream will
John@0 892 -- prevent proper undo, such as add/delete player or add/delete list
John@0 893
John@5 894 -- returns true if the events in the list are in time order
John@42 895 function CheckListCausality()
John@5 896 local t = nil
John@42 897 for i,v in ipairs(db.profile.changes) do
John@5 898 if t ~= nil then
John@5 899 if v.time <= t then
John@5 900 return false
John@5 901 end
John@5 902 end
John@5 903 t = v.time
John@5 904 end
John@5 905 return true
John@5 906 end
John@0 907