-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLibDataShare.lua
552 lines (466 loc) · 16.3 KB
/
LibDataShare.lua
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
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
local NAME = "LibDataShare"
local lib = {
version = "dev"
}
_G[NAME] = lib
local LMP = LibMapPing2
if LMP and not LMP.internal then LMP = nil end -- a naive check for LMP version just to avoid calling really old ones
local LGB = LibGroupBroadcast
local LGB_MESSAGE_ID = 2
local LGB_Handler = {}
local LGB_Protocol = {}
local mapHandlers = {} -- currently registered maps
local ENABLED = true -- this lib won't send or process map pings if this setting is set to false
local MAIN_MAP_INDEX = 30 -- Vvardenfell
local PING_RATE = 2020 -- minimum time between pings
local lastPingTime = 0 -- time of the latest map ping
local lastOnPingTime = 0 -- time of the latest received and processed map ping
local EM = EVENT_MANAGER
local time = GetGameTimeMilliseconds
local ApiVersion = GetAPIVersion()
local WorldName = GetWorldName()
local function IsCallable(f)
return type(f) == "function"
end
-- Utility object that works with a single map used by a specific addon to send map pings.
-- Each addon must acquire its own object by calling LibDataShare:RegisterMap()
local MapHandler = {}
function MapHandler:New(owner, mapId, dataHandler)
local handler = {mapId = mapId, owner = owner}
setmetatable(handler, self)
self.__index = self
handler:SetDataHandler(dataHandler)
return handler
end
-- Returns the number of unique points on a single axis of the current map.
-- This number squared is the largest number you can send via QueueData() and SendData().
function MapHandler:GetMapSize()
return lib.maps[self.mapId].size
end
-- Queue data for sending. It'll be sent as soon as it's safe to send the next ping.
-- If callback is a function, then it'll be called right after the data has been sent, but not received yet.
-- Returns the number of milliseconds this data needs to "wait" to be sent.
function MapHandler:QueueData(data, callback)
local t = time()
local x, y = lib:EncodeData(self.mapId, data)
local nextPingDelay = zo_max(0, lastPingTime - t + PING_RATE) -- make sure to always have at least PING_RATE delay between any pings
zo_callLater(function()
local result = false
if lib:PrepareMap(self.mapId) then
lib:SetMapPing(x, y)
lib:RestoreMap()
result = true
end
if IsCallable(callback) then
callback(result)
end
end, nextPingDelay)
lastPingTime = t + nextPingDelay
return nextPingDelay > 0 and nextPingDelay or 1
end
-- Send data instantly without checking if it's safe to do so (too many map pings in a row will kick the player from server).
-- You might want to call LibDataShare:IsSendWindow() before sending data, but if you need to send something urgently and rarely, then it's usually safe to do without any checks.
-- Returns true if data has been successfully sent, false otherwise.
function MapHandler:SendData(data)
if lib:PrepareMap(self.mapId) then
local x, y = lib:EncodeData(self.mapId, data)
lib:SetMapPing(x, y)
lib:RestoreMap()
lastPingTime = zo_max(lastPingTime, time())
return true
else
return false
end
end
-- A shortcut for LibDataShare:IsSendWindow().
function MapHandler:IsSendWindow()
return LibDataShare:IsSendWindow()
end
-- Set a function that will handle incoming data.
-- The function will receive 3 values: unitTag (sender's group tag), data (received data), time (time in milliseconds when the data was received)
function MapHandler:SetDataHandler(func)
self.dataHandler = func
end
-- When the lib is disabled, it won't send or process incoming pings.
function lib:IsEnabled()
return ENABLED
end
-- Enable/disable data sharing.
function lib:SetEnabled(enabled)
if enabled then
ENABLED = true
else
ENABLED = false
end
end
-- Encode data (a number between 0 and the corresponding map's amount of steps^2) into map coordinates.
-- mapId is ESO internal mapId found here: https://wiki.esoui.com/Maps
function lib:EncodeData(mapId, data)
local map = lib.maps[mapId]
if map and data >= 0 and data <= map.size^2 then
return zo_floor(data / map.size) * map.step, (data % map.size) * map.step
else
return 0, 0
end
end
--- Decode data from map coordinates.
function lib:DecodeData(mapId, x, y)
local map = lib.maps[mapId]
if map then
return zo_floor(x / map.step + 0.5) * map.size + zo_floor(y / map.step + 0.5)
else
return 0
end
end
-- Register a map for data sharing.
-- owner: name of the addon that wants to register the map.
-- mapId: check Map.lua for available maps.
-- dataHandler: function that will handle incoming data. It will receive 3 values: unitTag (sender's group tag), data (received data), time (time in milliseconds when the data was received).
-- Returns MapHandler object.
function lib:RegisterMap(owner, mapId, dataHandler)
if lib.maps[mapId] then
if mapHandlers[mapId] then
error(string.format("Map %d is already registered by %s.", mapId, mapHandlers[mapId].owner))
else
local handler = MapHandler:New(owner, mapId, dataHandler)
mapHandlers[mapId] = handler
return handler
end
end
end
-- Stop using a map for data sharing.
function lib:UnregisterMap(mapId)
mapHandlers[mapId] = nil
end
-- Get map ping for unitTag.
function lib:GetMapPing(unitTag)
-- Use the original function.
if LMP then
return LMP.internal.handler.original.GetMapPing(unitTag)
else
return GetMapPing(unitTag)
end
end
-- Set map ping at (x, y).
function lib:SetMapPing(x, y)
-- Use the original function.
if LMP then
LMP.internal.handler.original.PingMap(MAP_PIN_TYPE_PING, MAP_TYPE_LOCATION_CENTERED, x, y)
else
PingMap(MAP_PIN_TYPE_PING, MAP_TYPE_LOCATION_CENTERED, x, y)
end
end
-- Returns true, if it's safe to send a map ping (enough time has passed since the last ping).
-- Returns false otherwise.
function lib:IsSendWindow()
local t = time()
return t - lastPingTime > PING_RATE and t - lastOnPingTime > 100
end
-- Mute all ping sounds.
function lib:MutePings()
SOUNDS.MAP_PING = nil
SOUNDS.MAP_PING_REMOVE = nil
end
-- Unmute all ping sounds.
function lib:UnmutePings()
SOUNDS.MAP_PING = 'Map_Ping'
SOUNDS.MAP_PING_REMOVE = 'Map_Ping_Remove'
end
-- Minimum time between pings sent by the lib.
function lib:GetPingRate()
return PING_RATE
end
-- Disable data sharing in other addons.
-- This function is called by HodorReflexes when it's enabled, otherwise it will conflict with all of the following addons.
-- You need to disable Hodor Reflexes to use data sharing features of these addons.
function lib:ResolveConflicts()
-- Disable RaidNotifier ult exchange
if RaidNotifier then
RaidNotifier:UnregisterForUltimateChanges()
end
-- Disable Bandits sharing
if BUI and BUI.Vars then
if BUI.Vars.StatShare and BUI.StatShare then
BUI.Vars.StatShare = false
BUI.StatShare:Initialize(true)
end
if BUI.Vars.StatsShareDPS then
BUI.Vars.StatsShareDPS = false
end
if BUI.Vars.StatsUpdateDPS then
BUI.Vars.StatsUpdateDPS = false
EM:UnregisterForUpdate("BUI_ShareDPS")
end
BUI.StatShare.OnPing = function(eventCode, pingEventType, pingType, unitTag, offsetX, offsetY, isOwner) return end
end
-- Disable FTC dps sharing
if FTC and FTC.Stats then
FTC.Vars.StatsShareDPS = false
end
-- Disable Taos Group Tools
if TGT_SettingsHandler then
TGT_SettingsHandler.SavedVariables.IsSendingDataActive = false
EM:UnregisterForUpdate('TGT-PlayerHandler')
end
-- Disable Piece of Candy
if POC and POC.Comm and POC.Settings then
POC.Comm.Unload()
POC.Settings.SavedVariables.CommOff = true
end
-- Unregister all registered callbacks in LibMapPing.
-- It might be a bit too brutal, but helps to avoid potential conflicts and performance issues, e.g.
-- with LibGroupSocket, which processes every incoming ping without a way to disable it.
if LMP then
LMP.internal.callbackObject:UnregisterAllCallbacks("BeforePingAdded")
LMP.internal.callbackObject:UnregisterAllCallbacks("AfterPingRemoved")
end
end
--[[
-- Internal stuff.
--]]
-- Player opens/hides the world map.
local worldMapState = false -- world map is showing
local worldMapUpdate = false -- world map needs to be updated
local function MapStateChange(oldState, newState)
if newState == SCENE_SHOWING then
if worldMapUpdate then
ZO_WorldMap_UpdateMap()
worldMapUpdate = false
end
worldMapState = true
elseif newState == SCENE_HIDDEN then
worldMapState = false
end
end
-- Change map to Vvardenfell if possible.
local prepareMapResult = SET_MAP_RESULT_FAILED -- using SetMapResultCode constants, because why not
function lib:PrepareMap(mapId)
-- Don't prepare map if data sharing is disabled.
if not ENABLED then return false end
local sameMap = DoesCurrentMapMatchMapForPlayerLocation()
-- The following check fails if the player is viewing the map of another zone,
-- because we can't set Vvardenfell map and return back without complex operations (like in LibGPS).
if worldMapState and not sameMap then
prepareMapResult = SET_MAP_RESULT_FAILED
else
prepareMapResult = sameMap and SET_MAP_RESULT_CURRENT_MAP_UNCHANGED or SET_MAP_RESULT_MAP_CHANGED
SetMapToMapListIndex(mapId)
end
return prepareMapResult ~= SET_MAP_RESULT_FAILED
end
-- Return to the current map (should be called after each PrepareMap()).
function lib:RestoreMap()
if prepareMapResult ~= SET_MAP_RESULT_FAILED then
SetMapToPlayerLocation()
if prepareMapResult == SET_MAP_RESULT_MAP_CHANGED then
-- If player changed a subzone, then we need to update the world map, but only when he opens it.
worldMapUpdate = true
end
end
end
-- EVENT_MAP_PING handler.
local function OnMapPing(eventCode, pingEventType, pingType, pingTag, offsetX, offsetY, isLocalPlayerOwner)
if pingEventType == PING_EVENT_ADDED and pingType == MAP_PIN_TYPE_PING then
local t = time()
if isLocalPlayerOwner then
-- Ignore own ping.
lastPingTime = t
elseif lib:PrepareMap(MAIN_MAP_INDEX) then
lastOnPingTime = t
local x, y = lib:GetMapPing(pingTag)
-- If it's a ping on the main map, then use its handler directly, otherwise search for a suitable handler in mapHandlers.
if x >= 0 and x <= 1 and y >= 0 and y <= 1 then
local handler = mapHandlers[MAIN_MAP_INDEX]
if handler and IsCallable(handler.dataHandler) then
handler.dataHandler(pingTag, lib:DecodeData(MAIN_MAP_INDEX, x, y), t)
end
else
for mapId, handler in pairs(mapHandlers) do
if mapId ~= MAIN_MAP_INDEX then
local map = lib.maps[mapId]
if map and x >= map.x0 - map.step and x <= map.x1 + map.step and y >= map.y0 - map.step and y <= map.y1 + map.step then
if lib:PrepareMap(mapId) then
x, y = lib:GetMapPing(pingTag)
if IsCallable(handler.dataHandler) then
handler.dataHandler(pingTag, lib:DecodeData(mapId, x, y), t)
end
end
break
end
end
end
end
lib:RestoreMap()
end
end
end
--------------------------------------------------------------------------
--------------- COMPATIBILITY LAYER WITH LibGroupBroadcast ---------------
--------------------------------------------------------------------------
local queuedMessages = {}
local function sendQueuedMessages()
if not ENABLED then queuedMessages = {} return end
for mapId, message in pairs(queuedMessages) do
LGB_Protocol:Send({
mapId = mapId,
data = message.data
})
if IsCallable(message.callback) then
message.callback(true)
end
end
queuedMessages = {}
end
local function encodeNumber(num)
local byteArray = {}
while num > 0 do
table.insert(byteArray, num % 256)
num = math.floor(num / 256)
end
return byteArray
end
local function decodeNumber(byteArray)
local num = 0
for i = #byteArray, 1, -1 do
num = num * 256 + byteArray[i]
end
return num
end
local function mapHandler_SendData(self, data)
if not ENABLED then return end
LGB_Protocol:Send({
mapId = self.mapId,
data = encodeNumber(data)
})
end
local function mapHandler_QueueData(self, data, callback)
if not ENABLED then return end
local mapId = self.mapId
local msg = {
mapId = mapId,
data = encodeNumber(data),
callback = callback
}
queuedMessages[mapId] = msg
return GetGroupAddOnDataBroadcastCooldownRemainingMS()
end
local function lib_GetMapPing(self, unitTag) return nil end
local function lib_SetMapPing(self, x, y) return end
local function lib_IsSendWindow(self) return GetGroupAddOnDataBroadcastCooldownRemainingMS() <= 0 end
local function lib_ResolveConflicts(self) return end
local function lib_PrepareMap(self, mapId) return true end
local function lib_RestoreMap(self) return end
local function overwriteLibFunctions()
lib.GetMapPing = lib_GetMapPing
lib.SetMapPing = lib_SetMapPing
lib.IsSendWindow = lib_IsSendWindow
lib.ResolveConflicts = lib_ResolveConflicts
lib.PrepareMap = lib_PrepareMap
lib.RestoreMap = lib_RestoreMap
end
local function overwriteMapHandlerFunctions()
MapHandler.SendData = mapHandler_SendData
MapHandler.QueueData = mapHandler_QueueData
end
local function unregisterSendQueuedMessages()
EM:UnregisterForUpdate(NAME .. "SendQueuedMessages")
end
local function registerSendQueuedMessages()
unregisterSendQueuedMessages()
EM:RegisterForUpdate(NAME .. "SendQueuedMessages", PING_RATE, sendQueuedMessages)
end
local function OnMessageReceived(unitTag, data)
if AreUnitsEqual(unitTag, "player") then
return
end
local handler = mapHandlers[data.mapId]
if handler and IsCallable(handler.dataHandler) then
handler.dataHandler(unitTag, decodeNumber(data.data), t)
end
end
local function DeclareLGBProtocols()
local CreateArrayField = LGB.CreateArrayField
local CreateNumericField = LGB.CreateNumericField
local handler = LGB:RegisterHandler(NAME)
local protocol = handler:DeclareProtocol(LGB_MESSAGE_ID, NAME)
protocol:AddField(CreateNumericField("mapId", {
minValue = 2,
maxValue = 38,
}))
protocol:AddField(CreateArrayField(CreateNumericField("data", {
minValue = 0,
maxValue = 2^8-1,
}), {
maxLength = 5
}))
protocol:OnData(OnMessageReceived)
if not protocol:Finalize({
isRelevantInCombat = true,
replaceQueuedMessages = false,
}) then
error("Failed to finalize LibDataShare legacy protocol")
end
LGB_Handler = handler
LGB_Protocol = protocol
end
--------------------------------------------------------------------------
local function OnPlayerActivated()
-- EOL detection
if ApiVersion >= 101046 then
if WorldName ~= "PTS" then
zo_callLater(function()
d("|cFF00FFWarning: 'LibDataShare' is now disabled due to API restrictions in Update 46. The MapPing API only allows one ping every 10 seconds, and this library is no longer functional. Please uninstall it to avoid seeing this message.|r")
end, 5000)
-- remove functionality of the addon completely by overwriting all functions of the Library
MapHandler.QueueData = function() end
MapHandler.SendData = function() end
lib.PrepareMap = function() return false end
lib.IsEnabled = function() return false end
lib.GetMapPing = function() return 0, 0 end
lib.SetMapPing = function() end
lib.ResolveConflicts = function() end
OnMapPing = function() end
MapStateChange = function() end
return -- exit Initialize function here to not load the OnPlayerActivated function
end
-- set the ping rate to every 10seconds because of API changes in U46
PING_RATE = 10200
zo_callLater(function()
d("|cFF00FFImportant: Please remove 'LibDataShare' from your add-ons! The MapPing API has been rate-limited in Update 46, allowing only one ping every 10 seconds. Continuing to use this library may result in being kicked from the server.|r")
end, 5000)
end
-- Unregister map ping handler.
EM:UnregisterForEvent(NAME, EVENT_MAP_PING)
-- Unregister map state changes.
WORLD_MAP_SCENE:UnregisterCallback("StateChange", MapStateChange)
GAMEPAD_WORLD_MAP_SCENE:UnregisterCallback("StateChange", MapStateChange)
if lib:IsEnabled() then
-- Register map ping handler.
EM:RegisterForEvent(NAME, EVENT_MAP_PING, OnMapPing)
-- Register map state changes.
WORLD_MAP_SCENE:RegisterCallback("StateChange", MapStateChange)
GAMEPAD_WORLD_MAP_SCENE:RegisterCallback("StateChange", MapStateChange)
-- Mute all pings once and for all.
lib:MutePings()
else
lib:UnmutePings()
end
end
local function Initialize()
if not LGB then
-- run without compatibility layer
EM:RegisterForEvent(NAME, EVENT_PLAYER_ACTIVATED, OnPlayerActivated)
return
end
-- create compatibility layer
DeclareLGBProtocols()
overwriteLibFunctions()
overwriteMapHandlerFunctions()
registerSendQueuedMessages()
d("LibDataShare compatibility mode initiated")
end
EM:RegisterForEvent(NAME, EVENT_ADD_ON_LOADED, function(_, name)
if name ~= NAME then return end
EM:UnregisterForEvent(NAME, EVENT_ADD_ON_LOADED)
Initialize()
end)