annotate Lists.lua @ 69:b7352f007028

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