Repository navigation
Expand file tree
/
Copy pathPeerState.lua
More file actions
468 lines (449 loc) · 14.6 KB
/
Copy pathPeerState.lua
File metadata and controls
468 lines (449 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
-- Common ordering for public metadata, requested snapshots and departures.
-- This contains only addon-owned primitive timestamps, never private locations.
local QT = _G.QuestTogether
local TTL, LIMIT = 600, 1024
local function Now(a)
return a:SafeToNumber(a.API.GetTime())
end
local FRESHNESS_FIELDS = {
"at", "sampledAt", "receivedAt", "expiresAt", "lifetime", "cached", "manualSnapshot",
"observationSource", "observationName", "observationSession", "observationSequence", "sourceSampledAt",
}
-- Views over accepted records must keep the sample's provenance. In particular,
-- comparing fractional receipt times cannot order two whole-second wire samples.
function QT:CopyPeerRecordFreshness(source, target)
for _, field in ipairs(FRESHNESS_FIELDS) do
target[field] = source[field]
end
return target
end
function QT:ComparePeerRecordSamples(left, right)
if not left then return right and -1 or 0 end
if not right then return 1 end
local leftStamp, rightStamp = left.sourceSampledAt, right.sourceSampledAt
if leftStamp and rightStamp then
if leftStamp ~= rightStamp then return leftStamp > rightStamp and 1 or -1 end
-- Sequence numbers are comparable only within one sender's session. An
-- unsequenced diagnostic sample or another sender cannot break this tie.
if left.observationName and left.observationName == right.observationName
and left.observationSession and left.observationSession == right.observationSession
and left.observationSequence and right.observationSequence then
local difference = left.observationSequence - right.observationSequence
return difference > 0 and 1 or difference < 0 and -1 or 0
end
return 0
end
local difference = (left.sampledAt or left.at or 0) - (right.sampledAt or right.at or 0)
return difference > 0 and 1 or difference < 0 and -1 or 0
end
function QT:ComparePeerObservationToRecord(observation, record)
return self:ComparePeerRecordSamples({
sampledAt = observation.sampledAt, sourceSampledAt = observation.sourceSampledAt,
observationName = observation.name, observationSession = observation.session,
observationSequence = observation.sequence,
}, record)
end
local function State(a)
local state = rawget(a, "peerSnapshotState")
if not state then
state = { peers = {}, count = 0 }
a.peerSnapshotState = state
end
return state
end
local function Record(a, name, create)
local state, now = State(a), Now(a)
local row = state.peers[name]
if row and (not now or now < row.at or now - row.at >= TTL) then
state.peers[name], state.count, row = nil, state.count - 1, nil
end
if not row and create and now then
if state.count >= LIMIT then
local oldest, at
for key, peer in pairs(state.peers) do
if not at or peer.at < at then
oldest, at = key, peer.at
end
end
if oldest then
state.peers[oldest] = nil
state.count = state.count - 1
end
end
row = { at = now, fields = {} }
state.peers[name] = row
state.count = state.count + 1
end
return row, now
end
-- Stamped protocols share epoch ownership. A parsed packet is not evidence of
-- a new session until one of its fields has passed freshness/content validation.
function QT:CanAcceptPeerEpoch(name, session, stamp)
local row = Record(self, name, false)
local epoch = row and row.epoch
if not epoch or epoch.session == session then
return true
end
if epoch.retired[session] then
return false
end
-- A legacy unstamped packet or a same-second tie cannot establish which
-- unknown epoch is newer. A subsequent stamped publication resolves it.
if epoch.stamp and epoch.stamp > 0 and (not stamp or stamp <= epoch.stamp) then
return false
end
return true
end
function QT:RecordPeerEpoch(name, session, stamp)
if not self:CanAcceptPeerEpoch(name, session, stamp) then
return false
end
local row, now = Record(self, name, true)
if not row then
return false
end
local epoch = row.epoch
if not epoch then
epoch = { session = session, retired = {}, order = {} }
row.epoch = epoch
end
if epoch.session ~= session then
epoch.retired[epoch.session] = true
epoch.order[#epoch.order + 1] = epoch.session
if #epoch.order > 8 then
epoch.retired[table.remove(epoch.order, 1)] = nil
end
epoch.session, epoch.stamp = session, nil
end
if stamp and stamp > 0 then
epoch.stamp = math.max(epoch.stamp or 0, stamp)
end
row.at = now
return true
end
-- Read-only identity evidence query. A signed observation belongs only to the
-- current peer session and cannot survive a departure later than that proof.
function QT:IsPeerSessionObservationCurrent(name, session, sampledAt)
local row = Record(self, name, false)
return row ~= nil
and row.epoch ~= nil
and row.epoch.session == session
and (not row.departed or sampledAt > row.departed.sampledAt)
end
function QT:CanAcceptPeerUpdate(name, field, sampledAt, session, sequence, sourceSampledAt)
local row, now = Record(self, name, false)
if
not now
or not sampledAt
or sampledAt > now + 1
or now - sampledAt >= TTL
or self:IsIgnoredPlayerName(name)
or self:IsSelfSender(name)
then
return false
end
if not row then
return true
end
local departed = row.departed
if departed then
if session and departed.session == session then
if not sequence or sequence <= departed.sequence then
return false
end
elseif sampledAt <= departed.sampledAt and (not session or sampledAt < departed.sampledAt) then
return false
end
end
local previous = field and row.fields[field]
if previous then
if sourceSampledAt and previous.sourceSampledAt then
if sourceSampledAt < previous.sourceSampledAt then return false end
if sourceSampledAt == previous.sourceSampledAt
and not (session and session == previous.session and sequence and previous.sequence and not previous.manual) then
-- Unsequenced diagnostic samples cannot be ordered within a source
-- second. Keep the accepted fact until a strictly newer sample arrives.
return false
end
end
-- Envelope ages are whole seconds while receipt clocks are fractional.
-- For two ordered public samples, a newer sequence resolves that rounding
-- ambiguity. A manual refresh has no sequence and keeps strict ordering.
local orderedNewer = session
and previous.session == session
and sequence
and previous.sequence
and sequence > previous.sequence
and not previous.manual
if sampledAt < previous.sampledAt and not (orderedNewer and previous.sampledAt - sampledAt < 1) then
return false
end
if
session
and previous.session == session
and sequence
and previous.sequence
and sequence <= previous.sequence
then
return false
end
end
return true
end
function QT:RecordPeerUpdate(name, field, sampledAt, session, sequence, establishesPresence, sourceSampledAt)
if not self:CanAcceptPeerUpdate(name, field, sampledAt, session, sequence, sourceSampledAt) then
return false
end
local row, now = Record(self, name, true)
local previous = row.fields[field]
row.fields[field] = {
sampledAt = sampledAt,
session = session or (previous and previous.session),
sequence = sequence or (previous and previous.sequence),
manual = session == nil,
sourceSampledAt = sourceSampledAt,
}
row.at, row.latestSampledAt = now, math.max(row.latestSampledAt or sampledAt, sampledAt)
if session and sequence then
if establishesPresence ~= false then
row.activeAfterDeparture = true
end
-- Shared wire observations carry the same session and sequence across topics.
if session:match("^%d+%-%d+$") then
if row.session ~= session then
row.session, row.sequence = session, sequence
else
row.sequence = math.max(row.sequence or 0, sequence)
end
end
end
return true
end
-- Explicit, addon-owned observation metadata travels with the decoded field.
-- Producers retain distinct freshness policies; transport never edits payload stores.
function QT:CreatePeerObservation(name, field, options)
if
type(options) ~= "table"
or not self:CanAccessTable(options)
or not self:CanAccessValue(options.source)
or type(options.source) ~= "string"
or options.source == ""
then
return nil
end
local now = Now(self)
local age = self:SafeToNumber(options.age) or 0
local lifetime = self:SafeToNumber(options.lifetime) or TTL
if not now or age < 0 or age >= lifetime then
return nil
end
return {
name = name,
field = field,
source = options.source,
receivedAt = now,
sampledAt = now - age,
expiresAt = now - age + lifetime,
session = options.session,
sequence = options.sequence,
stamp = options.stamp,
sourceSampledAt = self:SafeToNumber(options.sourceSampledAt),
manual = options.source == "diagnostic",
}
end
function QT:ResolvePeerObservation(name, field, observation)
if
type(observation) ~= "table"
or not self:CanAccessTable(observation)
or not self:CanAccessValue(observation.name)
or not self:CanAccessValue(observation.field)
or observation.name ~= name
or observation.field ~= field
or not self:CanAccessValue(observation.source)
or type(observation.source) ~= "string"
or observation.source == ""
then
return nil
end
return observation
end
function QT:CanAcceptPeerObservation(observation)
local now = Now(self)
if
type(observation) ~= "table"
or not now
or type(observation.receivedAt) ~= "number"
or type(observation.sampledAt) ~= "number"
or type(observation.expiresAt) ~= "number"
or observation.receivedAt > now
or observation.sampledAt > observation.receivedAt
or observation.expiresAt <= now
then
return false
end
return observation
and observation.name
and observation.field
and (observation.stamp == nil or self:CanAcceptPeerEpoch(
observation.name,
observation.session,
observation.stamp
))
and self:CanAcceptPeerUpdate(
observation.name,
observation.field,
observation.sampledAt,
observation.session,
observation.sequence,
observation.sourceSampledAt
)
or false
end
function QT:CommitPeerObservation(observation, record, establishesPresence)
if not self:CanAcceptPeerObservation(observation) then
return false
end
local name = observation.name
if establishesPresence and not self:RecordQTPlayerPresence(name, true, observation) then
return false
end
if
not self:RecordPeerUpdate(
name,
observation.field,
observation.sampledAt,
observation.session,
observation.sequence,
establishesPresence == true,
observation.sourceSampledAt
)
then
return false
end
if observation.stamp ~= nil then
self:RecordPeerEpoch(name, observation.session, observation.stamp)
end
if record then
record.sampledAt, record.receivedAt = observation.sampledAt, observation.receivedAt
record.lifetime, record.expiresAt = observation.expiresAt - observation.receivedAt, observation.expiresAt
record.observationSource, record.manualSnapshot = observation.source, observation.manual
record.observationName, record.observationSession = observation.name, observation.session
record.observationSequence, record.sourceSampledAt = observation.sequence, observation.sourceSampledAt
end
return true
end
function QT:CanRecordPeerPresence(name, observation)
local row = Record(self, name, false)
if not row or not row.departed or row.activeAfterDeparture then
return true
end
return observation
and observation.name == name
and self:CanAcceptPeerUpdate(name, nil, observation.sampledAt, observation.session, observation.sequence)
or false
end
function QT:RecordPeerPresenceFromSnapshot(name, sampledAt, session, sequence)
local now = Now(self)
if not now or not sampledAt then
return false
end
local observation = { name = name, sampledAt = sampledAt, session = session, sequence = sequence }
if not self:CanAcceptPeerUpdate(name, nil, sampledAt, session, sequence) then
return false
end
local accepted = self:RecordQTPlayerPresence(name, true, observation)
if accepted then
local row = Record(self, name, true)
row.activeAfterDeparture = true
end
return accepted
end
function QT:RecordPeerDeparture(name, sampledAt, session, sequence)
if not self:CanAcceptPeerUpdate(name, "QTPR", sampledAt, session, sequence) then
return false
end
local row, now = Record(self, name, true)
local orderedNewer = session and row.session == session and sequence and sequence >= (row.sequence or 0)
if
(
row.latestSampledAt
and sampledAt < row.latestSampledAt
and not (orderedNewer and row.latestSampledAt - sampledAt < 1)
) or (session and row.session == session and sequence and sequence < (row.sequence or 0))
then
return false
end
row.at = now
row.activeAfterDeparture = false
row.departed = { sampledAt = sampledAt, session = session, sequence = sequence or 0 }
local presence = rawget(self, "qtPlayerPresenceState")
if presence then
local status = presence.questPartners and presence.questPartners[name]
if status then
-- Retain the real inner stream counter to reject delayed old On copies.
status.looking, status.receivedAt, status.sampledAt, status.lifetime = false, now, sampledAt, TTL
end
if presence.partnerQuests then
presence.partnerQuests[name] = nil
end
end
self:RecordQTPlayerPresence(name, false)
return true
end
function QT:MigratePlayerMapVisibility(profile)
if type(profile) ~= "table" then
return
end
if profile.mapPartyOnly == true then
profile.showPlayerLocations = false
end
profile.mapPartyOnly, profile.mapAlwaysShowParty = nil, nil
end
function QT:GetPlayerMapVisibilityChoice()
if self:GetOption("showPlayerLocations") ~= true or self:GetOption("mapPartyOnly") == true then
return 3
end
return self:GetOption("onlyShowQuestPartners") == true and 2 or 1
end
-- Blizzard owns our party's map indicators. Every QT location consumer uses
-- this same rule, including temporary developer dots and nearby streams.
function QT:ShouldShowPlayerLocation(name)
if
not self.isEnabled
or self.isLoggingOut
or self:GetOption("showPlayerLocations") ~= true
or self:GetOption("mapPartyOnly") == true
or self:IsSelfSender(name)
or self:IsGroupedSender(name)
or self:IsIgnoredPlayerName(name)
then
return false
end
return self:GetOption("onlyShowQuestPartners") ~= true or self:IsPlayerLookingForQuestPartners(name)
end
function QT:ResetPeerSnapshots()
self.peerSnapshotState = nil
end
-- Read admitted sample times through their owner instead of traversing feature caches.
function QT:ArePeerFieldsFresh(name, fields, maxAge)
name = self:NormalizeMemberName(name)
local row, now
if name then
row, now = Record(self, name, false)
end
if not row or not now then
return false
end
for _, field in ipairs(fields) do
local sample = row.fields[field]
if
not sample
or now < sample.sampledAt
or now - sample.sampledAt >= maxAge
or not self:CanAcceptPeerUpdate(name, nil, sample.sampledAt, sample.session, sample.sequence)
then
return false
end
end
return true
end