forked from Dusk/ShaguTweaks-ClassicAPI
Bound the player cache and stop per-frame range checks
- libunitscan: expire the shared player cache once per session (180 days for known classes, 90 for class-less, at most 10,000 players). Every sighting now stamps an age, so roster and mouseover entries can expire. - libunitscan: keep a player's GUID only while the class is unknown and resolve such entries later with GetPlayerInfoByGUID. - libunitscan: skip the GUID lookup for known chat speakers seen within the hour, throttle full guild rescans to once a minute, skip UnitGUID when UnitClass answered, and query ClassicAPI's player cache once per miss instead of twice. - Social Colors: drop its separate 180-day cleanup. - Range Color: use its own 0.2 s timer. Sharing Blizzard's rangeTimer ran the check every frame for every ranged button. - Docs/PERFORMANCE.md: document the cache rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# Performance notes
|
||||
|
||||
## Player cache (`ShaguTweaks_player_cache`)
|
||||
|
||||
The account-wide player cache feeds Social Colors, Chat Levels and the
|
||||
nameplate/chat class colors. It is read at every login and `/reload` and
|
||||
written at every logout, so its size is a direct load-time and memory cost.
|
||||
Earlier versions let it grow without limit; one account reached 4.1 MB.
|
||||
|
||||
`libs/libunitscan.lua` now owns expiry and runs it once per session, at the
|
||||
first loading screen:
|
||||
|
||||
| Entry | Kept for |
|
||||
|---|---|
|
||||
| Player whose class is known | 180 days after last sighting |
|
||||
| Player whose class was never learned | 90 days after last sighting |
|
||||
| Whole cache | at most 10,000 players; the oldest sightings go first |
|
||||
|
||||
- Every sighting refreshes the age: mouseover, target, nameplate, party,
|
||||
raid, guild roster, friend list, `/who` and chat.
|
||||
- Guild members and friends are refreshed by their roster events every
|
||||
session, so they do not expire while they stay in the guild or friend list.
|
||||
- Entries saved before this change have no age. They are dated from the
|
||||
cache's existing grace start (`players_cleanup`), as the old Social Colors
|
||||
cleanup did. The first login after the update can remove many entries.
|
||||
- A player's GUID is saved only while the class is unknown. Such entries are
|
||||
resolved later with `GetPlayerInfoByGUID` when the engine knows the GUID.
|
||||
|
||||
The separate 180-day cleanup in Social Colors is removed. It ran only every
|
||||
180 days and only while that module was enabled.
|
||||
|
||||
## Event handling
|
||||
|
||||
- Chat: a speaker whose class is known and who spoke within the hour needs
|
||||
no GUID lookup or write.
|
||||
- Guild roster: a full rescan runs at most once a minute unless the member
|
||||
count changed. Many addons request the roster every few seconds.
|
||||
- Mouseover, target, nameplate and group sightings skip `UnitGUID` when
|
||||
`UnitClass` already gave the class.
|
||||
- A name missing from the cache queries ClassicAPI's player cache once
|
||||
instead of twice.
|
||||
|
||||
## Range Color
|
||||
|
||||
The module shared Blizzard's `rangeTimer` and tested it against 0.2 seconds.
|
||||
Once the timer had reset, that test was always true, so the full range check
|
||||
ran on every frame for every ranged action button. It now keeps its own
|
||||
0.2-second timer. Blizzard's own range check runs again, so the hotkey text
|
||||
turns red out of range, as in the default UI.
|
||||
|
||||
## Not changed
|
||||
|
||||
- Health Numbers and Big Health use one-shot deferred updates only.
|
||||
- Both castbars update only while visible; Energy Tick hides at full mana
|
||||
out of combat.
|
||||
- Free Slot Count counts slots once per `BAG_UPDATE_DELAYED`. Disable it if a
|
||||
bag addon already shows the count.
|
||||
|
||||
Nothing here has been measured in game yet. To compare before and after, see
|
||||
[Measuring performance](CLASSICAPI_UPDATES.md#measuring-performance).
|
||||
@@ -179,6 +179,7 @@ For detailed provenance and licensing information, see:
|
||||
- [PROJECT_IDENTITY.md](PROJECT_IDENTITY.md)
|
||||
- [Docs/ASSET_PROVENANCE.md](Docs/ASSET_PROVENANCE.md)
|
||||
- [Docs/MODERN_MANA.md](Docs/MODERN_MANA.md)
|
||||
- [Docs/PERFORMANCE.md](Docs/PERFORMANCE.md) — player cache limits and other performance notes
|
||||
- [Docs/UPSTREAM_README.md](Docs/UPSTREAM_README.md) — the Dusk fork's README, kept for reference
|
||||
- [LICENSES/](LICENSES)
|
||||
|
||||
|
||||
+203
-78
@@ -5,7 +5,15 @@ local API = ShaguTweaks.API
|
||||
|
||||
local units = { players = {}, mobs = {} }
|
||||
local queue = { }
|
||||
local PASSIVE_CACHE_TTL = 90 * 24 * 60 * 60
|
||||
|
||||
-- Player cache limits. ShaguTweaks_player_cache is parsed at every login and
|
||||
-- written at every logout, so its size is a direct load-time and memory cost.
|
||||
local DAY = 24 * 60 * 60
|
||||
local PASSIVE_CACHE_TTL = 90 * DAY -- players whose class was never learned
|
||||
local KNOWN_CACHE_TTL = 180 * DAY -- every other player, counted from last sighting
|
||||
local MAX_CACHED_PLAYERS = 10000 -- oldest sightings go first above this
|
||||
local CHAT_REFRESH = 60 * 60 -- re-resolve a known chat speaker at most hourly
|
||||
local GUILD_SCAN_INTERVAL = 60 -- seconds between full guild roster scans
|
||||
|
||||
local libunitscan = CreateFrame("Frame", "ShaguTweaksUnitScan", UIParent)
|
||||
|
||||
@@ -289,21 +297,46 @@ end
|
||||
|
||||
ShaguTweaks.GetPlayerCache = EnsurePlayerCache
|
||||
|
||||
local function EnrichPlayerFromClassicAPI(name)
|
||||
if not name or not API or not API.GetCachedPlayerInfoByName then return end
|
||||
-- Same player name, ignoring case. Compare first so the common exact match
|
||||
-- does not create two lowercase strings.
|
||||
local function SameName(a, b)
|
||||
return a == b or string.lower(a) == string.lower(b)
|
||||
end
|
||||
|
||||
local _, class, _, _, _, _, _, guid = API.GetCachedPlayerInfoByName(name)
|
||||
class = class and class ~= "" and class or nil
|
||||
guid = guid and guid ~= "" and guid or nil
|
||||
if not class and not guid then return end
|
||||
-- New player records carry a timestamp from the start, so the login cleanup
|
||||
-- can age every entry, not only chat-only ones.
|
||||
local function GetOrCreatePlayer(name)
|
||||
local data = units.players[name]
|
||||
if not data then
|
||||
data = { lastseen_ts = time() }
|
||||
units.players[name] = data
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
-- Keep ShaguTweaks keyed by the exact name it already knows. This avoids
|
||||
-- duplicate SavedVariable entries if another cache normalizes name casing.
|
||||
units.players[name] = units.players[name] or {}
|
||||
units.players[name].class = class or units.players[name].class
|
||||
units.players[name].guid = guid or units.players[name].guid
|
||||
-- Store a class. A GUID is kept only while the class is unknown: it is then
|
||||
-- the one way to learn it later through GetPlayerInfoByGUID. Known players
|
||||
-- need no GUID in SavedVariables, and nothing else reads it.
|
||||
local function SetPlayerClass(data, class, guid)
|
||||
if class then data.class = class end
|
||||
if data.class then
|
||||
data.guid = nil
|
||||
elseif guid then
|
||||
data.guid = guid
|
||||
end
|
||||
end
|
||||
|
||||
return name
|
||||
-- A class-less entry that kept its GUID: ask the engine's name cache.
|
||||
local function EnrichPlayerFromStoredGUID(name, data)
|
||||
if not data or data.class or not data.guid then return end
|
||||
if not API or not API.GetPlayerInfoByGUID then return end
|
||||
|
||||
local _, class, _, _, _, apiName = API.GetPlayerInfoByGUID(data.guid)
|
||||
if not class or class == "" or not apiName or apiName == "" then return end
|
||||
if not SameName(apiName, name) then return end
|
||||
|
||||
SetPlayerClass(data, class)
|
||||
return true
|
||||
end
|
||||
|
||||
local function EnrichPlayerFromCurrentChat(name)
|
||||
@@ -319,11 +352,9 @@ local function EnrichPlayerFromCurrentChat(name)
|
||||
local _, class, _, _, _, apiName = API.GetPlayerInfoByGUID(guid)
|
||||
class = class and class ~= "" and class or nil
|
||||
if not apiName or apiName == "" or not class then return end
|
||||
if string.lower(apiName) ~= string.lower(name) then return end
|
||||
if not SameName(apiName, name) then return end
|
||||
|
||||
units.players[name] = units.players[name] or {}
|
||||
units.players[name].class = class
|
||||
units.players[name].guid = guid
|
||||
SetPlayerClass(GetOrCreatePlayer(name), class, guid)
|
||||
|
||||
return name
|
||||
end
|
||||
@@ -333,34 +364,41 @@ function ShaguTweaks.GetUnitData(name, active)
|
||||
|
||||
EnsurePlayerCache()
|
||||
|
||||
-- Persistent ClassicAPI data can answer class-only queries without making
|
||||
-- another SavedVariables record. Keep our level and social history fields.
|
||||
if (not units.players[name] or not units.players[name].class)
|
||||
and API and API.HasPersistentPlayerCache and API.HasPersistentPlayerCache() then
|
||||
local _, class, _, _, _, resolvedName, _, guid = API.GetCachedPlayerInfoByName(name)
|
||||
if class and class ~= "" and resolvedName
|
||||
and string.lower(resolvedName) == string.lower(name)
|
||||
local data = units.players[name]
|
||||
|
||||
if (not data or not data.class) and not EnrichPlayerFromStoredGUID(name, data) then
|
||||
-- ClassicAPI's player cache by name. Query it once: the persistent-storage
|
||||
-- check below and the plain enrichment both use the same result.
|
||||
local class, guid, resolvedName, _
|
||||
if API and API.GetCachedPlayerInfoByName then
|
||||
_, class, _, _, _, resolvedName, _, guid = API.GetCachedPlayerInfoByName(name)
|
||||
class = class and class ~= "" and class or nil
|
||||
guid = guid and guid ~= "" and guid or nil
|
||||
end
|
||||
|
||||
-- Persistent ClassicAPI data can answer class-only queries without making
|
||||
-- another SavedVariables record. Keep our level and social history fields.
|
||||
if class and resolvedName and SameName(resolvedName, name)
|
||||
and API.HasPersistentPlayerCache and API.HasPersistentPlayerCache()
|
||||
and API.RememberPlayerInfo(resolvedName, class, guid) then
|
||||
local data = units.players[name]
|
||||
return class, data and data.level, data and data.elite, true
|
||||
end
|
||||
end
|
||||
|
||||
if not units["players"][name] then
|
||||
EnrichPlayerFromClassicAPI(name)
|
||||
if not units["players"][name] then
|
||||
EnrichPlayerFromCurrentChat(name)
|
||||
-- Keep ShaguTweaks keyed by the exact name it already knows. This avoids
|
||||
-- duplicate SavedVariable entries if another cache normalizes name casing.
|
||||
if class or guid then
|
||||
data = GetOrCreatePlayer(name)
|
||||
SetPlayerClass(data, class, guid)
|
||||
end
|
||||
elseif not units["players"][name].class then
|
||||
EnrichPlayerFromClassicAPI(name)
|
||||
if not units["players"][name].class then
|
||||
|
||||
if not data or not data.class then
|
||||
EnrichPlayerFromCurrentChat(name)
|
||||
data = units.players[name]
|
||||
end
|
||||
end
|
||||
|
||||
if units["players"][name] then
|
||||
local ret = units["players"][name]
|
||||
return ret.class, ret.level, ret.elite, true
|
||||
if data then
|
||||
return data.class, data.level, data.elite, true
|
||||
end
|
||||
|
||||
if units["mobs"][name] then
|
||||
@@ -372,13 +410,28 @@ function ShaguTweaks.GetUnitData(name, active)
|
||||
end
|
||||
end
|
||||
|
||||
local function AddData(db, name, class, level, elite, guid)
|
||||
local function AddData(db, name, class, level, elite, guid, now)
|
||||
if not name then return end
|
||||
units[db][name] = units[db][name] or {}
|
||||
units[db][name].class = class or units[db][name].class
|
||||
units[db][name].level = level or units[db][name].level
|
||||
units[db][name].elite = elite or units[db][name].elite
|
||||
units[db][name].guid = guid or units[db][name].guid
|
||||
|
||||
local data = units[db][name]
|
||||
if not data then
|
||||
data = {}
|
||||
units[db][name] = data
|
||||
end
|
||||
|
||||
if level then data.level = level end
|
||||
if elite then data.elite = elite end
|
||||
|
||||
if db == "players" then
|
||||
SetPlayerClass(data, class, guid)
|
||||
-- Every sighting refreshes the cache age. "lastseen" stays the chat and
|
||||
-- friend-list date that Social Colors shows; this is only for expiry.
|
||||
data.lastseen_ts = now or time()
|
||||
else
|
||||
if class then data.class = class end
|
||||
if guid then data.guid = guid end
|
||||
end
|
||||
|
||||
queue[name] = nil
|
||||
end
|
||||
|
||||
@@ -387,6 +440,17 @@ local function RememberPlayer(name)
|
||||
|
||||
EnsurePlayerCache()
|
||||
|
||||
local now = time()
|
||||
local data = units.players[name]
|
||||
|
||||
-- Busy channels repeat the same speakers. A speaker whose class is known
|
||||
-- and who was already seen in chat within the hour needs no GUID lookup
|
||||
-- and no write.
|
||||
if data and data.class and type(data.lastseen) == "number"
|
||||
and now - data.lastseen < CHAT_REFRESH then
|
||||
return
|
||||
end
|
||||
|
||||
-- ClassicAPI exposes the GUID of the CHAT_MSG_* sender synchronously.
|
||||
-- Resolve class from the engine's existing name cache only: this is a pure
|
||||
-- cache read and never sends a /who or any other network query.
|
||||
@@ -401,29 +465,68 @@ local function RememberPlayer(name)
|
||||
|
||||
-- Skip only NEW chat-only records after native persistent storage succeeds.
|
||||
-- Existing records may contain levels/history and must retain timestamps.
|
||||
if not units.players[name] and API and API.RememberPlayerInfo
|
||||
if not data and API and API.RememberPlayerInfo
|
||||
and API.RememberPlayerInfo(name, class, guid) then return end
|
||||
|
||||
units.players[name] = units.players[name] or {}
|
||||
units.players[name].class = class or units.players[name].class
|
||||
units.players[name].guid = guid or units.players[name].guid
|
||||
local now = time()
|
||||
units.players[name].lastseen = now
|
||||
units.players[name].lastseen_ts = now
|
||||
data = GetOrCreatePlayer(name)
|
||||
SetPlayerClass(data, class, guid)
|
||||
data.lastseen = now
|
||||
data.lastseen_ts = now
|
||||
end
|
||||
|
||||
local function PrunePassivePlayers()
|
||||
-- One pass per session, at the first PLAYER_ENTERING_WORLD.
|
||||
local function PrunePlayers(realmdb)
|
||||
local players = units.players
|
||||
local now = time()
|
||||
|
||||
for name, data in pairs(units.players) do
|
||||
-- Normalize any empty legacy/API class token back to an unknown value.
|
||||
if data and data.class == "" then data.class = nil end
|
||||
-- Entries written before every sighting carried a timestamp have no age.
|
||||
-- Date them from the cache's grace start, as the old Social Colors cleanup
|
||||
-- did, so they expire on schedule unless the player is seen again.
|
||||
local graceStart = tonumber(realmdb.players_cleanup)
|
||||
if not graceStart then
|
||||
graceStart = now
|
||||
realmdb.players_cleanup = now
|
||||
end
|
||||
|
||||
-- Keep every player whose class was learned. Only anonymous/passive chat
|
||||
-- entries expire, and only when they carry our numeric last-seen timestamp.
|
||||
if data and not data.class and data.lastseen_ts and
|
||||
now - data.lastseen_ts > PASSIVE_CACHE_TTL then
|
||||
units.players[name] = nil
|
||||
local count = 0
|
||||
for name, data in pairs(players) do
|
||||
if type(data) ~= "table" then
|
||||
players[name] = nil
|
||||
else
|
||||
-- Normalize any empty legacy/API class token back to an unknown value.
|
||||
if data.class == "" then data.class = nil end
|
||||
if data.class then data.guid = nil end
|
||||
|
||||
local seen = GetPlayerTimestamp(data)
|
||||
if not seen then
|
||||
seen = graceStart
|
||||
data.lastseen_ts = graceStart
|
||||
end
|
||||
|
||||
local ttl = data.class and KNOWN_CACHE_TTL or PASSIVE_CACHE_TTL
|
||||
if now - seen > ttl then
|
||||
players[name] = nil
|
||||
else
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Hard cap: drop the oldest sightings. Guild members and friends are
|
||||
-- refreshed by their roster events every session, so they stay.
|
||||
if count > MAX_CACHED_PLAYERS then
|
||||
local stamps, n = {}, 0
|
||||
for _, data in pairs(players) do
|
||||
n = n + 1
|
||||
stamps[n] = GetPlayerTimestamp(data) or 0
|
||||
end
|
||||
table.sort(stamps)
|
||||
|
||||
local cutoff = stamps[n - MAX_CACHED_PLAYERS]
|
||||
for name, data in pairs(players) do
|
||||
if (GetPlayerTimestamp(data) or 0) <= cutoff then
|
||||
players[name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -446,6 +549,15 @@ local passiveChatEvents = {
|
||||
}
|
||||
|
||||
local useNameplateUnitEvents = API and API.nameplateevents
|
||||
local prunedThisSession = false
|
||||
local lastGuildScan, lastGuildCount
|
||||
|
||||
-- A GUID is only stored for players whose class is unknown, so skip the call
|
||||
-- when UnitClass already answered.
|
||||
local function GUIDIfClassless(unit, class)
|
||||
if class or not API or not API.UnitGUID then return end
|
||||
return API.UnitGUID(unit)
|
||||
end
|
||||
|
||||
libunitscan:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
libunitscan:RegisterEvent("PLAYER_LOGOUT")
|
||||
@@ -479,18 +591,21 @@ libunitscan:SetScript("OnEvent", function()
|
||||
-- Ensure the saved database is already bound even if another module queried
|
||||
-- it earlier while restoring UI state. From here onward SavedVariables are
|
||||
-- settled, so future cache reads can skip migration checks completely.
|
||||
EnsurePlayerCache()
|
||||
local _, realmdb = EnsurePlayerCache()
|
||||
cacheMigrationFinalized = true
|
||||
|
||||
-- One cheap cleanup pass at login. Known classes are kept forever.
|
||||
PrunePassivePlayers()
|
||||
-- PLAYER_ENTERING_WORLD also fires at every loading screen. Expire and
|
||||
-- cap the cache once per session only.
|
||||
if not prunedThisSession then
|
||||
prunedThisSession = true
|
||||
PrunePlayers(realmdb)
|
||||
end
|
||||
|
||||
-- update own character details
|
||||
local name = UnitName("player")
|
||||
local _, class = UnitClass("player")
|
||||
local level = UnitLevel("player")
|
||||
local guid = API and API.UnitGUID and API.UnitGUID("player")
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless("player", class))
|
||||
|
||||
elseif event == "NAME_PLATE_UNIT_ADDED" then
|
||||
-- ClassicAPI exposes an exact nameplateN unit token here. Cache players
|
||||
@@ -502,8 +617,7 @@ libunitscan:SetScript("OnEvent", function()
|
||||
local name = UnitName(unit)
|
||||
local _, class = UnitClass(unit)
|
||||
local level = UnitLevel(unit)
|
||||
local guid = API and API.UnitGUID and API.UnitGUID(unit)
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless(unit, class))
|
||||
end
|
||||
|
||||
elseif passiveChatEvents[event] then
|
||||
@@ -513,48 +627,60 @@ libunitscan:SetScript("OnEvent", function()
|
||||
|
||||
elseif event == "FRIENDLIST_UPDATE" then
|
||||
local name, class, level
|
||||
local now = time()
|
||||
for i = 1, GetNumFriends() do
|
||||
name, level, class = GetFriendInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
-- friendlist updates due to friend going off-line return level 0, let's not overwrite good older values
|
||||
level = level > 0 and level or nil
|
||||
AddData("players", name, class, level)
|
||||
AddData("players", name, class, level, nil, nil, now)
|
||||
end
|
||||
|
||||
elseif event == "GUILD_ROSTER_UPDATE" then
|
||||
local name, class, level, _
|
||||
for i = 1, GetNumGuildMembers() do
|
||||
name, _, _, level, class = GetGuildRosterInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
AddData("players", name, class, level)
|
||||
-- Many addons request the roster every few seconds. A class and level
|
||||
-- cache gains nothing from rescanning a large guild each time, so scan
|
||||
-- at most once a minute unless the member count changed.
|
||||
local count = GetNumGuildMembers()
|
||||
local now = time()
|
||||
if count > 0 and not (lastGuildScan and count == lastGuildCount
|
||||
and now - lastGuildScan < GUILD_SCAN_INTERVAL) then
|
||||
lastGuildScan, lastGuildCount = now, count
|
||||
|
||||
local name, class, level, _
|
||||
for i = 1, count do
|
||||
name, _, _, level, class = GetGuildRosterInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
AddData("players", name, class, level, nil, nil, now)
|
||||
end
|
||||
end
|
||||
|
||||
elseif event == "RAID_ROSTER_UPDATE" then
|
||||
local name, class, SubGroup, level, _
|
||||
local now = time()
|
||||
for i = 1, GetNumRaidMembers() do
|
||||
name, _, SubGroup, level, class = GetRaidRosterInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
local guid = API and API.UnitGUID and API.UnitGUID("raid" .. i)
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless("raid" .. i, class), now)
|
||||
end
|
||||
|
||||
elseif event == "PARTY_MEMBERS_CHANGED" then
|
||||
local name, class, level, unit, _
|
||||
local now = time()
|
||||
for i = 1, GetNumPartyMembers() do
|
||||
unit = "party" .. i
|
||||
_, class = UnitClass(unit)
|
||||
name = UnitName(unit)
|
||||
level = UnitLevel(unit)
|
||||
local guid = API and API.UnitGUID and API.UnitGUID(unit)
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless(unit, class), now)
|
||||
end
|
||||
|
||||
elseif event == "WHO_LIST_UPDATE" then
|
||||
local name, class, level, _
|
||||
local now = time()
|
||||
for i = 1, GetNumWhoResults() do
|
||||
name, _, level, _, class, _ = GetWhoInfo(i)
|
||||
class = L["class"][class] or nil
|
||||
AddData("players", name, class, level)
|
||||
AddData("players", name, class, level, nil, nil, now)
|
||||
end
|
||||
|
||||
elseif event == "UPDATE_MOUSEOVER_UNIT" or event == "PLAYER_TARGET_CHANGED" then
|
||||
@@ -564,8 +690,7 @@ libunitscan:SetScript("OnEvent", function()
|
||||
_, class = UnitClass(scan)
|
||||
level = UnitLevel(scan)
|
||||
name = UnitName(scan)
|
||||
local guid = API and API.UnitGUID and API.UnitGUID(scan)
|
||||
AddData("players", name, class, level, nil, guid)
|
||||
AddData("players", name, class, level, nil, GUIDIfClassless(scan, class))
|
||||
else
|
||||
_, class = UnitClass(scan)
|
||||
elite = UnitClassification(scan)
|
||||
|
||||
@@ -50,14 +50,28 @@ local function GetActionRange(action)
|
||||
return NormalizeRange(IsActionInRange(action))
|
||||
end
|
||||
|
||||
local RANGE_INTERVAL = TOOLTIP_UPDATE_TIME or 0.2
|
||||
|
||||
module.enable = function(self)
|
||||
hooksecurefunc("ActionButton_OnUpdate", function(elapsed)
|
||||
-- Blizzard sets rangeTimer only for actions that have a range. Use our
|
||||
-- own countdown instead of sharing it: decrementing rangeTimer here and
|
||||
-- testing "<= 0.2" ran this check on every frame for every ranged
|
||||
-- button, and kept Blizzard's own range check from ever firing.
|
||||
if this.rangeTimer then
|
||||
this.rangeTimer = this.rangeTimer - elapsed
|
||||
if this.rangeTimer <= 0.2 then
|
||||
local timer = (this.shaguRangeTimer or 0) - (elapsed or 0)
|
||||
if timer > 0 then
|
||||
this.shaguRangeTimer = timer
|
||||
else
|
||||
this.shaguRangeTimer = RANGE_INTERVAL
|
||||
local action = ActionButton_GetPagedID(this)
|
||||
local inRange = GetActionRange(action)
|
||||
local icon = _G[this:GetName() .. "Icon"]
|
||||
-- Look the icon up once instead of building its name each time.
|
||||
local icon = this.shaguRangeIcon
|
||||
if not icon then
|
||||
icon = _G[this:GetName() .. "Icon"]
|
||||
this.shaguRangeIcon = icon
|
||||
end
|
||||
|
||||
if icon then
|
||||
if inRange == 0 then
|
||||
@@ -78,8 +92,6 @@ module.enable = function(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
this.rangeTimer = TOOLTIP_UPDATE_TIME
|
||||
end
|
||||
end
|
||||
end, true)
|
||||
|
||||
+2
-46
@@ -10,7 +10,6 @@ local GetExpansion = ShaguTweaks.GetExpansion
|
||||
local cmatch = ShaguTweaks.cmatch
|
||||
local rgbhex = ShaguTweaks.rgbhex
|
||||
local friendinfo = gsub(gsub(FRIENDS_LEVEL_TEMPLATE,"%%s","%%s %%s"),"%%d","%%s")
|
||||
local PLAYER_CACHE_MAX_AGE = 180 * 24 * 60 * 60
|
||||
|
||||
local function TouchPlayer(entry)
|
||||
if not entry then return end
|
||||
@@ -56,43 +55,6 @@ local function GetCachedFriendDisplay(data, name)
|
||||
return cname, clevel
|
||||
end
|
||||
|
||||
local function CleanupPlayerDB(playerdb, realmdb)
|
||||
if not time then return end
|
||||
|
||||
local now = time()
|
||||
local lastCleanup = tonumber(realmdb.players_cleanup) or 0
|
||||
|
||||
-- Start the 180-day grace period without touching existing entries.
|
||||
if lastCleanup == 0 then
|
||||
realmdb.players_cleanup = now
|
||||
return
|
||||
end
|
||||
|
||||
if now - lastCleanup < PLAYER_CACHE_MAX_AGE then return end
|
||||
|
||||
for name, data in pairs(playerdb) do
|
||||
if type(data) ~= "table" then
|
||||
playerdb[name] = nil
|
||||
else
|
||||
-- The shared cache also contains libunitscan-only entries. Prefer the
|
||||
-- numeric Social Colors timestamp, then the passive scanner timestamp.
|
||||
-- Entries without either timestamp have survived a full grace period and
|
||||
-- can safely expire just like the old per-character cache did.
|
||||
local lastSeen = type(data.lastseen) == "number" and data.lastseen or nil
|
||||
local lastSeenTs = tonumber(data.lastseen_ts)
|
||||
if lastSeenTs and (not lastSeen or lastSeenTs > lastSeen) then
|
||||
lastSeen = lastSeenTs
|
||||
end
|
||||
|
||||
if not lastSeen or now - lastSeen >= PLAYER_CACHE_MAX_AGE then
|
||||
playerdb[name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
realmdb.players_cleanup = now
|
||||
end
|
||||
|
||||
local module = ShaguTweaks:register({
|
||||
title = T["Social Colors"],
|
||||
description = T["Show class colors in Who, Guild, Friends and Chat."],
|
||||
@@ -182,16 +144,10 @@ module.enable = function(self)
|
||||
HookChatColors()
|
||||
|
||||
local socialmod = CreateFrame("Frame", "ShaguTweaksSocialMod", UIParent)
|
||||
socialmod:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
-- Cache expiry lives in libs/libunitscan.lua, which runs whether or not
|
||||
-- this module is enabled.
|
||||
socialmod:RegisterEvent("CHAT_MSG_SYSTEM")
|
||||
socialmod:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
local playerdb, realmdb = GetPlayerCache()
|
||||
CleanupPlayerDB(playerdb, realmdb)
|
||||
this:UnregisterEvent("PLAYER_ENTERING_WORLD")
|
||||
return
|
||||
end
|
||||
|
||||
local currentdb = GetPlayerCache()
|
||||
local name = cmatch(arg1, _G.ERR_FRIEND_ONLINE_SS) or cmatch(arg1, _G.ERR_FRIEND_OFFLINE_S)
|
||||
if name and currentdb[name] then
|
||||
|
||||
Reference in New Issue
Block a user