annotate Lists.lua @ 101:25c127c4c1e8

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