Compare with Previous | Blame | View Log
local LIB_IDENTIFIER = "LibGroupSocket"
local lib = LibStub:NewLibrary(LIB_IDENTIFIER, 2)
if not lib then
return -- already loaded and no upgrade necessary
end
local LMP = LibStub("LibMapPing", true)
if(not LMP) then
error(string.format("[%s] Cannot load without LibMapPing", LIB_IDENTIFIER))
end
local LGPS = LibStub("LibGPS2", true)
if(not LGPS) then
error(string.format("[%s] Cannot load without LibGPS2", LIB_IDENTIFIER))
end
local function Log(message, ...)
df("[%s] %s", LIB_IDENTIFIER, message:format(...))
end
lib.Log = Log
--/script PingMap(89, 1, 1 / 2^16, 1 / 2^16) StartChatInput(table.concat({GetMapPlayerWaypoint()}, ","))
-- smallest step is around 1.428571431461e-005 for Wrothgar, so there should be 70000 steps
-- Coldharbour has a similar step size, meaning we can send 4 bytes of data per ping on both
local WROTHGAR_MAP_INDEX = 27
local COLDHARBOUR_MAP_INDEX = 23
local MAP_METRICS = {
[WROTHGAR_MAP_INDEX] = { zoneIndex = GetZoneIndex(684), stepSize = 1.428571431461e-005 },
[COLDHARBOUR_MAP_INDEX] = { zoneIndex = GetZoneIndex(347), stepSize = 1.4285034012573e-005 },
}
local NO_UPDATE = true
--lib.debug = true -- TODO
lib.cm = lib.cm or ZO_CallbackObject:New()
lib.outgoing = lib.outgoing or {}
lib.incoming = lib.incoming or {}
lib.handlers = lib.handlers or {}
local handlers = lib.handlers
local suppressedList = {}
local panel, button, entry
function lib:GetMapIndexForUnit(unitTag)
if(MAP_METRICS[WROTHGAR_MAP_INDEX].zoneIndex == GetUnitZoneIndex(unitTag)) then
return COLDHARBOUR_MAP_INDEX
else
return WROTHGAR_MAP_INDEX
end
end
function lib:GetStepSizeForUnit(unitTag)
return MAP_METRICS[lib:GetMapIndexForUnit(unitTag)].stepSize
end
------------------------------------------------------- Settings ------------------------------------------------------
local defaultData = {
version = 1,
enabled = false,
autoDisableOnGroupLeft = true,
autoDisableOnSessionStart = true,
handlers = {},
}
-- saved variables are not ready yet so we just use the defaults, the real saved variables will be loaded later in case the standalone lib is active
local saveData = ZO_DeepTableCopy(defaultData)
local function RefreshSettingsPanel()
if(not panel) then return end
CALLBACK_MANAGER:FireCallbacks("LAM-RefreshPanel", panel)
end
local function RefreshGroupMenuKeyboard()
if(not button) then return end
ZO_CheckButton_SetCheckState(button, saveData.enabled)
end
local function RefreshGroupMenuGamepad(noUpdate)
if(not entry) then return end
entry:SetText(saveData.enabled and "Disable sending" or "Enable sending")
if(not noUpdate) then
GAMEPAD_GROUP_MENU:UpdateMenuList()
end
end
local function InitializeGroupMenu()
if(not ZO_GroupMenu_Keyboard) then return end
-- keyboard
button = CreateControlFromVirtual("$(parent)_LibGroupSocketToggle", ZO_GroupMenu_Keyboard, "ZO_CheckButton_Text")
ZO_CheckButton_SetLabelText(button, "LibGroupSocket Sending:")
ZO_CheckButton_SetCheckState(button, saveData.enabled)
ZO_CheckButton_SetToggleFunction(button, function(control, checked)
if(checked ~= saveData.enabled) then
saveData.enabled = checked
RefreshSettingsPanel()
RefreshGroupMenuGamepad()
end
end)
button.label:ClearAnchors()
button.label:SetAnchor(TOPLEFT, ZO_GroupMenu_Keyboard, TOPLEFT, 10, 30)
button:SetAnchor(LEFT, button.label, RIGHT, -40, 0)
-- gamepad
local menu = GAMEPAD_GROUP_MENU
local MENU_ENTRY_TYPE_LGS_TOGGLE = #menu.menuEntries + 1
entry = ZO_GamepadEntryData:New("")
RefreshGroupMenuGamepad(NO_UPDATE)
entry.type = MENU_ENTRY_TYPE_LGS_TOGGLE
entry:SetHeader("LibGroupSocket")
menu.menuEntries[MENU_ENTRY_TYPE_LGS_TOGGLE] = entry
local list = GAMEPAD_GROUP_MENU:GetMainList()
local originalCommit = list.Commit
list.Commit = function(self, ...)
list:AddEntryWithHeader("ZO_GamepadMenuEntryTemplate", entry)
originalCommit(self, ...)
end
local InitializeKeybindDescriptors = menu.InitializeKeybindDescriptors
menu.InitializeKeybindDescriptors = function(self)
InitializeKeybindDescriptors(self)
local primary = menu.keybindStripDescriptor[1]
local callback = primary.callback
primary.callback = function()
callback()
local type = list:GetTargetData().type
if type == MENU_ENTRY_TYPE_LGS_TOGGLE then
PlaySound(SOUNDS.DEFAULT_CLICK)
saveData.enabled = not saveData.enabled
RefreshSettingsPanel()
RefreshGroupMenuKeyboard()
RefreshGroupMenuGamepad()
end
end
end
end
local function InitializeSettingsPanel() -- TODO: localization
local LAM = LibStub("LibAddonMenu-2.0", true)
if(LAM) then -- if LAM is not available, it is not the stand alone version of LGS. As we can't save anything in that case, we don't bother enforcing a dependency.
local function IsSendingDisabled() return not saveData.enabled end
local panelData = {
type = "panel",
name = "LibGroupSocket",
author = "sirinsidiator",
version = "2",
website = "http://www.esoui.com/downloads/info1337-LibGroupSocket.html",
registerForRefresh = true,
registerForDefaults = true
}
panel = LAM:RegisterAddonPanel("LibGroupSocketOptions", panelData)
local optionsData = {}
if(not lib.standalone) then -- the stand alone version contains a file that sets standalone = true
optionsData[#optionsData + 1] = {
type = "description",
text = "No stand alone installation detected. Settings won't be saved.",
reference = "LibGroupSocketStandAloneWarning"
}
end
optionsData[#optionsData + 1] = {
type = "header",
name = "General",
}
optionsData[#optionsData + 1] = {
type = "checkbox",
name = "Enable Sending",
tooltip = "Controls if the library sends any data. It will still receive and process data.",
getFunc = function() return saveData.enabled end,
setFunc = function(value)
saveData.enabled = value
RefreshGroupMenuKeyboard()
RefreshGroupMenuGamepad()
end,
default = defaultData.enabled
}
optionsData[#optionsData + 1] = {
type = "checkbox",
name = "Disable On Group Left",
tooltip = "Automatically disables sending when you leave a group in order to prevent accidentally sending data to a new group.",
getFunc = function() return saveData.autoDisableOnGroupLeft end,
setFunc = function(value) saveData.autoDisableOnGroupLeft = value end,
default = defaultData.enabled
}
optionsData[#optionsData + 1] = {
type = "checkbox",
name = "Disable On Session Start",
tooltip = "Automatically disables sending when you start the game in order to prevent accidentally sending data to an existing group.",
getFunc = function() return saveData.autoDisableOnSessionStart end,
setFunc = function(value) saveData.autoDisableOnSessionStart = value end,
default = defaultData.enabled
}
for handlerType, handler in pairs(handlers) do
if(handler.InitializeSettings) then
handler:InitializeSettings(optionsData, IsSendingDisabled)
end
end
LAM:RegisterOptionControls("LibGroupSocketOptions", optionsData)
end
end
------------------------------------------------- OutgoingPacket Class ------------------------------------------------
local OutgoingPacket = ZO_Object:Subclass()
function OutgoingPacket:New(messageType, data)
local object = ZO_Object.New(self)
object.messageType = messageType
object.header = lib:EncodeHeader(messageType, #data)
object.data = data
object.index = 0
return object
end
function OutgoingPacket:GetNext()
local next
if(self.index < 1) then
next = self.header
else
next = self.data[self.index] or 0
end
self.index = self.index + 1
return next
end
function OutgoingPacket:GetNextCoordinates()
local stepSize = lib:GetStepSizeForUnit("player")
return lib:EncodeData(self:GetNext(), self:GetNext(), self:GetNext(), self:GetNext(), stepSize)
end
function OutgoingPacket:HasMore()
return self.index <= #self.data
end
------------------------------------------------- IncomingPacket Class ------------------------------------------------
local IncomingPacket = ZO_Object:Subclass()
function IncomingPacket:New(unitTag)
local object = ZO_Object.New(self)
object.messageType = -1
object.data = {}
object.length = 0
object.stepSize = lib:GetStepSizeForUnit(unitTag)
return object
end
function IncomingPacket:AddCoordinates(x, y)
local b0, b1, b2, b3 = lib:DecodeData(x, y, self.stepSize)
local data = self.data
if(self.messageType < 0) then
self.messageType, self.length = lib:DecodeHeader(b0)
else
data[#data + 1] = b0
end
if(#data < self.length) then data[#data + 1] = b1 end
if(#data < self.length) then data[#data + 1] = b2 end
if(#data < self.length) then data[#data + 1] = b3 end
end
function IncomingPacket:IsComplete()
return self.length > 0 and #self.data >= self.length
end
local function IsValidMessageType(messageType)
return not (messageType < 0 or messageType > 31)
end
function IncomingPacket:HasValidHeader()
return IsValidMessageType(self.messageType) and self.length > 0 and self.length < 8
end
function IncomingPacket:IsValid()
return self:HasValidHeader() and #self.data == self.length
end
--------------------------------------------- Byte Manipulation Utilities ---------------------------------------------
--- Reads a bit from the data stream and increments the index and bit index accordingly
--- data - an array of integers between 0 and 255
--- index - the current position to read from
--- bitIndex - the current bit inside the current byte (starts from 1)
--- returns the state of the bit, the next position in the data array and the next bitIndex
function lib:ReadBit(data, index, bitIndex)
local p = 2 ^ (bitIndex - 1)
local isSet = (data[index] % (p + p) >= p)
local nextIndex = (bitIndex >= 8 and index + 1 or index)
local nextBitIndex = (bitIndex >= 8 and 1 or bitIndex + 1)
return isSet, nextIndex, nextBitIndex
end
--- Writes a bit to the data stream and increments the index and bit index accordingly
--- data - an array of integers between 0 and 255
--- index - the current position to write to
--- bitIndex - the current bit inside the current byte (starts from 1)
--- value - the new state of the bit
--- returns the next position in the data array and the next bitIndex
function lib:WriteBit(data, index, bitIndex, value)
local p = 2 ^ (bitIndex - 1)
local oldValue = data[index] or 0
local isSet = (oldValue % (p + p) >= p)
if(isSet and not value) then
oldValue = oldValue - p
elseif(not isSet and value) then
oldValue = oldValue + p
end
data[index] = oldValue
local nextIndex = (bitIndex >= 8 and index + 1 or index)
local nextBitIndex = (bitIndex >= 8 and 1 or bitIndex + 1)
return nextIndex, nextBitIndex
end
--- Reads a single byte from the data stream, converts it into a string character and increments the index accordingly
--- data - an array of integers between 0 and 255
--- index - the current position to read from
--- returns the character and the next position in the data array
function lib:ReadChar(data, index)
return string.char(data[index]), index + 1
end
--- Writes a single character to the data stream and increments the index accordingly
--- data - an array of integers between 0 and 255
--- index - the current position to write to
--- value - a single character or a string of characters
--- [charIndex] - optional index of the character that should be written to the data stream. Defaults to the first character
--- returns the next position in the data array
function lib:WriteChar(data, index, value, charIndex)
data[index] = value:byte(charIndex)
return index + 1
end
--- Reads a single byte from the data stream and increments the index accordingly
--- data - an array of integers between 0 and 255
--- index - the current position to read from
--- returns the 8-bit unsigned integer and the next position in the data array
function lib:ReadUint8(data, index)
return data[index], index + 1
end
--- Writes an 8-bit unsigned integer to the data stream and increments the index accordingly
--- The value is clamped and floored to match the data type.
--- data - an array of integers between 0 and 255
--- index - the current position to write to
--- value - an 8-bit unsigned integer
--- returns the next position in the data array
function lib:WriteUint8(data, index, value)
data[index] = math.min(0xff, math.max(0x00, math.floor(value)))
return index + 1
end
--- Reads two byte from the data stream, converts them to one integer and increments the index accordingly
--- data - an array of integers between 0 and 255
--- index - the current position to read from
--- returns the 16-bit unsigned integer and the next position in the data array
function lib:ReadUint16(data, index)
return (data[index] * 0x100 + data[index + 1]), index + 2
end
--- Writes a 16-bit unsigned integer to the data stream and increments the index accordingly
--- The value is clamped and floored to match the data type.
--- data - an array of integers between 0 and 255
--- index - the current position to write to
--- value - a 16-bit unsigned integer
--- returns the next position in the data array
function lib:WriteUint16(data, index, value)
value = math.min(0xffff, math.max(0x0000, math.floor(value)))
data[index] = math.floor(value / 0x100)
data[index + 1] = value % 0x100
return index + 2
end
--- Converts 4 bytes of data into coordinates for a map ping
--- b0 to b3 - integers between 0 and 255
--- step size specifies the smallest possible increment for the coordinates on a map
--- returns normalized x and y coordinates
function lib:EncodeData(b0, b1, b2, b3, stepSize)
b0 = b0 or 0
b1 = b1 or 0
b2 = b2 or 0
b3 = b3 or 0
return (b0 * 0x100 + b1) * stepSize, (b2 * 0x100 + b3) * stepSize
end
--- Converts normalized map ping coordinates into 4 bytes of data
--- step size specifies the smallest possible increment for the coordinates on a map
--- returns 4 integers between 0 and 255
function lib:DecodeData(x, y, stepSize)
x = math.floor(x / stepSize + 0.5) -- round to next integer
y = math.floor(y / stepSize + 0.5)
local b0 = math.floor(x / 0x100)
local b1 = x % 0x100
local b2 = math.floor(y / 0x100)
local b3 = y % 0x100
return b0, b1, b2, b3
end
--- Packs a 5-bit messageType and a 3-bit length value into one byte of data
--- messageType - integer between 0 and 31
--- length - integer between 0 and 7
--- returns encoded header byte
function lib:EncodeHeader(messageType, length)
return messageType * 0x08 + length
end
--- Unpacks a 5-bit messageType and a 3-bit length value from one byte of data
--- value - integer between 0 and 255
--- returns messageType and length
function lib:DecodeHeader(value)
local messageType = math.floor(value / 0x08)
local length = value % 0x08
return messageType, length
end
--------------------------------------------------- Data Processing ---------------------------------------------------
local function SetMapPingOnCommonMap(x, y)
local pingType = MAP_PIN_TYPE_PING
if(lib.debug and not IsUnitGrouped("player")) then
pingType = MAP_PIN_TYPE_PLAYER_WAYPOINT
end
LGPS:PushCurrentMap()
SetMapToMapListIndex(lib:GetMapIndexForUnit("player"))
LMP:SetMapPing(pingType, MAP_TYPE_LOCATION_CENTERED, x, y)
LGPS:PopCurrentMap()
end
local function GetMapPingOnCommonMap(pingType, pingTag)
LGPS:PushCurrentMap()
SetMapToMapListIndex(lib:GetMapIndexForUnit(pingTag))
local x, y = LMP:GetMapPing(pingType, pingTag)
LGPS:PopCurrentMap()
return x, y
end
local function DoSend(isFirst)
local packet = lib.outgoing[1]
if(not packet) then Log("Tried to send when no data in queue") return end
lib.isSending = true
local x, y = packet:GetNextCoordinates()
SetMapPingOnCommonMap(x, y)
lib.hasMore = packet:HasMore()
if(not lib.hasMore) then
table.remove(lib.outgoing, 1)
lib.hasMore = (#lib.outgoing > 0)
end
end
local function IsValidData(data)
if(#data > 7) then
Log("Tried to send %d of 7 allowed bytes", #data)
return false
end
for i = 1, #data do
local value = data[i]
if(type(value) ~= "number" or value < 0 or value > 255) then
Log("Invalid value '%s' at position %d in byte data", tostring(value), i)
return false
end
end
return true
end
--- Queues up to seven byte of data of the selected messageType for broadcasting to all group members
--- messageType - the protocol that is used for encoding the sent data
--- data - up to 7 byte of custom data. if more than 3 bytes are passed, the data will take 2 map pins to arrive.
--- returns true if the data was successfully queued. Data won't be queued when the general sending setting is off or an invalid value was passed.
function lib:Send(messageType, data)
if(not saveData.enabled) then return false end
if(not IsValidMessageType(messageType)) then Log("tried to send invalid messageType %s", tostring(messageType)) return false end
if(not IsValidData(data)) then return false end
-- TODO like all other api functions, this one also has a message rate limit. We need to avoid sending too much or we risk getting kicked
lib.outgoing[#lib.outgoing + 1] = OutgoingPacket:New(messageType, data)
if(not lib.isSending) then
DoSend()
else
lib.hasMore = true
end
return true
end
local function HandleDataPing(pingType, pingTag, x, y, isPingOwner)
x, y = GetMapPingOnCommonMap(pingType, pingTag)
if(not LMP:IsPositionOnMap(x, y)) then return false end
if(not lib.incoming[pingTag]) then
lib.incoming[pingTag] = IncomingPacket:New(pingTag)
end
local packet = lib.incoming[pingTag]
packet:AddCoordinates(x, y)
if(not packet:HasValidHeader()) then -- it might be a user set ping
lib.incoming[pingTag] = nil
return false
end
if(packet:IsComplete()) then
lib.incoming[pingTag] = nil
if(packet:IsValid()) then
lib.cm:FireCallbacks(packet.messageType, pingTag, packet.data, isPingOwner)
else
lib.incoming[pingTag] = nil
Log("received invalid packet from %s", GetUnitName(pingTag))
return false
end
end
if(isPingOwner) then
if(lib.hasMore) then
DoSend()
else
lib.isSending = false
end
end
return true
end
-------------------------------------------------- Map Ping Handling --------------------------------------------------
local function GetKey(pingType, pingTag)
return string.format("%d_%s", pingType, pingTag)
end
local function SuppressPing(pingType, pingTag)
local key = GetKey(pingType, pingTag)
if(not suppressedList[key]) then
LMP:SuppressPing(pingType, pingTag)
suppressedList[key] = true
end
end
local function UnsuppressPing(pingType, pingTag)
local key = GetKey(pingType, pingTag)
if(suppressedList[key]) then
LMP:UnsuppressPing(pingType, pingTag)
suppressedList[key] = false
end
end
LMP:RegisterCallback("BeforePingAdded", function(pingType, pingTag, x, y, isPingOwner)
if(pingType == MAP_PIN_TYPE_PING or (lib.debug and not IsUnitGrouped("player") and pingType == MAP_PIN_TYPE_PLAYER_WAYPOINT)) then
if(HandleDataPing(pingType, pingTag, x, y, isPingOwner)) then -- it is a valid data ping
SuppressPing(pingType, pingTag)
else -- ping is set by player
UnsuppressPing(pingType, pingTag)
end
end
end)
LMP:RegisterCallback("AfterPingRemoved", function(pingType, pingTag, x, y, isPingOwner)
UnsuppressPing(pingType, pingTag)
end)
---------------------------------------------------- Data Handlers ----------------------------------------------------
lib.MESSAGE_TYPE_RESERVED = 0 --- reserved in case we ever have more than 31 message types. can also be used for local tests
lib.MESSAGE_TYPE_RESOURCES = 1 --- for exchanging stamina and magicka values
lib.MESSAGE_TYPE_COMBATSTATS = 2 --- for combat stats like heal, damage and time in combat
--- Registers a handler module for a specific data type.
--- This module will keep everything related to data handling out of any single addon,
--- in order to let multiple addons use the same messageType.
--- messageType - The messageType the handler will take care of
--- handlerVersion - The loaded handler version. Works like the minor version in LibStub and prevents older instances from overwriting a newer one
--- returns the handler object and saveData for the messageType
function lib:RegisterHandler(messageType, handlerVersion)
if handlers[messageType] and handlers[messageType].version >= handlerVersion then
return false
else
handlers[messageType] = handlers[messageType] or {}
handlers[messageType].version = handlerVersion
saveData.handlers[messageType] = saveData.handlers[messageType] or {}
return handlers[messageType], saveData.handlers[messageType]
end
end
--- Gives access to an already registered handler for addons.
--- messageType - The messageType of the handler
--- returns the handler object
function lib:GetHandler(messageType)
return handlers[messageType]
end
--------------------------------------------------------- Misc --------------------------------------------------------
--- Register for unprocessed data of a messageType
function lib:RegisterCallback(messageType, callback)
self.cm:RegisterCallback(messageType, callback)
end
--- Unregister for unprocessed data of a messageType
function lib:UnregisterCallback(messageType, callback)
self.cm:UnregisterCallback(messageType, callback)
end
---------------------------------------------------- Initialization ---------------------------------------------------
local function Unload()
EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_PLAYER_ACTIVATED)
EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_UNIT_DESTROYED)
EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED)
SLASH_COMMANDS["/lgs"] = nil
end
local function Load()
EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_UNIT_DESTROYED, function()
if(saveData.autoDisableOnGroupLeft and not IsUnitGrouped("player")) then
saveData.enabled = false
RefreshSettingsPanel()
RefreshGroupMenuKeyboard()
RefreshGroupMenuGamepad()
end
end)
-- saved variables only become available when EVENT_ADD_ON_LOADED is fired for the library
EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED, function(_ ,addonName)
if(addonName == LIB_IDENTIFIER) then
LibGroupSocket_Data = LibGroupSocket_Data or {}
saveData = LibGroupSocket_Data[GetDisplayName()] or ZO_DeepTableCopy(defaultData)
LibGroupSocket_Data[GetDisplayName()] = saveData
--if(saveData.version == 1) then
-- saveData.setting = defaultData.setting
-- saveData.version = 2
--end
for messageType in pairs(handlers) do
saveData.handlers[messageType] = saveData.handlers[messageType] or {}
end
lib.cm:FireCallbacks("savedata-ready", saveData)
end
end)
-- don't initialize the settings menu before we can be sure that it is the newest version of the lib
EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_PLAYER_ACTIVATED, function(_, initial)
EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_PLAYER_ACTIVATED)
if(saveData.autoDisableOnSessionStart and initial) then
saveData.enabled = false -- don't need to refresh the settings or group menu here, because they are not initialized yet
end
InitializeSettingsPanel()
InitializeGroupMenu()
end)
SLASH_COMMANDS["/lgs"] = function(value)
saveData.enabled = (value == "1")
Log("Data sending %s", saveData.enabled and "enabled" or "disabled")
RefreshSettingsPanel()
RefreshGroupMenuKeyboard()
RefreshGroupMenuGamepad()
end
lib.Unload = Unload
end
if(lib.Unload) then lib.Unload() end
Load()