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