local LIB_IDENTIFIER = "LibGroupSocket" local lib = LibStub:NewLibrary(LIB_IDENTIFIER, 1) 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 --lib.debug = true -- TODO --/script PingMap(88, 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 -- meaning we can send 4 bytes of data per ping local stepSize = 1.428571431461e-005 local stepCount = 1/stepSize local WROTHGAR_MAP_INDEX = 27 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 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 function lib:ReadChar(data, index) return string.char(data[index]), index + 1 end function lib:WriteChar(data, index, value, charIndex) data[index] = value:byte(charIndex) return index + 1 end function lib:ReadUint8(data, index) return data[index], index + 1 end function lib:WriteUint8(data, index, value) data[index] = value return index + 1 end function lib:ReadUint16(data, index) return (data[index] * 0x100 + data[index + 1]), index + 2 end function lib:WriteUint16(data, index, value) data[index] = math.floor(value / 0x100) data[index + 1] = math.floor(value % 0x100) return index + 2 end function lib:EncodeData(b0, b1, b2, b3) 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 function lib:DecodeData(x, y) x = math.floor(x * stepCount + 0.5) -- round to next integer y = math.floor(y * stepCount + 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 function lib:EncodeHeader(addonId, length) return addonId * 0x08 + length end function lib:DecodeHeader(value) local addonId = math.floor(value / 0x08) local length = value % 0x08 return addonId, length 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 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(WROTHGAR_MAP_INDEX) -- TODO: check if we want to use 2 different maps to avoid setting stuff on the local zone map for easier user input detection LMP:SetMapPing(pingType, MAP_TYPE_LOCATION_CENTERED, x, y) LGPS:PopCurrentMap() end local function GetMapPingOnCommonMap(pingType, pingTag) LGPS:PushCurrentMap() SetMapToMapListIndex(WROTHGAR_MAP_INDEX) -- TODO: check if we want to use 2 different maps to avoid setting stuff on the local zone map for easier user input detection 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 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] end self.index = self.index + 1 return next end function OutgoingPacket:GetNextCoordinates() local x = lib:EncodeData(self:GetNext(), self:GetNext()) local y = lib:EncodeData(self:GetNext(), self:GetNext()) return x, y end function OutgoingPacket:HasMore() return self.index < #self.data end local IncomingPacket = ZO_Object:Subclass() function IncomingPacket:New() local object = ZO_Object.New(self) object.messageType = -1 object.data = {} object.length = 0 return object end function IncomingPacket:AddCoordinates(x, y) local b0, b1, b2, b3 = lib:DecodeData(x, y) 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 lib.cm = ZO_CallbackObject:New() function lib:RegisterCallback(messageType, callback) self.cm:RegisterCallback(messageType, callback) end function lib:UnregisterCallback(messageType, callback) self.cm:UnregisterCallback(messageType, callback) end lib.outgoing = {} lib.incoming = {} function lib:Send(messageType, data) if(not IsValidMessageType(messageType)) then Log("tried to send invalid messageType %s", tostring(messageType)) return end if(not IsValidData(data)) then return end -- TODO if(lastSendTime[messageType] < now + timeout)... lib.outgoing[#lib.outgoing + 1] = OutgoingPacket:New(messageType, data) if(not lib.isSending) then DoSend() else lib.hasMore = true end end function lib:PrepareNext() self.current = table.remove(self.sendQueue, 1) 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 local suppressedList = {} 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) lib.handlers = lib.handlers or {} local handlers = lib.handlers function lib:RegisterHandler(handlerType, handlerVersion) if handlers[handlerType] and handlers[handlerType].version >= handlerVersion then return false else handlers[handlerType] = handlers[handlerType] or {} handlers[handlerType].version = handlerVersion return handlers[handlerType] end end function lib:GetHandler(handlerType) return handlers[handlerType] end lib.MESSAGE_TYPE_RESOURCES = 1 lib.MESSAGE_TYPE_FTC_DPS = 2 lib.MESSAGE_TYPE_SYNC_TIMESTAMP = 3