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()