annotate Lists.lua @ 58:615346b6ee99

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