lotrointerface.com
Search Downloads

LoTROInterface SVN SequenceBars

[/] [trunk/] [Thurallor/] [Common/] [Utils/] [Watcher.lua] - Rev 212

Compare with Previous | Blame | View Log

-- Singleton object for efficiently observing and keeping track of game state,
-- providing convenient functions and events.

local Watcher = {};

AddCallback(Turbine.Plugin, "Unload", function()
--Puts("Saving...");
    -- Workaround for Turbine localization bug.
    local saveData = ExportTable(Watcher.settings);
    Turbine.PluginData.Save(Turbine.DataScope.Account, "Thurallor_GameInfo", saveData, function()
--Puts("Save complete.");
    end);
end);

local startupTime = Turbine.Engine.GetGameTime();

local function DoDelayedCallback(_time, event, args)
    if (not Watcher.delayedCallbacks) then
        Watcher.delayedCallbacks = {};
    end
    table.insert(Watcher.delayedCallbacks, { time = _time, event = event, args = args } );
    Watcher.window:SetWantsUpdates(true);
end

local function AddActorEffect(effectsObject, effect)
    local categories, name, curable = effect:GetCategory(), effect:GetName(), effect:IsCurable();

    -- Keep track of how many instances of this effect are currently on the character
    if (not effectsObject.cache[name]) then
        effectsObject.cache[name] = 0;
    end
    effectsObject.cache[name] = effectsObject.cache[name] + 1;

    -- Add the effect to "effect list" pulldown menus
    local knownEffects = Watcher.settings.knownEffects;
    if (not knownEffects[name]) then
        -- This is an effect we have never seen before.  Learn some info about it and save for future reference.
        knownEffects[name] = { categories, effect:GetIcon(), curable, effect:IsDebuff()};
    end

    -- Create efficient functions for bitwise category tracking
    local bits = categories;
    if (not Watcher.AddCategories[bits]) then
        -- This is a GetCategory value we have never seen before.  Create functions for adding/removing.
        local AddCategories = "local categories = ...; ";
        local RemoveCategories = "local categories = ...; ";
        if (bits >= 2048) then
            -- Unknown bits; discard them.
            bits = bits % 2048;
        end
        if (bits >= 1226) then
            -- Turbine.Gameplay.EffectCategory.Dispellable is the OR of several bits
            AddCategories = AddCategories .. "categories[1226] = categories[1226] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[1226] = categories[1226] - 1; ";
        end
        if (bits >= 1024) then
            AddCategories = AddCategories .. "categories[1024] = categories[1024] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[1024] = categories[1024] - 1; ";
            bits = bits - 1024;
        end
        if (bits >= 512) then
            AddCategories = AddCategories .. "categories[512] = categories[512] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[512] = categories[512] - 1; ";
            bits = bits - 512;
        end
        if (bits >= 256) then
            AddCategories = AddCategories .. "categories[256] = categories[256] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[256] = categories[256] - 1; ";
            bits = bits - 256;
        end
        if (bits >= 128) then
            AddCategories = AddCategories .. "categories[128] = categories[128] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[128] = categories[128] - 1; ";
            bits = bits - 128;
        end
        if (bits >= 64) then
            AddCategories = AddCategories .. "categories[64] = categories[64] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[64] = categories[64] - 1; ";
            bits = bits - 64;
        end
        if (bits >= 32) then
            AddCategories = AddCategories .. "categories[32] = categories[32] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[32] = categories[32] - 1; ";
            bits = bits - 32;
        end
        if (bits >= 16) then
            AddCategories = AddCategories .. "categories[16] = categories[16] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[16] = categories[16] - 1; ";
            bits = bits - 16;
        end
        if (bits >= 8) then
            AddCategories = AddCategories .. "categories[8] = categories[8] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[8] = categories[8] - 1; ";
            bits = bits - 8;
        end
        if (bits >= 4) then
            AddCategories = AddCategories .. "categories[4] = categories[4] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[4] = categories[4] - 1; ";
            bits = bits - 4;
        end
        if (bits >= 2) then
            AddCategories = AddCategories .. "categories[2] = categories[2] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[2] = categories[2] - 1; ";
            bits = bits - 2;
        end
        if (bits >= 1) then
            AddCategories = AddCategories .. "categories[1] = categories[1] + 1; ";
            RemoveCategories = RemoveCategories .. "categories[1] = categories[1] - 1; ";
        end

        Watcher.settings.funcs.AddCategories[categories] = AddCategories;
        Watcher.AddCategories[categories] = loadstring(AddCategories);
        Watcher.settings.funcs.RemoveCategories[categories] = RemoveCategories;
        Watcher.RemoveCategories[categories] = loadstring(RemoveCategories);
    end

    -- Increment/decrement bitwise category trackers
    Watcher.AddCategories[categories](effectsObject.activeCategories);
    if (curable) then
        Watcher.AddCategories[categories](effectsObject.activeCurableCategories);
    end
end

local function RemoveActorEffect(effectsObject, effect)
    local categories, name, curable = effect:GetCategory(), effect:GetName(), effect:IsCurable();

    -- Increment/decrement bitwise category trackers
    Watcher.RemoveCategories[categories](effectsObject.activeCategories);
    if (curable) then
        Watcher.RemoveCategories[categories](effectsObject.activeCurableCategories);
    end

    -- Keep track of how many instances of this effect are currently on the character
    effectsObject.cache[name] = effectsObject.cache[name] - 1;
    if (effectsObject.cache[name] == 0) then
        effectsObject.cache[name] = nil;
    end
end

local function GetActorEffects(effectsObject)
    effectsObject.cache = {};
    effectsObject.activeCategories = {
        [Turbine.Gameplay.EffectCategory.Dispellable] = 0;
        [Turbine.Gameplay.EffectCategory.Corruption] = 0;
        [Turbine.Gameplay.EffectCategory.Elemental] = 0;
        [Turbine.Gameplay.EffectCategory.Tactical] = 0;
        [Turbine.Gameplay.EffectCategory.Poison] = 0;
        [Turbine.Gameplay.EffectCategory.Fear] = 0;
        [Turbine.Gameplay.EffectCategory.Song] = 0;
        [Turbine.Gameplay.EffectCategory.Cry] = 0;
        [Turbine.Gameplay.EffectCategory.Wound] = 0;
        [Turbine.Gameplay.EffectCategory.Physical] = 0;
        [Turbine.Gameplay.EffectCategory.Disease] = 0;
        [1] = 0;
    };
    effectsObject.activeCurableCategories = {
        [Turbine.Gameplay.EffectCategory.Dispellable] = 0;
        [Turbine.Gameplay.EffectCategory.Corruption] = 0;
        [Turbine.Gameplay.EffectCategory.Elemental] = 0;
        [Turbine.Gameplay.EffectCategory.Tactical] = 0;
        [Turbine.Gameplay.EffectCategory.Poison] = 0;
        [Turbine.Gameplay.EffectCategory.Fear] = 0;
        [Turbine.Gameplay.EffectCategory.Song] = 0;
        [Turbine.Gameplay.EffectCategory.Cry] = 0;
        [Turbine.Gameplay.EffectCategory.Wound] = 0;
        [Turbine.Gameplay.EffectCategory.Physical] = 0;
        [Turbine.Gameplay.EffectCategory.Disease] = 0;
        [1] = 0;
    };
    for e = 1, effectsObject:GetCount(), 1 do
        local effect = effectsObject:Get(e);
        AddActorEffect(effectsObject, effect);
    end
end

local function GetEquipment()
    local eq = Watcher.playerEquipmentObject;
    eq.cache = { byName = {}; bySlot = {}; };
    for slot = 1, eq:GetSize() do
        local item = eq:GetItem(slot);
        if (item) then
            local name = item:GetName();
            eq.cache.byName[name] = { item, slot };
            eq.cache.bySlot[slot] = { item, name };
        end
    end
end

local function GetBackpack()
    local bp = Watcher.playerBackpackObject;
    bp.cache = { byName = {}; bySlot = {}; };
    for slot = 1, bp:GetSize() do
        local item = bp:GetItem(slot);
        if (item) then
            local name = item:GetName();
            if (not bp.cache.byName[name]) then
                bp.cache.byName[name] = { slot };
            else
                table.insert(bp.cache.byName[name], slot);
            end
            bp.cache.bySlot[slot] = { item, name };
        end
    end
    return bp.cache;
end

local function ResetTimeChanged(skill, args)
    DoCallbacks(self, "ResetTimeChanged", args);
end

local function StanceChanged()
    local stance = Watcher.playerClassAttributes:GetStance();
        if (stance ~= Watcher.stance) then
        -- Lua API bug workaround:  If invalid stance reported, keep the previous value.
        local validStance = {
            [Turbine.Gameplay.Class.Warden] = { 
                [Turbine.Gameplay.Attributes.WardenStance.Assailment] = true;
                [Turbine.Gameplay.Attributes.WardenStance.Determination] = true;
            };
            [Turbine.Gameplay.Class.Hunter] = { 
                [Turbine.Gameplay.Attributes.HunterStance.Endurance] = true;
                [Turbine.Gameplay.Attributes.HunterStance.Precision] = true;
                [Turbine.Gameplay.Attributes.HunterStance.Strength] = true;
            };
            [Turbine.Gameplay.Class.Minstrel] = { 
                [Turbine.Gameplay.Attributes.MinstrelStance.Melody] = true;
                [Turbine.Gameplay.Attributes.MinstrelStance.Dissonance] = true;
                [Turbine.Gameplay.Attributes.MinstrelStance.Resonance] = true;
            };
        };
        if (validStance[Watcher.playerObject:GetClass()][stance]) then
            Watcher.stance = stance;
            DoCallbacks(Watcher, "PlayerStanceChanged", { Stance = stance });
        end
    end
end

local function GetSkills(skills)
    skills.cache = { names = {}; byName = {}; byIcon = {} };
    local cache = skills.cache;
    for c = 1, skills:GetCount(), 1 do
        local skill = skills:GetItem(c);
        local skillInfo = skill:GetSkillInfo();
        local icon = skillInfo:GetIconImageID();
        local name = skillInfo:GetName();
        cache.byName[name] = skill;
        cache.byIcon[icon] = skill;
        table.insert(cache.names, name);
    end
    table.sort(cache.names);
    return skills.cache;
end

local function GetGambits()
    local gambits = Watcher.playerTrainedGambits;
    gambits.cache = { names = {}; byName = {}; byIcon = {} };
    if (Watcher.playerObject:GetClass() == Turbine.Gameplay.Class.Warden) then
        for c = 1, gambits:GetCount(), 1 do
            local skill = gambits:GetItem(c);
            local skillInfo = skill:GetSkillInfo();
            local icon = skillInfo:GetIconImageID();
            local name = skillInfo:GetName();
            gambits.cache.byName[name] = skill;
            gambits.cache.byIcon[icon] = skill;
        end
    end
    return gambits.cache;
end

local function GetParty()
    local party = Watcher.playerPartyObject;
    party.cache = { byName = {}; byNameUpper = {}; };
    for c = 1, party:GetMemberCount(), 1 do
        local member = party:GetMember(c);
        local name = member:GetName();
        party.cache.byName[name] = member;
        party.cache.byNameUpper[string.upper(name)] = member;
        member.num = c;
    end
    return party.cache;
end

local function PlayerEffectAdded(effectsObject, args)
    local effect = effectsObject:Get(args.Index);
    AddActorEffect(effectsObject, effect);
    DoCallbacks(Watcher, "PlayerEffectAdded", effect);
end

local function PlayerEffectRemoved(effectsObject, args)
    RemoveActorEffect(effectsObject, args.Effect);
    DoCallbacks(Watcher, "PlayerEffectRemoved", args.Effect);
end

local function PlayerEffectsCleared(effectsObject, args)
    GetActorEffects(effectsObject);
    DoCallbacks(Watcher, "PlayerEffectsCleared", args);
end

local function TargetEffectAdded(effectsObject, args)
    local effect = effectsObject:Get(args.Index);
    AddActorEffect(effectsObject, effect);
    DoCallbacks(Watcher, "TargetEffectAdded", effect);
end

function TargetEffectRemoved(effectsObject, args)
    RemoveActorEffect(effectsObject, args.Effect);
    DoCallbacks(Watcher, "TargetEffectRemoved", args.Effect);
end

local function TargetEffectsCleared(effectsObject, args)
    GetActorEffects(effectsObject);
    DoCallbacks(Watcher, "TargetEffectsCleared", args);
end

local function PartyMemberAdded(sender, args)
    -- Event does not work if you're in a raid (only a single fellowship)
    -- Event doesn't fire when the party is created (initial two members)
    -- rare occurrence, out-of-combat; efficiency not important
    GetParty();
end

local function PartyMemberRemoved(sender, args)
    -- Event does not work if you're in a raid (only a single fellowship)
    -- Event doesn't fire when the party is disbanded
    -- rare occurrence, out-of-combat; efficiency not important
    GetParty();
end

local function PartyAssistTargetAdded(sender, args)
    -- Event does not work if you're in a raid (only a single fellowship)
    -- rare occurrence, out-of-combat; efficiency not important
    GetParty();
end

local function PartyAssistTargetRemoved(sender, args)
    -- Event does not work if you're in a raid (only a single fellowship)
    -- rare occurrence, out-of-combat; efficiency not important
    GetParty();
end

local function PlayerPartyChanged(sender, args)
    Watcher.playerPartyObject = Watcher.playerObject:GetParty();
    if (Watcher.playerPartyObject) then
        AddCallback(Watcher.playerPartyObject, "MemberAdded", PartyMemberAdded);
        AddCallback(Watcher.playerPartyObject, "MemberRemoved", PartyMemberRemoved);
        AddCallback(Watcher.playerPartyObject, "AssistTargetAdded", PartyAssistTargetAdded);
        AddCallback(Watcher.playerPartyObject, "AssistTargetRemoved", PartyAssistTargetRemoved);
        GetParty();
    end
end

local function UpdatePlayerTarget()
    Watcher.targetIsPartyMember = nil;
    local target = Watcher.targetObject;

    if (target and target.GetBaseMaxMorale) then
        local targetName, targetMorale = target:GetName(), target:GetBaseMaxMorale();
        local pet = Watcher.playerObject:GetPet();
        local party = Watcher.playerPartyObject;

        -- Detect if the new target is the player's self.  If so, use the full LocalPlayer object
        -- instead of the Actor object returned by GetTarget().
        if (target:IsLocalPlayer()) then
            Watcher.targetObject = Watcher.playerObject;

        -- Detect if the new target is the player's pet.  If so, use the full Pet object instead of the
        -- Actor object returned by GetTarget().
        elseif (pet and (targetName == pet:GetName()) and (targetMorale == pet:GetBaseMaxMorale())) then
            Watcher.targetObject = pet;

        -- Detect if the new target is a member of the player's party.  If so, use the full Player object
        -- instead of the Actor object returned by GetTarget().
        elseif (party) then
            local member = party.cache.byName[targetName];
            if (member and (targetMorale == member:GetBaseMaxMorale())) then
                Watcher.targetIsPartyMember = member;
                Watcher.targetObject = member;
                -- Also save this for Watcher.GetNextPartyMember():
                Watcher.prevPartyMember = member;
            end
        end

    end

    -- Need to get the effects object now so it can start observing effects.
-- Target effects currently are too buggy to use.    
--[[
    if (Watcher.targetObject and Watcher.targetObject.GetEffects) then
        Watcher.targetEffectsObject = Watcher.targetObject:GetEffects();
    end
]]

    DoCallbacks(Watcher, "PlayerTargetChanged", Watcher.targetObject);
end

local function TargetsTargetChanged()
    Watcher.targetsTargetObject = nil;
    DoCallbacks(Watcher, "TargetsTargetChanged", Watcher.targetObject);
end

local function PlayerTargetChanged()
    if (Watcher.targetObject) then
        RemoveCallback(Watcher.targetObject, "TargetChanged", TargetsTargetChanged);
    end
    Watcher.targetObject = Watcher.playerObject:GetTarget();
    Watcher.targetEffectsObject = nil;
    Watcher.targetsTargetObject = nil;
    if (Watcher.targetObject) then
        AddCallback(Watcher.targetObject, "TargetChanged", TargetsTargetChanged);
    end

    -- Client may not immediately have information about the target.  When
    -- the info arrives, we'll get a BaseMaxPowerChanged event, which seems
    -- to occur near the end of the burst of events that will arrive.
    if (Watcher.targetObject and Watcher.targetObject.GetBaseMaxPower and (Watcher.targetObject:GetBaseMaxPower() == 0)) then
        Watcher.targetObject.BaseMaxPowerChanged = function()
            Watcher.targetObject.BaseMaxPowerChanged = nil;
            Watcher.targetObject = Watcher.playerObject:GetTarget();
            Watcher.targetEffectsObject = nil;
            UpdatePlayerTarget();
        end
    end
    
    UpdatePlayerTarget();
end

local function ItemEquipped(sender, args)
--Puts("ItemEquipped: args = " .. Serialize(args));    
    local slot, item, name = args.Index, args.Item, args.Item:GetName();
    local byName = Watcher.playerEquipmentObject.cache.byName;
    if (byName[name] == nil) then
        byName[name] = {};
    end
    table.insert(byName[name], item);
    table.insert(byName[name], slot);
    Watcher.playerEquipmentObject.cache.bySlot[slot] = { item, name };
    DoCallbacks(Watcher, "ItemEquipped", args);
end

local function ItemUnequipped(sender, args)
--Puts("ItemUnequipped: args = " .. Serialize(args));    
    local slot, name = args.Index, args.Item:GetName();
    Watcher.playerEquipmentObject.cache.bySlot[slot] = nil;
    local byName = Watcher.playerEquipmentObject.cache.byName;
    if (byName[name]) then
        table.remove(byName[name], 1);
        table.remove(byName[name], 1);
        if (#byName[name] == 0) then
            byName[name] = nil;
        end
    end
    DoCallbacks(Watcher, "ItemUnequipped", args);
end

local function ItemAdded(sender, args)
--Puts("ItemAdded: args = " .. Serialize(args));    
    -- occurs out-of-combat; efficiency not important
    Watcher.playerBackpackObject.cache = nil;
end

local function ItemRemoved(sender, args)
--Puts("ItemRemoved: args = " .. Serialize(args));    
    -- occurs out-of-combat; efficiency not important
    Watcher.playerBackpackObject.cache = nil;
end

local function ItemMoved(sender, args)
    -- occurs out-of-combat; efficiency not important
    Watcher.playerBackpackObject.cache = nil;
    DoCallbacks(Watcher, "ItemMoved", args);
end

local function BackpackSizeChanged(sender, args)
    -- rare occurrence, out-of-combat; efficiency not important
    Watcher.playerBackpackObject.cache = nil;
end

local function ChatReceived(sender, args)
    if (args.ChatType == Turbine.ChatType.Death) then
        if (string.match(args.Message, Watcher.PlayerIncapacitatedStr[1]) or 
            string.match(args.Message, Watcher.PlayerIncapacitatedStr[2])) then
            DoCallbacks(Watcher, "PlayerIncapacitated");
        elseif (string.match(args.Message, Watcher.PlayerRevivedStr[1]) or 
            string.match(args.Message, Watcher.PlayerRevivedStr[2])) then
            DoCallbacks(Watcher, "PlayerRevived");
        end
    elseif (args.ChatType == Turbine.ChatType.Advancement) then
        if (string.match(args.Message, Watcher.SkillsChangedStr)) then
            -- Trait line changed.  Reread skills lists.
            Watcher.playerTrainedSkills.cache = nil;
            Watcher.playerUntrainedSkills.cache = nil;
        elseif (string.match(args.Message, Watcher.TraitTreeChangedStr)) then
            DoDelayedCallback(Turbine.Engine.GetGameTime() + 1.5, "TraitTreeChanged", args);
        end
    end
end

local function SkillAdded(sender, args)
    -- rare occurrence, out-of-combat; efficiency not important
    GetSkills(Watcher.playerTrainedSkills);
    GetSkills(Watcher.playerUntrainedSkills);
end

local function SkillRemoved(sender, args)
    -- rare occurrence, out-of-combat; efficiency not important
    GetSkills(Watcher.playerTrainedSkills);
    GetSkills(Watcher.playerUntrainedSkills);
end

local function InCombatChanged()
        local inCombat = Watcher.playerObject:IsInCombat();
    if (inCombat) then
                collectgarbage("stop");
    else
                collectgarbage("restart");
    end
        DoCallbacks(Watcher, "InCombatChanged", { InCombat = inCombat });
end

local function LoadSettings()
--Puts("Loading...");
    Turbine.PluginData.Load(Turbine.DataScope.Account, "Thurallor_GameInfo", function(loadData, args)
        if (not loadData) then
            return;
        end
        
        -- Workaround for Turbine localization bug.
        local settings = ImportTable(loadData);
        if (not settings) then
            return;
        end
        
        -- Previously-saved settings override the defaults
        DeepTableCopy(settings, Watcher.settings);
        
        -- Compile loaded functions
        for categories, sourceCode in pairs(settings.funcs.AddCategories) do
            Watcher.AddCategories[categories] = loadstring(sourceCode);
        end
        for categories, sourceCode in pairs(settings.funcs.RemoveCategories) do
            Watcher.RemoveCategories[categories] = loadstring(sourceCode);
        end
        
        -- Lua API bug workaround: Target effects object will not contain valid
        -- data until a few ticks after plugin load.
        if (Watcher.targetEffectsObject) then
            Watcher.targetEffectsObject.cache = nil;
        end
--Puts("Load complete.");
    end);
end

function Watcher.GetTrainedSkillsInfo()
    local cache = Watcher.playerTrainedSkills.cache;
    if (not cache) then
        cache = GetSkills(Watcher.playerTrainedSkills);
    end
    return cache;
end

function Watcher.GetUntrainedSkillsInfo()
    local cache = Watcher.playerUntrainedSkills.cache;
    if (not cache) then
        cache = GetSkills(Watcher.playerUntrainedSkills);
    end
    return cache;
end

function Watcher.GetSkillByName(name)
    local cache = Watcher.playerTrainedSkills.cache;
    if (not cache) then
        cache = GetSkills(Watcher.playerTrainedSkills);
    end
    local skill = cache.byName[name];
    if (skill) then
        return skill;
    end
    local cache = Watcher.playerUntrainedSkills.cache;
    if (not cache) then
        cache = GetSkills(Watcher.playerUntrainedSkills);
    end
    return cache.byName[name];
end

-- accepts a list of names (aliases for the same skill)
function Watcher.GetSkillByNames(names)
    for _, name in pairs(names) do
        local skill = Watcher.GetSkillByName(name);
        if (skill) then
            return skill;
        end
    end
end

function Watcher.GetSkillsInfo(trainedSkillsFirst)
    local trainedSkillsCache = Watcher.GetTrainedSkillsInfo();
    local untrainedSkillsCache = Watcher.GetUntrainedSkillsInfo();
    if (#untrainedSkillsCache.names > 0) then
        local cache = { names = {}; byName = {}; byIcon = {} };
        for c in values({ trainedSkillsCache, untrainedSkillsCache }) do
            for k, v in pairs(c.byIcon) do
                cache.byIcon[k] = v;
            end
            for k, v in pairs(c.byName) do
                cache.byName[k] = v;
            end
            for k, v in pairs(c.names) do
                table.insert(cache.names, v);
            end
        end
        if (not trainedSkillsFirst) then
            table.sort(cache.names);
        end
        return cache;
    else
        return trainedSkillsCache;
    end
end

function Watcher.GetGambitsInfo()
    local cache = Watcher.playerTrainedGambits.cache;
    if (not cache) then
        cache = GetGambits();
    end
    for name in sorted_keys(cache.byName) do
        table.insert(cache.names, name);
    end
    return cache;
end

function Watcher.GetKnownEffectNames()
    local names = {};
    for name in sorted_keys(Watcher.settings.knownEffects) do
        table.insert(names, name);
    end
    return names;
end

-- If itemName is specified, the equipped item (if any) with the specified name is returned.
-- If itemSlot is specified, the equipped item (if any) at the specified slot is returned.
-- If neither is specified, a random equipped item (if any) is returned.
-- Returns slot, item (or nil).
function Watcher.GetEquippedItem(itemName, itemSlot)
    if ((not itemSlot) and (not itemName)) then
        for name, byName in pairs(Watcher.playerEquipmentObject.cache.byName) do
            local item, slot = unpack(byName);
            return slot, item;
        end
    elseif (not itemSlot) then
        local byName = Watcher.playerEquipmentObject.cache.byName[itemName];
        if (byName) then
            local item, slot = unpack(byName);
            return slot, item;
        end
    elseif (not itemName) then
        local bySlot = Watcher.playerEquipmentObject.cache.bySlot[itemSlot];
        if (bySlot) then
            local item, name = unpack(bySlot);
            return itemSlot, item;
        end
    else -- itemName and itemSlot specified
        local bySlot = Watcher.playerEquipmentObject.cache.bySlot[itemSlot];
        if (bySlot) then
            local item, name = unpack(bySlot);
            if (name == itemName) then
                return itemSlot, item;
            end
        end
    end
end

function Watcher.GetItemQuantity(itemName)
    local quantity = 0;
    local cache = Watcher.playerBackpackObject.cache or GetBackpack();
    local slots = cache.byName[itemName];
    if (slots) then
        for i, s in pairs(slots) do
            local item = Watcher.playerBackpackObject:GetItem(s);
            if (item) then
                quantity = quantity + item:GetQuantity();
            end
        end
    end
    return quantity;
end

-- Can accept either an iconID (number) or a skill name (string).
function Watcher.SkillReady(arg)
    local cache = Watcher.playerTrainedSkills.cache;
    if (not cache) then
        cache = GetSkills(Watcher.playerTrainedSkills);
    end
    if (type(arg) == "string") then
        skill = cache.byName[arg];
    elseif (type(arg) == "number") then
        skill = cache.byIcon[arg];
    end
--    return (skill and skill:IsUsable() and (skill:GetResetTime() == -1));
    return (skill and (skill:GetResetTime() == -1));
end

-- Can accept either an iconID (number) or a skill name (string).
function Watcher.SkillUsable(arg)
    local cache = Watcher.playerTrainedSkills.cache;
    if (not cache) then
        cache = GetSkills(Watcher.playerTrainedSkills);
    end
    if (type(arg) == "string") then
        skill = cache.byName[arg];
    elseif (type(arg) == "number") then
        skill = cache.byIcon[arg];
    end
    return (skill and skill:IsUsable());
end

-- Can accept either an iconID (number) or a skill name (string).
function Watcher.SkillTrained(arg)
    local cache, skill = Watcher.playerTrainedSkills.cache;
    if (not cache) then
        cache = GetSkills(Watcher.playerTrainedSkills);
    end
    if (type(arg) == "string") then
        skill = cache.byName[arg];
    elseif (type(arg) == "number") then
        skill = cache.byIcon[arg];
    end
    return (skill ~= nil);
end

local function WatchPlayerEffects()
    -- Start watching player effects.
    Watcher.playerEffectsObject = Watcher.playerObject:GetEffects();
    AddCallback(Watcher.playerEffectsObject, "EffectAdded", PlayerEffectAdded);
    AddCallback(Watcher.playerEffectsObject, "EffectRemoved", PlayerEffectRemoved);
    AddCallback(Watcher.playerEffectsObject, "EffectsCleared", PlayerEffectsCleared);
    GetActorEffects(Watcher.playerEffectsObject);
end

-- Returns whether effects of the specified category exist on the player.
-- If 'curable' is true, then this function returns whether there are curable effects of the specified category.
function Watcher.PlayerHasEffectCategory(category, curable)
    if (not Watcher.playerEffectsObject) then
        WatchPlayerEffects();
    end
    if (category) then
        if (curable) then
            return Watcher.playerEffectsObject.activeCurableCategories[category] > 0;
        else
            return Watcher.playerEffectsObject.activeCategories[category] > 0;
        end
    end
end

function Watcher.PlayerHasEffect(name)
    if (not Watcher.playerEffectsObject) then
        WatchPlayerEffects();
    end
    if (name) then
        return Watcher.playerEffectsObject.cache[name];
    end
end

local function WatchTargetEffects()
    -- Start watching target effects.
    if (Watcher.targetObject and Watcher.targetObject.GetEffects) then
        Watcher.targetEffectsObject = Watcher.targetObject:GetEffects();
        AddCallback(Watcher.targetEffectsObject, "EffectAdded", TargetEffectAdded);
        AddCallback(Watcher.targetEffectsObject, "EffectRemoved", TargetEffectRemoved);
        AddCallback(Watcher.targetEffectsObject, "EffectsCleared", TargetEffectsCleared);
        GetActorEffects(Watcher.targetEffectsObject);
    end
end

function Watcher.TargetHasEffectCategory(category)
    if (not Watcher.targetEffectsObject) then
        WatchTargetEffects();
    end
    if (Watcher.targetEffectsObject) then
        return Watcher.targetEffectsObject.activeCategories[category] > 0;
    end
end

function Watcher.TargetHasEffect(name)
    if (not Watcher.targetEffectsObject) then
        WatchTargetEffects();
    end
    if (Watcher.targetEffectsObject) then
        return Watcher.targetEffectsObject.cache[name];
    end
end

function Watcher.TargetIsPartyMember()
    return Watcher.targetIsPartyMember;
end

function Watcher.GetStance()
    return Watcher.stance;
end

function Watcher.GetParty()
    return Watcher.playerPartyObject;
end

function Watcher.GetPartyMemberByName(name, ignoreCase)
    if (Watcher.playerPartyObject) then
        -- Bug: MemberAdded, MemberRemoved events don't fire for raids (only fellowships)
        -- Therefore we have to reread the object every time, because we don't know if it's changed.
        GetParty(); -- workaround
        if (ignoreCase) then
            return Watcher.playerPartyObject.cache.byNameUpper[string.upper(name)];
        else
            return Watcher.playerPartyObject.cache.byName[name];
        end
    end
end

function Watcher.GetPartyAssistTarget(num)
    local party = Watcher.playerPartyObject;
    if (party and (num <= party:GetAssistTargetCount())) then
        return party:GetAssistTarget(num);
    end
end

-- Returns party members one after the other, in round-robin fashion
-- "prevMember" argument is an optional Player object
function Watcher.GetNextPartyMember(prevMember)
    if (prevMember) then
        Watcher.prevPartyMember = prevMember;
    else
        local party = Watcher.playerPartyObject;
        if (party) then
            local member = Watcher.prevPartyMember or party:GetMember(1);
            local num = (member and member.num) or 0;
            while true do
                num = num + 1;
                if (num > party:GetMemberCount()) then
                    num = 1;
                end
                member = party:GetMember(num);
                if (member) then
                    return member;
                end
            end
        end
    end
end

function Watcher.GetNextAssistTarget()
    local party = Watcher.playerPartyObject;
    if (party and (party:GetAssistTargetCount() > 0)) then
        local member = Watcher.prevPartyMember or party:GetMember(1);
        local num = (member and member.num) or 0;
        while true do
            num = num + 1;
            if (num > party:GetMemberCount()) then
                num = 1;
            end
            member = party:GetMember(num);
            if (member and party:IsAssistTarget(member)) then
                return member;
            end
        end
    end
end

function Watcher.GetPlayerTarget()
    return Watcher.targetObject;
end

function Watcher.GetTargetsTarget()
    -- If target's target not previously cached, get it now.
    if (not Watcher.targetsTargetObject and Watcher.targetObject and Watcher.targetObject.GetTarget) then
        Watcher.targetsTargetObject = Watcher.targetObject:GetTarget();
    end
    return Watcher.targetsTargetObject;
end

-- Saves the selected target, assigning the specified ID.
function Watcher.SaveTarget(id)
    Watcher.savedTargets[id] = Watcher.targetObject;
end

-- Returns the saved target with the given ID.
function Watcher.GetSavedTarget(id)
    local target = Watcher.savedTargets[id];
    if (target and target.GetName and target:GetName() and (target:GetName() ~= "")) then
        return target;
    end
end

function Watcher.TraitTreeChanged()
    -- Trait line changed.  Reread skills lists.
    Watcher.playerTrainedSkills.cache = nil;
    Watcher.playerUntrainedSkills.cache = nil;
end

-- In the absence of a BrawlerAttributes.GetMettle() function, we need to watch the "Battle Flow x" effects.
function Watcher.GetPlayerMettle()
    for n = 1, 9 do
        if (Watcher.PlayerHasEffect(Watcher.MettleEffectNames[n])) then
            return n;
        end
    end
    return 0;
end

local function Constructor()
    
    -- Default settings
    Watcher.settings = {};
    Watcher.settings.knownEffects = {};
    Watcher.settings.funcs = {};
    Watcher.settings.funcs.AddCategories = {};
    Watcher.settings.funcs.RemoveCategories = {};

    -- Optimized functions generated on-the-fly
    Watcher.AddCategories = {};
    Watcher.RemoveCategories = {};

    Watcher.savedTargets = {};
    
    -- Object for monitoring display updates:
    Watcher.window = Turbine.UI.Window();
    function Watcher.window:Update()
        self:SetWantsUpdates(false);
        if (Watcher.delayedCallbacks) then
            local notyet = {};
            for k, v in pairs(Watcher.delayedCallbacks) do
                if (Turbine.Engine.GetGameTime() >= v.time) then
                    DoCallbacks(Watcher, v.event, v.args);
                else
                    table.insert(notyet, v);
                end
            end
            Watcher.delayedCallbacks = nil;
            if (#notyet > 0) then
                Watcher.delayedCallbacks = notyet;
                self:SetWantsUpdates(true);
            end
        end
    end
    
    -- Object for monitoring player events
    Watcher.playerObject = Turbine.Gameplay.LocalPlayer:GetInstance();
    AddCallback(Watcher.playerObject, "InCombatChanged", InCombatChanged);
    AddCallback(Watcher.playerObject, "PartyChanged", PlayerPartyChanged);
    AddCallback(Watcher.playerObject, "TargetChanged", PlayerTargetChanged);
    PlayerPartyChanged();
    PlayerTargetChanged();
    
    Watcher.playerEquipmentObject = Watcher.playerObject:GetEquipment();
    AddCallback(Watcher.playerEquipmentObject, "ItemEquipped", ItemEquipped);
    AddCallback(Watcher.playerEquipmentObject, "ItemUnequipped", ItemUnequipped);
    GetEquipment();

    Watcher.playerBackpackObject = Watcher.playerObject:GetBackpack();
    AddCallback(Watcher.playerBackpackObject, "ItemAdded", ItemAdded);
    AddCallback(Watcher.playerBackpackObject, "ItemRemoved", ItemRemoved);
    AddCallback(Watcher.playerBackpackObject, "ItemMoved", ItemMoved);
    AddCallback(Watcher.playerBackpackObject, "SizeChanged", BackpackSizeChanged);
    GetBackpack();
    
    Watcher.playerTrainedSkills = Watcher.playerObject:GetTrainedSkills();
    AddCallback(Watcher.playerTrainedSkills, "SkillAdded", PlayerSkillAdded);
    AddCallback(Watcher.playerTrainedSkills, "SkillRemoved", PlayerSkillRemoved);
    GetSkills(Watcher.playerTrainedSkills);
    
    Watcher.playerUntrainedSkills = Watcher.playerObject:GetUntrainedSkills();
    GetSkills(Watcher.playerUntrainedSkills);
    
    Watcher.playerClassAttributes = Watcher.playerObject:GetClassAttributes();
    if (Watcher.playerObject:GetClass() == Turbine.Gameplay.Class.Warden) then
        Watcher.playerTrainedGambits = Watcher.playerClassAttributes:GetTrainedGambits();
    else
        Watcher.playerTrainedGambits = {};
    end
    
    -- Lua API bug workaround:
    -- Need to cache the GetStance() result, because it returns incorrect values
    -- after the user changes trait trees, until he selects a new stance.
    if (Watcher.playerClassAttributes.GetStance) then
        Watcher.stance = Watcher.playerClassAttributes:GetStance();
        Watcher.playerClassAttributes.StanceChanged = StanceChanged;
    end
    
    -- For monitoring chat messages
    AddCallback(Turbine.Chat, "Received", ChatReceived);
    local language = Turbine.Engine:GetLanguage();
    if ((language == Turbine.Language.EnglishGB) or (language == Turbine.Language.English)) then
        Watcher.SkillsChangedStr = "^You have acquired the .* skill%.";
        Watcher.TraitTreeChangedStr = "^You have acquired the Class Specialization Bonus Trait";
        Watcher.PlayerIncapacitatedStr = {
            " incapacitated you%.";
            "You have been incapacitated";
        }
        Watcher.PlayerRevivedStr = {
            "You have been revived";
            "You succumb to your wounds";
        }
        Watcher.MettleEffectNames = {
            "Battle Flow 1", "Battle Flow 2", "Battle Flow 3",
            "Battle Flow 4", "Battle Flow 5", "Battle Flow 6",
            "Battle Flow 7", "Battle Flow 8", "Battle Flow 9"
        };
    elseif (language == Turbine.Language.German) then
        Watcher.SkillsChangedStr = "^Ihr habt Euch die Fertigkeit .* angeeignet%.";
        Watcher.TraitTreeChangedStr = "^Ihr habt diese Bonus-Eigenschaft für Klassenspezialisierung erlangt";
        Watcher.PlayerIncapacitatedStr = {
            " hat Euch außer Gefecht gesetzt%.";
            "Ihr wurdet durch ein Missgeschick außer Gefecht gesetzt%.";
        }
        Watcher.PlayerRevivedStr = {
            "Ihr wurdet wiederbelebt%.";
            "Ihr erliegt Euren Verletzungen%.";
        }
        Watcher.MettleEffectNames = {
            "Kampffluss (Stufe 1)", "Kampffluss (Stufe 2)", "Kampffluss (Stufe 3)",
            "Kampffluss (Stufe 4)", "Kampffluss (Stufe 5)", "Kampffluss (Stufe 6)",
            "Kampffluss (Stufe 7)", "Kampffluss (Stufe 8)", "Kampffluss (Stufe 9)"
        };
    elseif (language == Turbine.Language.French) then
        Watcher.SkillsChangedStr = "^Vous avez acquis la compétence .*%.";
        Watcher.TraitTreeChangedStr = "^Vous avez obtenu le trait bonus de spécialisation de classe";
        Watcher.PlayerIncapacitatedStr = {
            " a réussi à vous mettre hors de combat%.";
            "Un incident vous a réduit à l'impuissance%.";
        }
        Watcher.PlayerRevivedStr = {
            "Vous revenez à la vie%.";
            "Vous avez succombé à vos blessures%.";
        }
        Watcher.MettleEffectNames = {
            "Flot de Bataille 1", "Flot de Bataille 2", "Flot de Bataille 3",
            "Flot de Bataille 4", "Flot de Bataille 5", "Flot de Bataille 6",
            "Flot de Bataille 7", "Flot de Bataille 8", "Flot de Bataille 9"
        };
    elseif (language == Turbine.Language.Russian) then
        Watcher.SkillsChangedStr = "^Вы приобрели умение <%.";
        Watcher.TraitTreeChangedStr = "^Получен талант специализации";
        Watcher.PlayerIncapacitatedStr = {
            " наносит вам решающий удар%.";
            -- incapacitated you.\n
            "Вы окончательно пали духом%.";
            --You have been incapacitated. You cannot retreat until the fight has ended.
            --You have been incapacitated. Do you want to retreat from battle? You will automatically retreat in
            "Монстр-игрок лишил вас боевого духа%.";
            --You have been incapacitated by a monster player. Do you want to retreat from battle? You will automatically retreat in
            "От этого несчастного случая вы пали духом%.";
            --You have been incapacitated by misadventure.\n
        }
        Watcher.PlayerRevivedStr = {
            "Боевой дух к вам вернулся%.";
            --You have been revived.\n
            "Боевой дух покидает вас%.";
            --You succumb to your wounds. \n
        }
        Watcher.MettleEffectNames = {
            "Боевой настрой 1", "Боевой настрой 2", "Боевой настрой 3",
            "Боевой настрой 4", "Боевой настрой 5", "Боевой настрой 6",
            "Боевой настрой 7", "Боевой настрой 8", "Боевой настрой 9"
        };        
    end
end


-- Create single instance and load saved settings (if any).
Constructor();
LoadSettings();


--t = Turbine.UI.Window();
--t:SetVisible(true);
--t:SetBackColor(Turbine.UI.Color.Red);
--t:SetMouseVisible(true);
--function t:MouseClick()
--    Puts("cache = " .. PrettyPrint(Watcher.playerEffectsObject.cache, ""));
--end

Thurallor = Thurallor or {};
Thurallor.Utils = Thurallor.Utils or {};
Thurallor.Utils.Watcher = Watcher;

Compare with Previous | Blame


All times are GMT -5. The time now is 09:58 PM.


Our Network
EQInterface | EQ2Interface | Minion | WoWInterface | ESOUI | LoTROInterface | MMOUI | Swtorui