renamed lua files

René Welbers [02-13-16 - 17:31]
renamed lua files
added libs to sync timestamp from grouplead
used sync timestamp offset to store data
Filename
PanicModeCombatAnalyzer.lua
PanicModeCombatAnalyzer.txt
PmCa.lua
lib/LibGPS/LICENSE
lib/LibGPS/LibGPS.lua
lib/LibGroupSocket/LibGroupSocket.lua
lib/LibGroupSocket/handlers/SyncTimeStampHandler.lua
lib/LibMapPing/LibMapPing.lua
lib/LibStub/LibStub.lua
diff --git a/PanicModeCombatAnalyzer.lua b/PanicModeCombatAnalyzer.lua
deleted file mode 100644
index 0900380..0000000
--- a/PanicModeCombatAnalyzer.lua
+++ /dev/null
@@ -1,264 +0,0 @@
-
-PanicModeCombatAnalyzer = PanicModeCombatAnalyzer or {}
-base64 = base64 or {}
-
-PanicModeCombatAnalyzer.name		  = 'PanicModeCombatAnalyzer'
-PanicModeCombatAnalyzer.version	      = '1.0.12'
-PanicModeCombatAnalyzer.versionDB	  = 4
-PanicModeCombatAnalyzer.loaded	      = false
-PanicModeCombatAnalyzer.author        = 'silentgecko, deevilius'
-PanicModeCombatAnalyzer.savedVarsName = 'PMCAVars'
-
-PanicModeCombatAnalyzer.variables = {
-    data = {},
-    metadata = {
-        server = 'EU',
-        language = 'de',
-    }
-}
-PanicModeCombatAnalyzer.tempVars = {
-    lastSave     = 0,
-    lastSaveTimeStamp = 0,
-    inFight      = false,
-    lastDps      = 0,
-    lastDpsCount = 0,
-    trialRunning = false,
-    trialId      = 0,
-}
-
----------Passing saved variables to the labels at initialize-------
-function PanicModeCombatAnalyzer.Initialize(_, addonName)
-    local self = PanicModeCombatAnalyzer
-
-    if addonName ~= self.name then return end
-
-    EVENT_MANAGER:UnregisterForEvent(self.name, EVENT_ADD_ON_LOADED)
-
-    self.savedVariables = ZO_SavedVars:New(self.savedVarsName, self.versionDB, nil, self.variables)
-
-    --save current server and language
-    self.savedVariables.metadata = {
-        server = GetWorldName(),
-        language = GetCVar('language.2')
-    }
-
-    self.cleanUp()
-
-    -- death/alive/rezz
-    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_DEAD, self.onDeath)
-    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_ALIVE, self.onAlive)
-    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_RESURRECT_REQUEST, self.onRezz)
-
-    --combat state
-    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_COMBAT_STATE, self.OnPlayerCombatState)
-
-    CALLBACK_MANAGER:RegisterCallback("FTC_NewDamage", self.addDamage)
-end
-
--- cleanup
-function PanicModeCombatAnalyzer.cleanUp()
-    local self      = PanicModeCombatAnalyzer
-    local savedVars = self.savedVariables.data
-    local currentTs = GetTimeStamp()
-    local limitTs   = currentTs - 604800 -- one week in seconds
-
-    for ts, data in pairs(savedVars) do
-        if ts <= limitTs then
-            self.savedVariables.data[ts] = nil
-        end
-    end
-end
-
--- fight status
-function PanicModeCombatAnalyzer.OnPlayerCombatState(_, inCombat)
-    local self       = PanicModeCombatAnalyzer
-    local timeStamp  = GetTimeStamp()
-    local data       = self.savedVariables.data or {}
-
-    data[timeStamp]          = data[timeStamp] or {}
-    data[timeStamp]['event'] = data[timeStamp]['event'] or {}
-    table.insert(data[timeStamp]['event'], {combat = inCombat})
-
-    -- add group and trial to event
-    data[timeStamp]['trial'] = self.getTrial()
-    data[timeStamp]['group'] = self.getGroupEnc()
-    self.savedVariables.data[timeStamp] = data[timeStamp]
-end
-
--- get current trial
-function PanicModeCombatAnalyzer.getTrial()
-    local self         = PanicModeCombatAnalyzer
-    local currentRaid  = GetCurrentParticipatingRaidId() or 0
-    return currentRaid
-end
-
--- get dmg from FTC
-function PanicModeCombatAnalyzer.addDamage(damage)
-    local self = PanicModeCombatAnalyzer
-
-    --only log outgoing stuff and greater than zero
-    if damage['out'] and damage['value'] > 0 and damage['target'] ~= '' then
-        local data         = self.savedVariables.data or {}
-        local lastSave     = self.tempVars.lastSave
-        local lastSaveTS   = self.tempVars.lastSaveTimeStamp
-        local currentTime  = GetGameTimeMilliseconds()
-        local timeStamp    = GetTimeStamp()
-        local lastSaveDiff = currentTime - lastSave
-
-        -- if the last saving data is 1 sek ago, make a new table
-        if lastSaveDiff >= 1000 then
-            lastSaveTS = timeStamp
-            self.tempVars.lastSaveTimeStamp = timeStamp
-            self.tempVars.lastSave          = currentTime
-        end
-        data[lastSaveTS]            = data[lastSaveTS] or {}
-        data[lastSaveTS]['damage']  = data[lastSaveTS]['damage'] or {}
-        data[lastSaveTS]['healing'] = data[lastSaveTS]['healing'] or {}
-
-        local damageData = {
-            abilityId = damage['abilityId'],
-            value     = damage['value'],
-            crit      = damage['crit'],
-            ms        = damage['ms'],
-        }
-
-        if damage['heal'] then
-            --filter unwanted stuff
-            if damage['result'] == ACTION_RESULT_HEAL or damage['result'] == ACTION_RESULT_CRITICAL_HEAL or damage['result'] == ACTION_RESULT_HOT_TICK or damage['result'] == ACTION_RESULT_HOT_TICK_CRITICAL then
-                local currentData = data[lastSaveTS]['healing'][damage['target']] or {}
-                table.insert(currentData, damageData)
-                if #currentData ~= 0 then
-                    data[lastSaveTS]['healing'][damage['target']] = currentData
-                end
-            end
-        else
-            --filter unwanted stuff
-            if damage['result'] == ACTION_RESULT_DAMAGE or damage['result'] == ACTION_RESULT_CRITICAL_DAMAGE or damage['result'] == ACTION_RESULT_DOT_TICK or damage['result'] == ACTION_RESULT_DOT_TICK_CRITICAL then
-                local currentData = data[lastSaveTS]['damage'][damage['target']] or {}
-                table.insert(currentData, damageData)
-                if #currentData ~= 0 then
-                    data[lastSaveTS]['damage'][damage['target']] = currentData
-                end
-            end
-        end
-
-        -- only store, when we have damage or healing
-        if #data[lastSaveTS]['damage'] ~= 0 or #data[lastSaveTS]['healing'] ~= 0 then
-            data[lastSaveTS]['trial'] = self.getTrial()
-            data[lastSaveTS]['group'] = self.getGroupEnc()
-
-            self.savedVariables.data[lastSaveTS] = data[lastSaveTS]
-        end
-    end
-end
-
--- player death / revive
-function PanicModeCombatAnalyzer.onDeath(event)
-    local self       = PanicModeCombatAnalyzer
-    local timeStamp  = GetTimeStamp()
-    local data       = self.savedVariables.data or {}
-    data[timeStamp] = data[timeStamp] or {}
-    if (event == EVENT_PLAYER_DEAD) then -- player died
-        data[timeStamp]['event'] = data[timeStamp]['event'] or {}
-        table.insert(data[timeStamp]['event'], {death = true})
-    else -- player lives again
-        data[timeStamp]['event'] = data[timeStamp]['event'] or {}
-        table.insert(data[timeStamp]['event'], {revive = true})
-    end
-
-    -- add group and trial to event
-    data[timeStamp]['trial'] = self.getTrial()
-    data[timeStamp]['group'] = self.getGroupEnc()
-
-    self.savedVariables.data[timeStamp] = data[timeStamp]
-end
-
--- player alive
-function PanicModeCombatAnalyzer.onAlive(event)
-    local self       = PanicModeCombatAnalyzer
-    local timeStamp  = GetTimeStamp()
-    local data       = self.savedVariables.data or {}
-
-    data[timeStamp]          = data[timeStamp] or {}
-    data[timeStamp]['event'] = data[timeStamp]['event'] or {}
-    table.insert(data[timeStamp]['event'], {alive = true})
-
-    -- add group and trial to event
-    data[timeStamp]['trial'] = self.getTrial()
-    data[timeStamp]['group'] = self.getGroupEnc()
-
-    self.savedVariables.data[timeStamp] = data[timeStamp]
-end
-
--- player rezz
-function PanicModeCombatAnalyzer.onRezz(event, requesterCharacterName, timeLeftToAccept)
-    local self       = PanicModeCombatAnalyzer
-    local timeStamp  = GetTimeStamp()
-    local data       = self.savedVariables.data or {}
-
-    data[timeStamp]          = data[timeStamp] or {}
-    data[timeStamp]['event'] = data[timeStamp]['event'] or {}
-
-    table.insert(data[timeStamp]['event'], {rezz = zo_strformat("<<!aC:1>>", requesterCharacterName)})
-
-    -- add group and trial to event
-    data[timeStamp]['trial'] = self.getTrial()
-    data[timeStamp]['group'] = self.getGroupEnc()
-
-    self.savedVariables.data[timeStamp] = data[timeStamp]
-end
-
-
--- group change
-function PanicModeCombatAnalyzer.getGroupEnc()
-    local self       = PanicModeCombatAnalyzer
-    local group      = self.getGroup()
-    local groupEnc   = ''
-    local groupStr   = ''
-
-    --sort group
-    local groupSort = function(char1, char2) return char1.name < char2.name end
-    table.sort(group, groupSort)
-
-    -- encode group
-    for key,charName in pairs(group) do --actualcode
-        groupStr = groupStr .. "," .. charName.name
-    end
-    groupStr = string.sub(groupStr, 2);
-
-    groupEnc = base64.enc(groupStr)
-
-    return groupEnc
-end
-
--- get group
-function PanicModeCombatAnalyzer.getGroup()
-    local self      = PanicModeCombatAnalyzer
-    local groupSize = GetGroupSize()
-    local group     = {}
-
-    -- iterate over group
-    if groupSize > 0 then
-        for i = 1, groupSize do
-            local unitTag = GetGroupUnitTagByIndex(i)
-            if (DoesUnitExist(unitTag)) then
-                local charName = zo_strformat("<<!aC:1>>", GetUnitName(unitTag))
-                local charTable = {
-                    name = charName
-                }
-                table.insert(group, charTable)
-            end
-        end
-    end
-
-    return group
-end
-
--- debug func
-function PanicModeCombatAnalyzer.debug(message, data)
-    d('PM CA Debug:')
-    d(message, data)
-end
-
----------Events-------
-EVENT_MANAGER:RegisterForEvent(PanicModeCombatAnalyzer.name, EVENT_ADD_ON_LOADED, PanicModeCombatAnalyzer.Initialize)
diff --git a/PanicModeCombatAnalyzer.txt b/PanicModeCombatAnalyzer.txt
index e306bad..dba2f5c 100644
--- a/PanicModeCombatAnalyzer.txt
+++ b/PanicModeCombatAnalyzer.txt
@@ -1,11 +1,17 @@
 ## Title: |cEFEBBEPanic Mode Combat Analyzer|r
 ## Description: Saves your dps during fight, based on FTC DPS Meter.
 ## Author: |c009ad6silentgecko|r, deevilius
-## Version: 1.0.12
+## Version: 1.1.1
 ## APIVersion: 100014
 ## SavedVariables: PMCAVars
 ## DependsOn: FoundryTacticalCombat

+lib/LibStub/LibStub.lua
+lib/LibMapPing/LibMapPing.lua
+lib/LibGPS/LibGPS.lua
+lib/LibGroupSocket/LibGroupSocket.lua
+lib/LibGroupSocket/handlers/SyncTimeStampHandler.lua
+
 ext/base64.lua
 OverwriteFTC.lua
-PanicModeCombatAnalyzer.lua
\ No newline at end of file
+PmCa.lua
\ No newline at end of file
diff --git a/PmCa.lua b/PmCa.lua
new file mode 100644
index 0000000..4698276
--- /dev/null
+++ b/PmCa.lua
@@ -0,0 +1,279 @@
+
+PmCa = PmCa or {}
+base64 = base64 or {}
+
+PmCa.name		   = 'Panic Moce Combat Analyzer'
+PmCa.version	   = '1.1.1'
+PmCa.versionDB	   = 4
+PmCa.loaded	       = false
+PmCa.author        = 'silentgecko, deevilius'
+PmCa.savedVarsName = 'PMCAVars'
+
+PmCa.variables = {
+    data = {},
+    metadata = {
+        server = 'EU',
+        language = 'de',
+    }
+}
+PmCa.tempVars = {
+    lastSave     = 0,
+    lastSaveTimeStamp = 0,
+    inFight      = false,
+    lastDps      = 0,
+    lastDpsCount = 0,
+    trialRunning = false,
+    trialId      = 0,
+    timeStampOffsetToLead = 0,
+}
+
+---------Passing saved variables to the labels at initialize-------
+function PmCa.Initialize(_, addonName)
+    local self = PmCa
+
+    if addonName ~= self.name then return end
+
+    EVENT_MANAGER:UnregisterForEvent(self.name, EVENT_ADD_ON_LOADED)
+
+    self.savedVariables = ZO_SavedVars:New(self.savedVarsName, self.versionDB, nil, self.variables)
+
+    --save current server and language
+    self.savedVariables.metadata = {
+        server = GetWorldName(),
+        language = GetCVar('language.2')
+    }
+
+    self.cleanUp()
+
+    -- death/alive/rezz
+    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_DEAD, self.onDeath)
+    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_ALIVE, self.onAlive)
+    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_RESURRECT_REQUEST, self.onRezz)
+
+    --combat state
+    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_COMBAT_STATE, self.OnPlayerCombatState)
+
+    CALLBACK_MANAGER:RegisterCallback("FTC_NewDamage", self.addDamage)
+
+end
+
+function PmCa:getOffset(offset, finished)
+    local self = PmCa
+    self.debug('PmCa Synced Offset from lead:', offset)
+    self.tempVars.timeStampOffsetToLead = offset
+end
+
+-- cleanup
+function PmCa.cleanUp()
+    local self      = PmCa
+    local savedVars = self.savedVariables.data
+    local currentTs = GetTimeStamp()
+    local limitTs   = currentTs - 604800 -- one week in seconds
+
+    for ts, data in pairs(savedVars) do
+        if ts <= limitTs then
+            self.savedVariables.data[ts] = nil
+        end
+    end
+end
+
+-- fight status
+function PmCa.OnPlayerCombatState(_, inCombat)
+    local self       = PmCa
+    local timeStamp  = GetTimeStamp() + self.tempVars.timeStampOffsetToLead
+    local data       = self.savedVariables.data or {}
+
+    data[timeStamp]          = data[timeStamp] or {}
+    data[timeStamp]['event'] = data[timeStamp]['event'] or {}
+    table.insert(data[timeStamp]['event'], {combat = inCombat})
+
+    -- add group and trial to event
+    data[timeStamp]['trial'] = self.getTrial()
+    data[timeStamp]['group'] = self.getGroupEnc()
+    self.savedVariables.data[timeStamp] = data[timeStamp]
+end
+
+-- get current trial
+function PmCa.getTrial()
+    local self         = PmCa
+    local currentRaid  = GetCurrentParticipatingRaidId() or 0
+    return currentRaid
+end
+
+-- get dmg from FTC
+function PmCa.addDamage(damage)
+    local self = PmCa
+
+    --only log outgoing stuff and greater than zero
+    if damage['out'] and damage['value'] > 0 and damage['target'] ~= '' then
+        local data         = self.savedVariables.data or {}
+        local lastSave     = self.tempVars.lastSave
+        local lastSaveTS   = self.tempVars.lastSaveTimeStamp
+        local currentTime  = GetGameTimeMilliseconds()
+        local timeStamp    = GetTimeStamp() + self.tempVars.timeStampOffsetToLead
+        local lastSaveDiff = currentTime - lastSave
+
+        -- if the last saving data is 1 sek ago, make a new table
+        if lastSaveDiff >= 1000 then
+            lastSaveTS = timeStamp
+            self.tempVars.lastSaveTimeStamp = timeStamp
+            self.tempVars.lastSave          = currentTime
+        end
+        data[lastSaveTS]            = data[lastSaveTS] or {}
+        data[lastSaveTS]['damage']  = data[lastSaveTS]['damage'] or {}
+        data[lastSaveTS]['healing'] = data[lastSaveTS]['healing'] or {}
+
+        local damageData = {
+            abilityId = damage['abilityId'],
+            value     = damage['value'],
+            crit      = damage['crit'],
+            ms        = damage['ms'],
+        }
+
+        if damage['heal'] then
+            --filter unwanted stuff
+            if damage['result'] == ACTION_RESULT_HEAL or damage['result'] == ACTION_RESULT_CRITICAL_HEAL or damage['result'] == ACTION_RESULT_HOT_TICK or damage['result'] == ACTION_RESULT_HOT_TICK_CRITICAL then
+                local currentData = data[lastSaveTS]['healing'][damage['target']] or {}
+                table.insert(currentData, damageData)
+                if #currentData ~= 0 then
+                    data[lastSaveTS]['healing'][damage['target']] = currentData
+                end
+            end
+        else
+            --filter unwanted stuff
+            if damage['result'] == ACTION_RESULT_DAMAGE or damage['result'] == ACTION_RESULT_CRITICAL_DAMAGE or damage['result'] == ACTION_RESULT_DOT_TICK or damage['result'] == ACTION_RESULT_DOT_TICK_CRITICAL then
+                local currentData = data[lastSaveTS]['damage'][damage['target']] or {}
+                table.insert(currentData, damageData)
+                if #currentData ~= 0 then
+                    data[lastSaveTS]['damage'][damage['target']] = currentData
+                end
+            end
+        end
+
+        -- only store, when we have damage or healing
+        if #data[lastSaveTS]['damage'] ~= 0 or #data[lastSaveTS]['healing'] ~= 0 then
+            data[lastSaveTS]['trial'] = self.getTrial()
+            data[lastSaveTS]['group'] = self.getGroupEnc()
+
+            self.savedVariables.data[lastSaveTS] = data[lastSaveTS]
+        end
+    end
+end
+
+-- player death / revive
+function PmCa.onDeath(event)
+    local self       = PmCa
+    local timeStamp  = GetTimeStamp() + self.tempVars.timeStampOffsetToLead
+    local data       = self.savedVariables.data or {}
+    data[timeStamp] = data[timeStamp] or {}
+    if (event == EVENT_PLAYER_DEAD) then -- player died
+        data[timeStamp]['event'] = data[timeStamp]['event'] or {}
+        table.insert(data[timeStamp]['event'], {death = true})
+    else -- player lives again
+        data[timeStamp]['event'] = data[timeStamp]['event'] or {}
+        table.insert(data[timeStamp]['event'], {revive = true})
+    end
+
+    -- add group and trial to event
+    data[timeStamp]['trial'] = self.getTrial()
+    data[timeStamp]['group'] = self.getGroupEnc()
+
+    self.savedVariables.data[timeStamp] = data[timeStamp]
+end
+
+-- player alive
+function PmCa.onAlive(event)
+    local self       = PmCa
+    local timeStamp  = GetTimeStamp() + self.tempVars.timeStampOffsetToLead
+    local data       = self.savedVariables.data or {}
+
+    data[timeStamp]          = data[timeStamp] or {}
+    data[timeStamp]['event'] = data[timeStamp]['event'] or {}
+    table.insert(data[timeStamp]['event'], {alive = true})
+
+    -- add group and trial to event
+    data[timeStamp]['trial'] = self.getTrial()
+    data[timeStamp]['group'] = self.getGroupEnc()
+
+    self.savedVariables.data[timeStamp] = data[timeStamp]
+end
+
+-- player rezz
+function PmCa.onRezz(event, requesterCharacterName, timeLeftToAccept)
+    local self       = PmCa
+    local timeStamp  = GetTimeStamp() + self.tempVars.timeStampOffsetToLead
+    local data       = self.savedVariables.data or {}
+
+    data[timeStamp]          = data[timeStamp] or {}
+    data[timeStamp]['event'] = data[timeStamp]['event'] or {}
+
+    table.insert(data[timeStamp]['event'], {rezz = zo_strformat("<<!aC:1>>", requesterCharacterName)})
+
+    -- add group and trial to event
+    data[timeStamp]['trial'] = self.getTrial()
+    data[timeStamp]['group'] = self.getGroupEnc()
+
+    self.savedVariables.data[timeStamp] = data[timeStamp]
+end
+
+
+-- group change
+function PmCa.getGroupEnc()
+    local self       = PmCa
+    local group      = self.getGroup()
+    local groupEnc   = ''
+    local groupStr   = ''
+
+    --sort group
+    local groupSort = function(char1, char2) return char1.name < char2.name end
+    table.sort(group, groupSort)
+
+    -- encode group
+    for key,charName in pairs(group) do --actualcode
+        groupStr = groupStr .. "," .. charName.name
+    end
+    groupStr = string.sub(groupStr, 2);
+
+    groupEnc = base64.enc(groupStr)
+
+    return groupEnc
+end
+
+-- get group
+function PmCa.getGroup()
+    local self      = PmCa
+    local groupSize = GetGroupSize()
+    local group     = {}
+
+    -- iterate over group
+    if groupSize > 0 then
+        for i = 1, groupSize do
+            local unitTag = GetGroupUnitTagByIndex(i)
+            if (DoesUnitExist(unitTag)) then
+                local charName = zo_strformat("<<!aC:1>>", GetUnitName(unitTag))
+                local charTable = {
+                    name = charName
+                }
+                table.insert(group, charTable)
+            end
+        end
+    end
+
+    return group
+end
+
+-- debug func
+function PmCa.debug(message, data)
+    d('PM CA Debug:')
+    d(message, data)
+end
+
+---------Callbacks-----
+local LGS = LibStub("LibGroupSocket")
+local SyncTimeStampHandler = LGS:GetHandler(LGS.MESSAGE_TYPE_SYNC_TIMESTAMP)
+SyncTimeStampHandler:RegisterForFinishedSyncOffset(function(offset, syncFinished)
+    PmCa:getOffset(offset, syncFinished)
+end)
+
+---------Events-------
+EVENT_MANAGER:RegisterForEvent(PmCa.name, EVENT_ADD_ON_LOADED, PmCa.Initialize)
diff --git a/lib/LibGPS/LICENSE b/lib/LibGPS/LICENSE
new file mode 100644
index 0000000..d8164f9
--- /dev/null
+++ b/lib/LibGPS/LICENSE
@@ -0,0 +1,201 @@
+               The Artistic License 2.0
+
+           Copyright (c) 2015 sirinsidiator
+
+     Everyone is permitted to copy and distribute verbatim copies
+      of this license document, but changing it is not allowed.
+
+Preamble
+
+This license establishes the terms under which a given free software
+Package may be copied, modified, distributed, and/or redistributed.
+The intent is that the Copyright Holder maintains some artistic
+control over the development of that Package while still keeping the
+Package available as open source and free software.
+
+You are always permitted to make arrangements wholly outside of this
+license directly with the Copyright Holder of a given Package.  If the
+terms of this license do not permit the full use that you propose to
+make of the Package, you should contact the Copyright Holder and seek
+a different licensing arrangement.
+
+Definitions
+
+    "Copyright Holder" means the individual(s) or organization(s)
+    named in the copyright notice for the entire Package.
+
+    "Contributor" means any party that has contributed code or other
+    material to the Package, in accordance with the Copyright Holder's
+    procedures.
+
+    "You" and "your" means any person who would like to copy,
+    distribute, or modify the Package.
+
+    "Package" means the collection of files distributed by the
+    Copyright Holder, and derivatives of that collection and/or of
+    those files. A given Package may consist of either the Standard
+    Version, or a Modified Version.
+
+    "Distribute" means providing a copy of the Package or making it
+    accessible to anyone else, or in the case of a company or
+    organization, to others outside of your company or organization.
+
+    "Distributor Fee" means any fee that you charge for Distributing
+    this Package or providing support for this Package to another
+    party.  It does not mean licensing fees.
+
+    "Standard Version" refers to the Package if it has not been
+    modified, or has been modified only in ways explicitly requested
+    by the Copyright Holder.
+
+    "Modified Version" means the Package, if it has been changed, and
+    such changes were not explicitly requested by the Copyright
+    Holder.
+
+    "Original License" means this Artistic License as Distributed with
+    the Standard Version of the Package, in its current version or as
+    it may be modified by The Perl Foundation in the future.
+
+    "Source" form means the source code, documentation source, and
+    configuration files for the Package.
+
+    "Compiled" form means the compiled bytecode, object code, binary,
+    or any other form resulting from mechanical transformation or
+    translation of the Source form.
+
+
+Permission for Use and Modification Without Distribution
+
+(1)  You are permitted to use the Standard Version and create and use
+Modified Versions for any purpose without restriction, provided that
+you do not Distribute the Modified Version.
+
+
+Permissions for Redistribution of the Standard Version
+
+(2)  You may Distribute verbatim copies of the Source form of the
+Standard Version of this Package in any medium without restriction,
+either gratis or for a Distributor Fee, provided that you duplicate
+all of the original copyright notices and associated disclaimers.  At
+your discretion, such verbatim copies may or may not include a
+Compiled form of the Package.
+
+(3)  You may apply any bug fixes, portability changes, and other
+modifications made available from the Copyright Holder.  The resulting
+Package will still be considered the Standard Version, and as such
+will be subject to the Original License.
+
+
+Distribution of Modified Versions of the Package as Source
+
+(4)  You may Distribute your Modified Version as Source (either gratis
+or for a Distributor Fee, and with or without a Compiled form of the
+Modified Version) provided that you clearly document how it differs
+from the Standard Version, including, but not limited to, documenting
+any non-standard features, executables, or modules, and provided that
+you do at least ONE of the following:
+
+    (a)  make the Modified Version available to the Copyright Holder
+    of the Standard Version, under the Original License, so that the
+    Copyright Holder may include your modifications in the Standard
+    Version.
+
+    (b)  ensure that installation of your Modified Version does not
+    prevent the user installing or running the Standard Version. In
+    addition, the Modified Version must bear a name that is different
+    from the name of the Standard Version.
+
+    (c)  allow anyone who receives a copy of the Modified Version to
+    make the Source form of the Modified Version available to others
+    under
+
+    (i)  the Original License or
+
+    (ii)  a license that permits the licensee to freely copy,
+    modify and redistribute the Modified Version using the same
+    licensing terms that apply to the copy that the licensee
+    received, and requires that the Source form of the Modified
+    Version, and of any works derived from it, be made freely
+    available in that license fees are prohibited but Distributor
+    Fees are allowed.
+
+
+Distribution of Compiled Forms of the Standard Version
+or Modified Versions without the Source
+
+(5)  You may Distribute Compiled forms of the Standard Version without
+the Source, provided that you include complete instructions on how to
+get the Source of the Standard Version.  Such instructions must be
+valid at the time of your distribution.  If these instructions, at any
+time while you are carrying out such distribution, become invalid, you
+must provide new instructions on demand or cease further distribution.
+If you provide valid instructions or cease distribution within thirty
+days after you become aware that the instructions are invalid, then
+you do not forfeit any of your rights under this license.
+
+(6)  You may Distribute a Modified Version in Compiled form without
+the Source, provided that you comply with Section 4 with respect to
+the Source of the Modified Version.
+
+
+Aggregating or Linking the Package
+
+(7)  You may aggregate the Package (either the Standard Version or
+Modified Version) with other packages and Distribute the resulting
+aggregation provided that you do not charge a licensing fee for the
+Package.  Distributor Fees are permitted, and licensing fees for other
+components in the aggregation are permitted. The terms of this license
+apply to the use and Distribution of the Standard or Modified Versions
+as included in the aggregation.
+
+(8) You are permitted to link Modified and Standard Versions with
+other works, to embed the Package in a larger work of your own, or to
+build stand-alone binary or bytecode versions of applications that
+include the Package, and Distribute the result without restriction,
+provided the result does not expose a direct interface to the Package.
+
+
+Items That are Not Considered Part of a Modified Version
+
+(9) Works (including, but not limited to, modules and scripts) that
+merely extend or make use of the Package, do not, by themselves, cause
+the Package to be a Modified Version.  In addition, such works are not
+considered parts of the Package itself, and are not subject to the
+terms of this license.
+
+
+General Provisions
+
+(10)  Any use, modification, and distribution of the Standard or
+Modified Versions is governed by this Artistic License. By using,
+modifying or distributing the Package, you accept this license. Do not
+use, modify, or distribute the Package, if you do not accept this
+license.
+
+(11)  If your Modified Version has been derived from a Modified
+Version made by someone other than you, you are nevertheless required
+to ensure that your Modified Version complies with the requirements of
+this license.
+
+(12)  This license does not grant you the right to use any trademark,
+service mark, tradename, or logo of the Copyright Holder.
+
+(13)  This license includes the non-exclusive, worldwide,
+free-of-charge patent license to make, have made, use, offer to sell,
+sell, import and otherwise transfer the Package with respect to any
+patent claims licensable by the Copyright Holder that are necessarily
+infringed by the Package. If you institute patent litigation
+(including a cross-claim or counterclaim) against any party alleging
+that the Package constitutes direct or contributory patent
+infringement, then this Artistic License to you shall terminate on the
+date that such litigation is filed.
+
+(14)  Disclaimer of Warranty:
+THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS
+IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
+NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL
+LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LibGPS/LibGPS.lua b/lib/LibGPS/LibGPS.lua
new file mode 100644
index 0000000..08dfd18
--- /dev/null
+++ b/lib/LibGPS/LibGPS.lua
@@ -0,0 +1,706 @@
+-- LibGPS2 & its files © sirinsidiator                          --
+-- Distributed under The Artistic License 2.0 (see LICENSE)     --
+------------------------------------------------------------------
+
+local LIB_NAME = "LibGPS2"
+local lib = LibStub:NewLibrary(LIB_NAME, 999) -- only for test purposes. releases will get a smaller number
+
+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_NAME))
+end
+
+local DUMMY_PIN_TYPE = LIB_NAME .. "DummyPin"
+local LIB_IDENTIFIER_FINALIZE = LIB_NAME .. "_Finalize"
+lib.LIB_EVENT_STATE_CHANGED = "OnLibGPS2MeasurementChanged"
+
+local LOG_WARNING = "Warning"
+local LOG_NOTICE = "Notice"
+local LOG_DEBUG = "Debug"
+
+local POSITION_MIN = 0.085
+local POSITION_MAX = 0.915
+
+local TAMRIEL_MAP_INDEX, COLDHARBOUR_MAP_INDEX
+if(GetAPIVersion() == 100013) then -- TODO: remove
+	function DoesCurrentMapMatchMapForPlayerLocation()
+		return GetPlayerLocationName() == GetMapName()
+	end
+	TAMRIEL_MAP_INDEX = 1
+	COLDHARBOUR_MAP_INDEX = 23
+else
+	TAMRIEL_MAP_INDEX = GetZoneIndex(2)
+	COLDHARBOUR_MAP_INDEX = GetZoneIndex(131)
+end
+
+--lib.debugMode = 1 -- TODO
+lib.mapMeasurements = lib.mapMeasurements or {}
+local mapMeasurements = lib.mapMeasurements
+lib.mapStack = lib.mapStack or {}
+local mapStack = lib.mapStack
+
+local MAP_PIN_TYPE_PLAYER_WAYPOINT = MAP_PIN_TYPE_PLAYER_WAYPOINT
+local currentWaypointX, currentWaypointY, currentWaypointMapId = 0, 0, nil
+local needWaypointRestore = false
+local orgSetMapToMapListIndex = nil
+local orgSetMapToQuestCondition = nil
+local orgSetMapToPlayerLocation = nil
+local orgSetMapToQuestZone = nil
+local orgSetMapFloor = nil
+local orgProcessMapClick = nil
+local measuring = false
+
+SLASH_COMMANDS["/libgpsdebug"] = function(value)
+	lib.debugMode = (tonumber(value) == 1)
+	df("[%s] debug mode %s", LIB_NAME, lib.debugMode and "enabled" or "disabled")
+end
+
+local function LogMessage(type, message, ...)
+	if not lib.debugMode then return end
+	df("[%s] %s: %s", LIB_NAME, type, zo_strjoin(" ", message, ...))
+end
+
+local function GetAddon()
+	local addOn
+	local function errornous() addOn = 'a' + 1 end
+	local function errorHandler(err) addOn = string.match(err, "'GetAddon'.+user:/AddOns/(.-:.-):") end
+	xpcall(errornous, errorHandler)
+	return addOn
+end
+
+local function FinalizeMeasurement()
+	EVENT_MANAGER:UnregisterForUpdate(LIB_IDENTIFIER_FINALIZE)
+	LMP:UnsuppressPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
+	if needWaypointRestore then
+		LogMessage(LOG_DEBUG, "Update waypoint pin", LMP:GetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT))
+		LMP:RefreshMapPin(MAP_PIN_TYPE_PLAYER_WAYPOINT)
+		needWaypointRestore = false
+	end
+	measuring = false
+	CALLBACK_MANAGER:FireCallbacks(lib.LIB_EVENT_STATE_CHANGED, measuring)
+end
+
+local function HandlePingEvent(pingType, pingTag, x, y, isPingOwner)
+	if(not isPingOwner or pingType ~= MAP_PIN_TYPE_PLAYER_WAYPOINT or not measuring) then return end
+	-- we delay our handler until all events have been fired and so that other addons can react to it first in case they use IsMeasuring
+	EVENT_MANAGER:UnregisterForUpdate(LIB_IDENTIFIER_FINALIZE)
+	EVENT_MANAGER:RegisterForUpdate(LIB_IDENTIFIER_FINALIZE, 0, FinalizeMeasurement)
+end
+
+local function GetPlayerPosition()
+	return GetMapPlayerPosition("player")
+end
+
+local function GetPlayerWaypoint()
+	return LMP:GetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
+end
+
+local function SetMeasurementWaypoint(x, y)
+	-- this waypoint stays invisible for others
+	LMP:SuppressPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
+	LMP:SetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_TYPE_LOCATION_CENTERED, x, y)
+end
+
+local function SetPlayerWaypoint(x, y)
+	LMP:SetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_TYPE_LOCATION_CENTERED, x, y)
+end
+
+local function RemovePlayerWaypoint()
+	LMP:RemoveMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
+end
+
+local function GetReferencePoints()
+	local x1, y1 = GetPlayerPosition()
+	local x2, y2 = GetPlayerWaypoint()
+	return x1, y1, x2, y2
+end
+
+local function IsMapMeasured(mapId)
+	return (mapMeasurements[mapId or GetMapTileTexture()] ~= nil)
+end
+
+local function StoreTamrielMapMeasurements()
+	-- no need to actually measure the world map
+	if (orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX) ~= SET_MAP_RESULT_FAILED) then
+		mapMeasurements[GetMapTileTexture()] = {
+			scaleX = 1,
+			scaleY = 1,
+			offsetX = 0,
+			offsetY = 0,
+			mapIndex = TAMRIEL_MAP_INDEX
+		}
+		return true
+	end
+
+	return false
+end
+
+local function CalculateMeasurements(mapId, localX, localY)
+	-- select the map corner farthest from the player position
+	local wpX, wpY = POSITION_MIN, POSITION_MIN
+	-- on some maps we cannot set the waypoint to the map border (e.g. Aurdion)
+	-- Opposite corner:
+	if (localX < 0.5) then wpX = POSITION_MAX end
+	if (localY < 0.5) then wpY = POSITION_MAX end
+
+	SetMeasurementWaypoint(wpX, wpY)
+
+	-- add local points to seen maps
+	local measurementPositions = {}
+	table.insert(measurementPositions, { mapId = mapId, pX = localX, pY = localY, wpX = wpX, wpY = wpY })
+
+	-- switch to zone map in order to get the mapIndex for the current location
+	local x1, y1, x2, y2
+	while not(GetMapType() == MAPTYPE_ZONE and GetMapContentType() ~= MAP_CONTENT_DUNGEON) do
+		if (MapZoomOut() ~= SET_MAP_RESULT_MAP_CHANGED) then break end
+		-- collect measurements for all maps we come through on our way to the zone map
+		x1, y1, x2, y2 = GetReferencePoints()
+		table.insert(measurementPositions, { mapId = GetMapTileTexture(), pX = x1, pY = y1, wpX = x2, wpY = y2 })
+	end
+
+	-- some non-zone maps like Eyevea zoom directly to the Tamriel map
+	local mapIndex = GetCurrentMapIndex() or TAMRIEL_MAP_INDEX
+
+	-- switch to world map so we can calculate the global map scale and offset
+	if orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX) == SET_MAP_RESULT_FAILED then
+		-- failed to switch to the world map
+		LogMessage(LOG_NOTICE, "Could not switch to world map")
+		return
+	end
+
+	-- get the two reference points on the world map
+	x1, y1, x2, y2 = GetReferencePoints()
+
+	-- calculate scale and offset for all maps that we saw
+	local scaleX, scaleY, offsetX, offsetY
+	for _, m in ipairs(measurementPositions) do
+		if (mapMeasurements[m.mapId]) then break end -- we always go up in the hierarchy so we can stop once a measurement already exists
+		LogMessage(LOG_DEBUG, "Store map measurement for", m.mapId:sub(10, -7))
+		scaleX = (x2 - x1) / (m.wpX - m.pX)
+		scaleY = (y2 - y1) / (m.wpY - m.pY)
+		offsetX = x1 - m.pX * scaleX
+		offsetY = y1 - m.pY * scaleY
+		if (math.abs(scaleX - scaleY) > 1e-3) then
+			LogMessage(LOG_WARNING, "Current map measurement might be wrong", m.mapId:sub(10, -7), mapIndex, m.pX, m.pY, m.wpX, m.wpY, x1, y1, x2, y2, offsetX, offsetY, scaleX, scaleY)
+		end
+
+		-- store measurements
+		mapMeasurements[m.mapId] = {
+			scaleX = scaleX,
+			scaleY = scaleY,
+			offsetX = offsetX,
+			offsetY = offsetY,
+			mapIndex = mapIndex
+		}
+	end
+	return mapIndex
+end
+
+local function StoreCurrentWaypoint()
+	currentWaypointX, currentWaypointY = GetPlayerWaypoint()
+	currentWaypointMapId = GetMapTileTexture()
+end
+
+local function ClearCurrentWaypoint()
+	currentWaypointX, currentWaypointY = 0, 0, nil
+end
+
+local function GetColdharbourMeasurement()
+	-- switch to the Coldharbour map
+	orgSetMapToMapListIndex(COLDHARBOUR_MAP_INDEX)
+	local coldharbourId = GetMapTileTexture()
+	if(not IsMapMeasured(coldharbourId)) then
+		-- calculate the measurements of Coldharbour without worrying about the waypoint
+		local mapIndex = CalculateMeasurements(coldharbourId, GetPlayerPosition())
+		if (mapIndex ~= COLDHARBOUR_MAP_INDEX) then
+			LogMessage(LOG_WARNING, "CalculateMeasurements returned different index while measuring Coldharbour map. expected:", COLDHARBOUR_MAP_INDEX, "actual:", mapIndex)
+			if(not IsMapMeasured(coldharbourId)) then
+				LogMessage(LOG_WARNING, "Failed to measure Coldharbour map.")
+				return
+			end
+		end
+	end
+	return mapMeasurements[coldharbourId]
+end
+
+local function RestoreCurrentWaypoint()
+	if(not currentWaypointMapId) then
+		LogMessage(LOG_DEBUG, "Called RestoreCurrentWaypoint without calling StoreCurrentWaypoint.")
+		return
+	end
+
+	local wasSet = false
+	if (currentWaypointX ~= 0 or currentWaypointY ~= 0) then
+		-- calculate waypoint position on the worldmap
+		local measurements = mapMeasurements[currentWaypointMapId]
+		local x = currentWaypointX * measurements.scaleX + measurements.offsetX
+		local y = currentWaypointY * measurements.scaleY + measurements.offsetY
+
+		if (x > 0 and x < 1 and y > 0 and y < 1) then
+			-- if it is inside the Tamriel map we set it there
+			if(orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX) ~= SET_MAP_RESULT_FAILED) then
+				SetPlayerWaypoint(x, y)
+				wasSet = true
+			else
+				LogMessage(LOG_DEBUG, "Cannot reset waypoint because switching to the world map failed")
+			end
+		else -- when the waypoint is outside of the Tamriel map check if it is in Coldharbour
+			measurements = GetColdharbourMeasurement()
+			if(measurements) then
+				-- calculate waypoint coodinates within coldharbour
+				x = (x - measurements.offsetX) / measurements.scaleX
+				y = (y - measurements.offsetY) / measurements.scaleY
+				if not(x < 0 or x > 1 or y < 0 or y > 1) then
+					if(orgSetMapToMapListIndex(COLDHARBOUR_MAP_INDEX) ~= SET_MAP_RESULT_FAILED) then
+						SetPlayerWaypoint(x, y)
+						wasSet = true
+					else
+						LogMessage(LOG_DEBUG, "Cannot reset waypoint because switching to the Coldharbour map failed")
+					end
+				else
+					LogMessage(LOG_DEBUG, "Cannot reset waypoint because it was outside of our reach")
+				end
+			else
+				LogMessage(LOG_DEBUG, "Cannot reset waypoint because Coldharbour measurements are unavailable")
+			end
+		end
+	end
+
+	if(wasSet) then
+		LogMessage(LOG_DEBUG, "Waypoint was restored, request pin update")
+		needWaypointRestore = true -- we need to update the pin on the worldmap afterwards
+	else
+		RemovePlayerWaypoint()
+	end
+	ClearCurrentWaypoint()
+end
+
+local function InterceptMapPinManager()
+	if (lib.mapPinManager) then return end
+	ZO_WorldMap_AddCustomPin(DUMMY_PIN_TYPE, function(pinManager)
+		lib.mapPinManager = pinManager
+		ZO_WorldMap_SetCustomPinEnabled(_G[DUMMY_PIN_TYPE], false)
+	end , nil, { level = 0, size = 0, texture = "" })
+	ZO_WorldMap_SetCustomPinEnabled(_G[DUMMY_PIN_TYPE], true)
+	ZO_WorldMap_RefreshCustomPinsOfType(_G[DUMMY_PIN_TYPE])
+end
+
+local function HookSetMapToQuestCondition()
+	orgSetMapToQuestCondition = SetMapToQuestCondition
+	local function NewSetMapToQuestCondition(...)
+		local result = orgSetMapToQuestCondition(...)
+		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
+			LogMessage(LOG_DEBUG, "SetMapToQuestCondition")
+
+			local success, mapResult = lib:CalculateMapMeasurements()
+			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
+				result = mapResult
+			end
+		end
+		-- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
+		return result
+	end
+	SetMapToQuestCondition = NewSetMapToQuestCondition
+end
+
+local function HookSetMapToQuestZone()
+	orgSetMapToQuestZone = SetMapToQuestZone
+	local function NewSetMapToQuestZone(...)
+		local result = orgSetMapToQuestZone(...)
+		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
+			LogMessage(LOG_DEBUG, "SetMapToQuestZone")
+
+			local success, mapResult = lib:CalculateMapMeasurements()
+			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
+				result = mapResult
+			end
+		end
+		-- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
+		return result
+	end
+	SetMapToQuestZone = NewSetMapToQuestZone
+end
+
+local function HookSetMapToPlayerLocation()
+	orgSetMapToPlayerLocation = SetMapToPlayerLocation
+	local function NewSetMapToPlayerLocation(...)
+		if not DoesUnitExist("player") then return SET_MAP_RESULT_MAP_FAILED end
+		local result = orgSetMapToPlayerLocation(...)
+		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
+			LogMessage(LOG_DEBUG, "SetMapToPlayerLocation")
+
+			local success, mapResult = lib:CalculateMapMeasurements()
+			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
+				result = mapResult
+			end
+		end
+		-- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
+		return result
+	end
+	SetMapToPlayerLocation = NewSetMapToPlayerLocation
+end
+
+local function HookSetMapToMapListIndex()
+	orgSetMapToMapListIndex = SetMapToMapListIndex
+	local function NewSetMapToMapListIndex(mapIndex)
+		local result = orgSetMapToMapListIndex(mapIndex)
+		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
+			LogMessage(LOG_DEBUG, "SetMapToMapListIndex")
+
+			local success, mapResult = lib:CalculateMapMeasurements()
+			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
+				result = mapResult
+			end
+		end
+
+		-- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
+		return result
+	end
+	SetMapToMapListIndex = NewSetMapToMapListIndex
+end
+
+local function HookProcessMapClick()
+	orgProcessMapClick = ProcessMapClick
+	local function NewProcessMapClick(...)
+		local result = orgProcessMapClick(...)
+		if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
+			LogMessage(LOG_DEBUG, "ProcessMapClick")
+			local success, mapResult = lib:CalculateMapMeasurements()
+			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
+				result = mapResult
+			end
+		end
+		return result
+	end
+	ProcessMapClick = NewProcessMapClick
+end
+
+local function HookSetMapFloor()
+	orgSetMapFloor = SetMapFloor
+	local function NewSetMapFloor(...)
+		local result = orgSetMapFloor(...)
+		if result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured() then
+			LogMessage(LOG_DEBUG, "SetMapFloor")
+			local success, mapResult = lib:CalculateMapMeasurements()
+			if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
+				result = mapResult
+			end
+		end
+		return result
+	end
+	SetMapFloor = NewSetMapFloor
+end
+
+local function Initialize() -- wait until we have defined all functions
+	--- Unregister handler from older libGPS ( < 3)
+	EVENT_MANAGER:UnregisterForEvent("LibGPS2_SaveWaypoint", EVENT_PLAYER_DEACTIVATED)
+	EVENT_MANAGER:UnregisterForEvent("LibGPS2_RestoreWaypoint", EVENT_PLAYER_ACTIVATED)
+
+	--- Unregister handler from older libGPS ( <= 5.1)
+	EVENT_MANAGER:UnregisterForEvent(LIB_NAME .. "_Init", EVENT_PLAYER_ACTIVATED)
+
+	if (lib.Unload) then
+		-- Undo action from older libGPS ( >= 5.2)
+		lib:Unload()
+	end
+
+	--- Register new Unload
+	function lib:Unload()
+		SetMapToQuestCondition = orgSetMapToQuestCondition
+		SetMapToQuestZone = orgSetMapToQuestZone
+		SetMapToPlayerLocation = orgSetMapToPlayerLocation
+		SetMapToMapListIndex = orgSetMapToMapListIndex
+		ProcessMapClick = orgProcessMapClick
+		SetMapFloor = orgSetMapFloor
+
+		LMP:UnregisterCallback("AfterPingAdded", HandlePingEvent)
+		LMP:UnregisterCallback("AfterPingRemoved", HandlePingEvent)
+	end
+
+	InterceptMapPinManager()
+
+	--- Unregister handler from older libGPS, as it is now managed by LibMapPing ( >= 6)
+	EVENT_MANAGER:UnregisterForEvent(LIB_NAME .. "_UnmuteMapPing", EVENT_MAP_PING)
+
+	HookSetMapToQuestCondition()
+	HookSetMapToQuestZone()
+	HookSetMapToPlayerLocation()
+	HookSetMapToMapListIndex()
+	HookProcessMapClick()
+	HookSetMapFloor()
+
+	StoreTamrielMapMeasurements()
+	SetMapToPlayerLocation() -- initial measurement so we can get back to where we are correctly
+
+	LMP:RegisterCallback("AfterPingAdded", HandlePingEvent)
+	LMP:RegisterCallback("AfterPingRemoved", HandlePingEvent)
+end
+
+------------------------ public functions ----------------------
+
+--- Returns true as long as the player exists.
+function lib:IsReady()
+	return DoesUnitExist("player")
+end
+
+--- Returns true if the library is currently doing any measurements.
+function lib:IsMeasuring()
+	return measuring
+end
+
+--- Removes all cached measurement values.
+function lib:ClearMapMeasurements()
+	mapMeasurements = { }
+end
+
+--- Removes the cached measurement values for the map that is currently active.
+function lib:ClearCurrentMapMeasurements()
+	local mapId = GetMapTileTexture()
+	mapMeasurements[mapId] = nil
+end
+
+--- Returns a table with the measurement values for the active map or nil if the measurements could not be calculated for some reason.
+--- The table contains scaleX, scaleY, offsetX, offsetY and mapIndex.
+--- scaleX and scaleY are the dimensions of the active map on the Tamriel map.
+--- offsetX and offsetY are the offset of the top left corner on the Tamriel map.
+--- mapIndex is the mapIndex of the parent zone of the current map.
+function lib:GetCurrentMapMeasurements()
+	local mapId = GetMapTileTexture()
+	if (not mapMeasurements[mapId]) then
+		-- try to calculate the measurements if they are not yet available
+		lib:CalculateMapMeasurements()
+	end
+	return mapMeasurements[mapId]
+end
+
+--- Calculates the measurements for the current map and all parent maps.
+--- This method does nothing if there is already a cached measurement for the active map.
+--- return[1] boolean - True, if a valid measurement was calculated
+--- return[2] SetMapResultCode - Specifies if the map has changed or failed during measurement (independent of the actual result of the measurement)
+function lib:CalculateMapMeasurements(returnToInitialMap)
+	-- cosmic map cannot be measured (GetMapPlayerWaypoint returns 0,0)
+	if (GetMapType() == MAPTYPE_COSMIC) then return false, SET_MAP_RESULT_CURRENT_MAP_UNCHANGED end
+
+	-- no need to take measurements more than once
+	local mapId = GetMapTileTexture()
+	if (mapMeasurements[mapId] or mapId == "") then return false end
+
+	if (lib.debugMode) then
+		LogMessage("Called from", GetAddon(), "for", mapId)
+	end
+
+	-- get the player position on the current map
+	local localX, localY = GetPlayerPosition()
+	if (localX == 0 and localY == 0) then
+		-- cannot take measurements while player position is not initialized
+		return false, SET_MAP_RESULT_CURRENT_MAP_UNCHANGED
+	end
+
+	returnToInitialMap = (returnToInitialMap ~= false)
+
+	measuring = true
+	CALLBACK_MANAGER:FireCallbacks(lib.LIB_EVENT_STATE_CHANGED, measuring)
+
+	-- check some facts about the current map, so we can reset it later
+	--	local oldMapIsZoneMap, oldMapFloor, oldMapFloorCount
+	if returnToInitialMap then
+		lib:PushCurrentMap()
+	end
+
+	local hasWaypoint = LMP:HasMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
+	if(hasWaypoint) then StoreCurrentWaypoint() end
+
+	local mapIndex = CalculateMeasurements(mapId, localX, localY)
+
+	-- Until now, the waypoint was abused. Now the waypoint must be restored or removed again (not from Lua only).
+	if(hasWaypoint) then
+		RestoreCurrentWaypoint()
+	else
+		RemovePlayerWaypoint()
+	end
+
+	if (returnToInitialMap) then
+		local result = lib:PopCurrentMap()
+		return true, result
+	end
+
+	return true, (mapId == GetMapTileTexture()) and SET_MAP_RESULT_CURRENT_MAP_UNCHANGED or SET_MAP_RESULT_MAP_CHANGED
+end
+
+--- Converts the given map coordinates on the current map into coordinates on the Tamriel map.
+--- Returns x and y on the world map and the mapIndex of the parent zone
+--- or nil if the measurements of the active map are not available.
+function lib:LocalToGlobal(x, y)
+	local measurements = lib:GetCurrentMapMeasurements()
+	if (measurements) then
+		x = x * measurements.scaleX + measurements.offsetX
+		y = y * measurements.scaleY + measurements.offsetY
+		return x, y, measurements.mapIndex
+	end
+end
+
+--- Converts the given global coordinates into a position on the active map.
+--- Returns x and y on the current map or nil if the measurements of the active map are not available.
+function lib:GlobalToLocal(x, y)
+	local measurements = lib:GetCurrentMapMeasurements()
+	if (measurements) then
+		x =(x - measurements.offsetX) / measurements.scaleX
+		y =(y - measurements.offsetY) / measurements.scaleY
+		return x, y
+	end
+end
+
+--- Converts the given map coordinates on the specified zone map into coordinates on the Tamriel map.
+--- This method is useful if you want to convert global positions from the old LibGPS version into the new format.
+--- Returns x and y on the world map and the mapIndex of the parent zone
+--- or nil if the measurements of the zone map are not available.
+function lib:ZoneToGlobal(mapIndex, x, y)
+	lib:GetCurrentMapMeasurements()
+	-- measurement done in here:
+	SetMapToMapListIndex(mapIndex)
+	x, y, mapIndex = lib:LocalToGlobal(x, y)
+	return x, y, mapIndex
+end
+
+--- This function zooms and pans to the specified position on the active map.
+function lib:PanToMapPosition(x, y)
+	-- if we don't have access to the mapPinManager we cannot do anything
+	if (not lib.mapPinManager) then return end
+
+	local mapPinManager = lib.mapPinManager
+	-- create dummy pin
+	local pin = mapPinManager:CreatePin(_G[DUMMY_PIN_TYPE], "libgpsdummy", x, y)
+
+	-- replace GetPlayerPin to return our dummy pin
+	local getPlayerPin = mapPinManager.GetPlayerPin
+	mapPinManager.GetPlayerPin = function() return pin end
+
+	-- let the map pan to our dummy pin
+	ZO_WorldMap_PanToPlayer()
+
+	-- cleanup
+	mapPinManager.GetPlayerPin = getPlayerPin
+	mapPinManager:RemovePins(DUMMY_PIN_TYPE)
+end
+
+--- This function sets the current map as player chosen so it won't switch back to the previous map.
+function lib:SetPlayerChoseCurrentMap()
+	-- replace the original functions
+	local oldIsChangingAllowed = ZO_WorldMap_IsMapChangingAllowed
+	local oldSetMapToMapListIndex = SetMapToMapListIndex
+	ZO_WorldMap_IsMapChangingAllowed = function() return true end
+	SetMapToMapListIndex = function() return SET_MAP_RESULT_MAP_CHANGED end
+
+	-- make our rigged call to set the player chosen flag
+	ZO_WorldMap_SetMapByIndex()
+
+	-- cleanup
+	ZO_WorldMap_IsMapChangingAllowed = oldIsChangingAllowed
+	SetMapToMapListIndex = oldSetMapToMapListIndex
+end
+
+--- Repeatedly calls ProcessMapClick on the given global position starting on the Tamriel map until nothing more would happen.
+--- Returns SET_MAP_RESULT_FAILED, SET_MAP_RESULT_MAP_CHANGED or SET_MAP_RESULT_CURRENT_MAP_UNCHANGED depending on the result of the API calls.
+function lib:MapZoomInMax(x, y)
+	local result = SetMapToMapListIndex(TAMRIEL_MAP_INDEX)
+
+	if (result ~= SET_MAP_RESULT_FAILED) then
+		local localX, localY = x, y
+
+		while WouldProcessMapClick(localX, localY) do
+			result = orgProcessMapClick(localX, localY)
+			if (result == SET_MAP_RESULT_FAILED) then break end
+			localX, localY = lib:GlobalToLocal(x, y)
+		end
+	end
+
+	return result
+end
+
+--- Stores information about how we can back to this map on a stack.
+function lib:PushCurrentMap()
+	local wasPlayerLocation, currentMapIndex, targetMapTileTexture, currentMapFloor, currentMapFloorCount
+	wasPlayerLocation = DoesCurrentMapMatchMapForPlayerLocation()
+	currentMapIndex = GetCurrentMapIndex()
+	targetMapTileTexture = GetMapTileTexture()
+	currentMapFloor, currentMapFloorCount = GetMapFloorInfo()
+
+	mapStack[#mapStack + 1] = {wasPlayerLocation, currentMapIndex, targetMapTileTexture, currentMapFloor, currentMapFloorCount}
+end
+
+--- Switches to the map that was put on the stack last.
+--- Returns SET_MAP_RESULT_FAILED, SET_MAP_RESULT_MAP_CHANGED or SET_MAP_RESULT_CURRENT_MAP_UNCHANGED depending on the result of the API calls.
+function lib:PopCurrentMap()
+	local result = SET_MAP_RESULT_FAILED
+	local data = table.remove(mapStack, #mapStack)
+	if(not data) then
+		LogMessage(LOG_DEBUG, "PopCurrentMap failed. No data on map stack.")
+		return result
+	end
+
+	local wasPlayerLocation, currentMapIndex, targetMapTileTexture, currentMapFloor, currentMapFloorCount = unpack(data)
+
+	local currentTileTexture = GetMapTileTexture()
+	if(currentTileTexture ~= targetMapTileTexture) then
+		if(wasPlayerLocation) then -- switch back to the player location
+			result = orgSetMapToPlayerLocation()
+
+		elseif(currentMapIndex ~= nil and currentMapIndex > 0) then -- set to a zone map
+			result = orgSetMapToMapListIndex(currentMapIndex)
+
+		else -- here is where it gets tricky
+			local target = mapMeasurements[targetMapTileTexture]
+			assert(target, string.format("No measurement for \"%s\".", targetMapTileTexture))
+
+			-- switch to the parent zone
+			if(target.mapIndex == TAMRIEL_MAP_INDEX) then -- zone map has no mapIndex (e.g. Eyevea or Hew's Bane on first PTS patch for update 9)
+				-- switch to the tamriel map just in case
+				result = orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX)
+				if(result == SET_MAP_RESULT_FAILED) then return result end
+				-- get global coordinates of target map center
+				local x = target.offsetX + (target.scaleX / 2)
+				local y = target.offsetY + (target.scaleY / 2)
+				assert(WouldProcessMapClick(x, y), string.format("Cannot process click at %s/%s on map \"%s\" in order to get to \"%s\".", tostring(x), tostring(y), GetMapTileTexture(), targetMapTileTexture))
+				result = orgProcessMapClick(x, y)
+				if(result == SET_MAP_RESULT_FAILED) then return result end
+			else
+				result = orgSetMapToMapListIndex(target.mapIndex)
+				if(result == SET_MAP_RESULT_FAILED) then return result end
+			end
+
+			-- switch to the sub zone
+			currentTileTexture = GetMapTileTexture()
+			if(currentTileTexture ~= targetMapTileTexture) then
+				-- determine where on the zone map we have to click to get to the sub zone map
+				-- get global coordinates of target map center
+				local x = target.offsetX + (target.scaleX / 2)
+				local y = target.offsetY + (target.scaleY / 2)
+				-- transform to local coordinates
+				local current = mapMeasurements[currentTileTexture]
+				assert(current, string.format("No measurement for \"%s\".", currentTileTexture))
+				x = (x - current.offsetX) / current.scaleX
+				y = (y - current.offsetY) / current.scaleY
+
+				assert(WouldProcessMapClick(x, y), string.format("Cannot process click at %s/%s on map \"%s\" in order to get to \"%s\".", tostring(x), tostring(y), GetMapTileTexture(), targetMapTileTexture))
+				result = orgProcessMapClick(x, y)
+				if(result == SET_MAP_RESULT_FAILED) then return result end
+			end
+
+			-- switch to the correct floor (e.g. Elden Root)
+			if (currentMapFloorCount > 0) then
+				result = orgSetMapFloor(currentMapFloor)
+			end
+		end
+	else
+		result = SET_MAP_RESULT_CURRENT_MAP_UNCHANGED
+	end
+
+	return result
+end
+
+Initialize()
diff --git a/lib/LibGroupSocket/LibGroupSocket.lua b/lib/LibGroupSocket/LibGroupSocket.lua
new file mode 100644
index 0000000..ae20e7c
--- /dev/null
+++ b/lib/LibGroupSocket/LibGroupSocket.lua
@@ -0,0 +1,342 @@
+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
diff --git a/lib/LibGroupSocket/handlers/SyncTimeStampHandler.lua b/lib/LibGroupSocket/handlers/SyncTimeStampHandler.lua
new file mode 100644
index 0000000..20d7218
--- /dev/null
+++ b/lib/LibGroupSocket/handlers/SyncTimeStampHandler.lua
@@ -0,0 +1,207 @@
+---------------------------------------------------------------
+--- Handler To Sync timestamps from groupleader to group ------
+--- Version 1.0 -----------------------------------------------
+--- Author: silentgecko ---------------------------------------
+---------------------------------------------------------------
+
+local LGS = LibStub("LibGroupSocket")
+local type, version = LGS.MESSAGE_TYPE_SYNC_TIMESTAMP, 1
+local handler = LGS:RegisterHandler(type, version) -- TODO return saveData
+if(not handler) then return end
+local SKIP_CREATE = true
+local Log = LGS.Log
+local ON_GET_OFFSET         = "OnGetOffset"
+local ON_FIN_SYNC_TIMESTAMP = "OnFinishedSyncTimeStamp"
+local ON_FIN_SYNC_OFFSET    = "OnFinishedSyncOffset"
+
+local lastSendTime = 0
+
+local saveData = {
+    enabled = true,
+} -- TODO
+
+local SEND_TIMEOUT = 300
+local MIN_SYNC_TIMEOUT = 5
+local COUNT_SYNC = 0
+local MAX_SYNC_TIMES = 5
+
+--calculation vars
+local tempOffsets = {}
+local offset      = 0
+local finished    = false
+
+local function OnData(unitTag, data, isSelf)
+    -- don't handle data from self, this is the grpleader
+    if isSelf then return end
+
+    local index = 1
+    local firstPart,  index = LGS:ReadUint16(data, index)
+    local count,      index = LGS:ReadUint8(data, index)
+    local secondPart, index = LGS:ReadUint16(data, index)
+    local thirdPart,  index = LGS:ReadUint16(data, index)
+
+    local timeStamp = tonumber(tostring(firstPart) .. tostring(secondPart) .. tostring(thirdPart))
+
+    local expectedLength = 7
+    if(#data < expectedLength) then Log("SyncTimeStampHandler received only %d of %d byte", #data, expectedLength) return end
+
+    --Log("SyncTimeStampHandler: Synchronice TS received from: %s, TS: %s", GetUnitName(unitTag), tostring(timeStamp))
+
+    handler:calculateTimeStampOffset(timeStamp, count)
+
+    LGS.cm:FireCallbacks(ON_GET_OFFSET, offset, finished)
+
+    if finished then
+        local syncedTimeStamp, _ = handler:GetTimeStamp()
+        LGS.cm:FireCallbacks(ON_FIN_SYNC_TIMESTAMP, syncedTimeStamp, finished)
+        LGS.cm:FireCallbacks(ON_FIN_SYNC_OFFSET, offset, finished)
+    end
+end
+
+function handler:GetTimeStamp()
+    local timeStamp = GetTimeStamp() + offset
+
+    return timeStamp, finished
+end
+
+function handler:calculateTimeStampOffset(timeStamp, count)
+    --round function
+    local function round(x)
+       return x>=0 and math.floor(x+0.5) or math.ceil(x-0.5)
+    end
+
+    local localTs = GetTimeStamp()
+
+    local offsetCalc = round(localTs - timeStamp)
+    table.insert(tempOffsets, offsetCalc)
+
+    -- we can't calculate when we only have 1 data
+    if count == 0 then return end
+
+    if count == MAX_SYNC_TIMES then
+        finished = true
+    end
+
+    local calcOffset = 0
+    for i=1, #tempOffsets do
+       calcOffset = calcOffset + tempOffsets[i]
+    end
+    local countOffsets = #tempOffsets
+    offset = round(calcOffset / countOffsets)
+
+    -- reset table
+    if count == 5 then
+       tempOffsets = {}
+    end
+end
+
+function handler:Send()
+    -- only send ping when enabled AND is in group AND player is leader
+    if(not saveData.enabled or not IsUnitGrouped("player") or not IsUnitGroupLeader("player")) then return end
+
+    local now = GetTimeStamp()
+
+    local timeout = SEND_TIMEOUT
+    if COUNT_SYNC < MAX_SYNC_TIMES then
+        timeout = MIN_SYNC_TIMEOUT
+    end
+
+    if(now - lastSendTime < timeout) then
+        return
+    end
+
+    COUNT_SYNC = COUNT_SYNC+1
+
+    local data = {}
+    local index = 1
+    local ts = GetTimeStamp()
+    local firstPart  = tonumber(string.sub(tostring(ts), 1, 4))
+    local secondPart = tonumber(string.sub(tostring(ts), 5, 7))
+    local thirdPart  = tonumber(string.sub(tostring(ts), 8))
+
+
+    index = LGS:WriteUint16(data, index, firstPart)
+    index = LGS:WriteUint8(data, index, (COUNT_SYNC-1))
+    index = LGS:WriteUint16(data, index, secondPart)
+    index = LGS:WriteUint16(data, index, thirdPart)
+    index = index + 1
+
+    lastSendTime = now
+    --Log("SyncTimeStampHandler: Send Synchronice TS Ping: %s", tostring(ts))
+    LGS:Send(type, data)
+end
+
+function handler:RegisterForGetOffset(callback)
+    LGS.cm:RegisterCallback(ON_GET_OFFSET, callback)
+end
+
+function handler:UnregisterForGetOffset(callback)
+    LGS.cm:UnregisterCallback(ON_GET_OFFSET, callback)
+end
+
+function handler:RegisterForFinishedSyncTimeStamp(callback)
+    LGS.cm:RegisterCallback(ON_FIN_SYNC_TIMESTAMP, callback)
+end
+
+function handler:UnregisterForFinishedSyncTimeStamp(callback)
+    LGS.cm:UnregisterCallback(ON_FIN_SYNC_TIMESTAMP, callback)
+end
+
+function handler:RegisterForFinishedSyncOffset(callback)
+    LGS.cm:RegisterCallback(ON_FIN_SYNC_OFFSET, callback)
+end
+
+function handler:UnregisterForFinishedSyncOffset(callback)
+    LGS.cm:UnregisterCallback(ON_FIN_SYNC_OFFSET, callback)
+end
+
+local function OnUpdate()
+    handler:Send()
+end
+
+local isActive = false
+
+local function StartSending()
+    if(not isActive and saveData.enabled and IsUnitGrouped("player")) then
+        EVENT_MANAGER:RegisterForUpdate("LibGroupSocketSyncTimeStampHandler", 1000, OnUpdate)
+        isActive = true
+    end
+end
+
+local function StopSending()
+    if(isActive) then
+        EVENT_MANAGER:UnregisterForUpdate("LibGroupSocketSyncTimeStampHandler")
+        isActive = false
+    end
+end
+
+local function OnGroupChange()
+    -- reset count sync for sender and receiver
+    COUNT_SYNC = 0
+    finished = false
+    if IsUnitGrouped("player") and IsUnitGroupLeader("player") then
+        StartSending()
+    else
+        StopSending()
+    end
+end
+
+local function Unload()
+    LGS.cm:UnregisterCallback(type, handler.dataHandler)
+    EVENT_MANAGER:UnregisterForEvent("LibGroupSocketFtcDpsHandler", EVENT_LEADER_UPDATE)
+    EVENT_MANAGER:UnregisterForEvent("LibGroupSocketFtcDpsHandler", EVENT_GROUP_MEMBER_JOINED)
+    StopSending()
+end
+
+local function Load()
+    handler.dataHandler = OnData
+    LGS.cm:RegisterCallback(type, OnData)
+    EVENT_MANAGER:RegisterForEvent("LibGroupSocketFtcDpsHandler", EVENT_LEADER_UPDATE, OnGroupChange)
+    EVENT_MANAGER:RegisterForEvent("LibGroupSocketFtcDpsHandler", EVENT_GROUP_MEMBER_JOINED, OnGroupChange)
+    handler.Unload = Unload
+
+    StartSending()
+end
+
+if(handler.Unload) then handler.Unload() end
+Load()
\ No newline at end of file
diff --git a/lib/LibMapPing/LibMapPing.lua b/lib/LibMapPing/LibMapPing.lua
new file mode 100644
index 0000000..711b5ed
--- /dev/null
+++ b/lib/LibMapPing/LibMapPing.lua
@@ -0,0 +1,219 @@
+local LIB_IDENTIFIER = "LibMapPing"
+local lib = LibStub:NewLibrary("LibMapPing", 1)
+
+if not lib then
+	return	-- already loaded and no upgrade necessary
+end
+local g_mapPinManager
+
+local function Log(message, ...)
+	df("[%s] %s", LIB_IDENTIFIER, message:format(...))
+end
+
+local MAP_PIN_TYPE_PLAYER_WAYPOINT = MAP_PIN_TYPE_PLAYER_WAYPOINT
+local MAP_PIN_TYPE_PING = MAP_PIN_TYPE_PING
+local MAP_PIN_TYPE_RALLY_POINT = MAP_PIN_TYPE_RALLY_POINT
+
+local MAP_PIN_TAG_PLAYER_WAYPOINT = "waypoint"
+local MAP_PIN_TAG_RALLY_POINT = "rally"
+local PING_CATEGORY = "pings"
+
+local MAP_PIN_TAG = {
+	[MAP_PIN_TYPE_PLAYER_WAYPOINT] = MAP_PIN_TAG_PLAYER_WAYPOINT,
+	--[MAP_PIN_TYPE_PING] = group pings have individual tags for each member
+	[MAP_PIN_TYPE_RALLY_POINT] = MAP_PIN_TAG_RALLY_POINT,
+}
+
+local GET_MAP_PING_FUNCTION = {
+	[MAP_PIN_TYPE_PLAYER_WAYPOINT] = GetMapPlayerWaypoint,
+	[MAP_PIN_TYPE_PING] = GetMapPing,
+	[MAP_PIN_TYPE_RALLY_POINT] = GetMapRallyPoint,
+}
+
+local REMOVE_MAP_PING_FUNCTION = {
+	[MAP_PIN_TYPE_PLAYER_WAYPOINT] = RemovePlayerWaypoint,
+	[MAP_PIN_TYPE_PING] = function()
+		-- there is no such function for group pings, but we can set it to 0, 0 which effectively hides it
+		PingMap(MAP_PIN_TYPE_PING, MAP_TYPE_LOCATION_CENTERED, 0, 0)
+	end,
+	[MAP_PIN_TYPE_RALLY_POINT] = RemoveRallyPoint,
+}
+
+lib.mutePing = {}
+lib.suppressPing = {}
+lib.isPingSet = {}
+
+local function GetKey(pingType, pingTag)
+	if(pingType == MAP_PIN_TYPE_PLAYER_WAYPOINT) then
+		pingTag = MAP_PIN_TAG_PLAYER_WAYPOINT
+	elseif(pingType == MAP_PIN_TYPE_RALLY_POINT) then
+		pingTag = MAP_PIN_TAG_RALLY_POINT
+	end
+	return string.format("%d_%s", pingType, pingTag)
+end
+
+function GetMapPlayerWaypoint()
+	if(lib:IsPingSuppressed(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_PIN_TAG_PLAYER_WAYPOINT)) then
+		return 0, 0
+	end
+	return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PLAYER_WAYPOINT]()
+end
+
+function GetMapPing(pingTag)
+	if(lib:IsPingSuppressed(MAP_PIN_TYPE_PING, pingTag)) then
+		return 0, 0
+	end
+	return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PING](pingTag)
+end
+
+function GetMapRallyPoint()
+	if(lib:IsPingSuppressed(MAP_PIN_TYPE_RALLY_POINT, MAP_PIN_TAG_RALLY_POINT)) then
+		return 0, 0
+	end
+	return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_RALLY_POINT]()
+end
+
+function lib:SetMapPing(pingType, mapType, x, y)
+	PingMap(pingType, mapType, x, y)
+end
+
+function lib:RemoveMapPing(pingType)
+	if(REMOVE_MAP_PING_FUNCTION[pingType]) then
+		REMOVE_MAP_PING_FUNCTION[pingType]()
+	end
+end
+
+function lib:GetMapPing(pingType, pingTag)
+	local x, y = 0, 0
+	if(GET_MAP_PING_FUNCTION[pingType]) then
+		x, y = GET_MAP_PING_FUNCTION[pingType](pingTag)
+	end
+	return x, y
+end
+
+function lib:HasMapPing(pingType, pingTag) -- TODO: this should return true immediately after calling set, but false inside the map ping event when a ping was set
+	pingTag = pingTag or MAP_PIN_TAG[pingType]
+	if(not pingTag) then
+		Log("No pingTag specified for HasMapPing")
+		return false
+	end
+	local key = GetKey(pingType, pingTag)
+	return (lib.isPingSet[key] == true)
+end
+
+function lib:RefreshMapPin(pingType, pingTag)
+	if(not g_mapPinManager) then
+		Log("PinManager not available. Using ZO_WorldMap_UpdateMap instead.")
+		ZO_WorldMap_UpdateMap()
+		return true
+	end
+
+	pingTag = pingTag or MAP_PIN_TAG[pingType]
+	if(not pingTag) then
+		Log("No pingTag specified for RefreshMapPing")
+		return false
+	end
+
+	g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)
+
+	local x, y = lib:GetMapPing(pingType, pingTag)
+	if(lib:IsPositionOnMap(x, y)) then
+		g_mapPinManager:CreatePin(pingType, pingTag, x, y)
+		return true
+	end
+	return false
+end
+
+function lib:IsPositionOnMap(x, y)
+	return not (x < 0 or y < 0 or x > 1 or y > 1 or (x == 0 and y == 0))
+end
+
+function lib:MutePing(pingType, pingTag)
+	local key = GetKey(pingType, pingTag)
+	local mute = lib.mutePing[key] or 0
+	lib.mutePing[key] = mute + 1
+end
+
+function lib:UnmutePing(pingType, pingTag)
+	local key = GetKey(pingType, pingTag)
+	local mute = (lib.mutePing[key] or 0) - 1
+	if(mute < 0) then mute = 0 end
+	lib.mutePing[key] = mute
+end
+
+function lib:IsPingMuted(pingType, pingTag)
+	local key = GetKey(pingType, pingTag)
+	return lib.mutePing[key] and lib.mutePing[key] > 0
+end
+
+function lib:SuppressPing(pingType, pingTag)
+	local key = GetKey(pingType, pingTag)
+	local suppress = lib.suppressPing[key] or 0
+	lib.suppressPing[key] = suppress + 1
+end
+
+function lib:UnsuppressPing(pingType, pingTag)
+	local key = GetKey(pingType, pingTag)
+	local suppress = (lib.suppressPing[key] or 0) - 1
+	if(suppress < 0) then suppress = 0 end
+	lib.suppressPing[key] = suppress
+end
+
+function lib:IsPingSuppressed(pingType, pingTag)
+	local key = GetKey(pingType, pingTag)
+	return lib.suppressPing[key] and lib.suppressPing[key] > 0
+end
+
+local function InterceptMapPinManager()
+	if (g_mapPinManager) then return end
+	local orgRefreshCustomPins = ZO_WorldMapPins.RefreshCustomPins
+	function ZO_WorldMapPins:RefreshCustomPins()
+		g_mapPinManager = self
+	end
+	ZO_WorldMap_RefreshCustomPinsOfType()
+	ZO_WorldMapPins.RefreshCustomPins = orgRefreshCustomPins
+end
+InterceptMapPinManager()
+
+-- TODO keep an eye on worldmap.lua for changes
+local function HandleMapPing(eventCode, pingEventType, pingType, pingTag, x, y, isPingOwner)
+	if(pingEventType == PING_EVENT_ADDED) then
+		lib.cm:FireCallbacks("BeforePingAdded", pingType, pingTag, x, y, isPingOwner)
+		lib.isPingSet[GetKey(pingType, pingTag)] = true
+		g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)
+		if(not lib:IsPingSuppressed(pingType, pingTag)) then
+			g_mapPinManager:CreatePin(pingType, pingTag, x, y)
+			if(isPingOwner and not lib:IsPingMuted(pingType, pingTag)) then
+				PlaySound(SOUNDS.MAP_PING)
+			end
+		end
+		lib.cm:FireCallbacks("AfterPingAdded", pingType, pingTag, x, y, isPingOwner)
+	elseif(pingEventType == PING_EVENT_REMOVED) then
+		lib.cm:FireCallbacks("BeforePingRemoved", pingType, pingTag, x, y, isPingOwner)
+		lib.isPingSet[GetKey(pingType, pingTag)] = false
+		g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)
+		if (isPingOwner and not lib:IsPingSuppressed(pingType, pingTag) and not lib:IsPingMuted(pingType, pingTag)) then
+			PlaySound(SOUNDS.MAP_PING_REMOVE)
+		end
+		lib.cm:FireCallbacks("AfterPingRemoved", pingType, pingTag, x, y, isPingOwner)
+	end
+end
+
+EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED)
+EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED, function(_, addonName)
+	if(addonName == "ZO_Ingame") then
+		EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED)
+		-- don't let worldmap do anything as we manage it instead
+		EVENT_MANAGER:UnregisterForEvent("ZO_WorldMap", EVENT_MAP_PING)
+		EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_MAP_PING, HandleMapPing)
+	end
+end)
+
+lib.cm = ZO_CallbackObject:New()
+function lib:RegisterCallback(eventName, callback)
+	lib.cm:RegisterCallback(eventName, callback)
+end
+
+function lib:UnregisterCallback(eventName, callback)
+	lib.cm:UnregisterCallback(eventName, callback)
+end
diff --git a/lib/LibStub/LibStub.lua b/lib/LibStub/LibStub.lua
new file mode 100644
index 0000000..0e6bf67
--- /dev/null
+++ b/lib/LibStub/LibStub.lua
@@ -0,0 +1,38 @@
+-- LibStub is a simple versioning stub meant for use in Libraries.  http://www.wowace.com/wiki/LibStub for more info
+-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
+-- LibStub developed for World of Warcraft by above members of the WowAce community.
+-- Ported to Elder Scrolls Online by Seerah
+
+local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 4
+local LibStub = _G[LIBSTUB_MAJOR]
+
+local strformat = string.format
+if not LibStub or LibStub.minor < LIBSTUB_MINOR then
+	LibStub = LibStub or {libs = {}, minors = {} }
+	_G[LIBSTUB_MAJOR] = LibStub
+	LibStub.minor = LIBSTUB_MINOR
+
+	function LibStub:NewLibrary(major, minor)
+		assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
+		if type(minor) ~= "number" then
+			minor = assert(tonumber(zo_strmatch(minor, "%d+%.?%d*")), "Minor version must either be a number or contain a number.")
+		end
+
+		local oldminor = self.minors[major]
+		if oldminor and oldminor >= minor then return nil end
+		self.minors[major], self.libs[major] = minor, self.libs[major] or {}
+		return self.libs[major], oldminor
+	end
+
+	function LibStub:GetLibrary(major, silent)
+		if not self.libs[major] and not silent then
+			error(strformat("Cannot find a library instance of %q.", tostring(major)), 2)
+		end
+		return self.libs[major], self.minors[major]
+	end
+
+	function LibStub:IterateLibraries() return pairs(self.libs) end
+	setmetatable(LibStub, { __call = LibStub.GetLibrary })
+end
+
+LibStub.SILENT = true
\ No newline at end of file