-- Singleton object for efficiently observing and keeping track of game state,
-- providing convenient functions and events.
local Watcher = {};
Thurallor.Utils.Watcher = Watcher;
local function DoSave()
--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 SaveSettings()
if (not Watcher.settingsLoaded) then
if (Turbine.Engine.GetGameTime() - startupTime > 5) then
-- Settings file probably doesn't exist if it hasn't loaded in 5 seconds.
Watcher.settingsLoaded = true;
else
return;
end
end
-- Do the save operation at the next Update; thus aggregating multiple consecutive save requests.
if (not Watcher.saveRequested) then
Watcher.saveRequested = true;
Watcher.window:SetWantsUpdates(true);
end
end
local function AddActorEffect(effectsObject, effect)
local categories, name = effect:GetCategory(), effect:GetName();
if (not effectsObject.cache[name]) then
effectsObject.cache[name] = 0;
end
effectsObject.cache[name] = effectsObject.cache[name] + 1;
if (not Watcher.settings.knownEffects[name]) then
-- This is an effect we have never seen before. Learn some info about it and save for future reference.
categories = effect:GetCategory();
Watcher.settings.knownEffects[name] = {
categories, effect:GetIcon(), effect:IsCurable(), effect:IsDebuff()};
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
SaveSettings();
else
categories = Watcher.settings.knownEffects[name][1];
end
Watcher.AddCategories[categories](effectsObject.activeCategories);
end
local function RemoveActorEffect(effectsObject, effect)
local categories, name = effect:GetCategory(), effect:GetName();
Watcher.RemoveCategories[categories](effectsObject.activeCategories);
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;
};
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:GetItemInfo():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:GetItemInfo():GetName();
bp.cache.byName[name] = { item, slot };
bp.cache.bySlot[slot] = { item, name };
end
end
end
local function ResetTimeChanged(skill, args)
DoCallbacks(self, "ResetTimeChanged", args);
end
local function GetSkills()
local skills = Watcher.playerTrainedSkills;
skills.cache = { names = {}; byName = {}; byIcon = {} };
for c = 1, skills:GetCount(), 1 do
local skill = skills:GetItem(c);
-- AddCallback(skill, "ResetTimeChanged", ResetTimeChanged);
local skillInfo = skill:GetSkillInfo();
local icon = skillInfo:GetIconImageID();
local name = skillInfo:GetName();
skills.cache.byName[name] = skill;
skills.cache.byIcon[icon] = skill;
end
return skills.cache;
end
local function GetParty()
local party = Watcher.playerPartyObject;
party.cache = { byName = {}; };
for c = 1, party:GetMemberCount(), 1 do
local member = party:GetMember(c);
local name = member:GetName();
party.cache.byName[name] = member;
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 PlayerPartyChanged(sender, args)
Watcher.playerPartyObject = Watcher.playerObject:GetParty();
if (Watcher.playerPartyObject) then
AddCallback(Watcher.playerPartyObject, "MemberAdded", PartyMemberAdded);
AddCallback(Watcher.playerPartyObject, "MemberRemoved", PartyMemberRemoved);
GetParty();
end
end
local function PlayerTargetChanged()
Watcher.targetObject = Watcher.playerObject:GetTarget();
Watcher.targetEffectsObject = nil;
-- Bug workaround:
-- If the target is a member of the player's party, GetEffects() won't work
-- unless we use the Party object to get the Player object.
if (Watcher.targetObject and Watcher.playerPartyObject) then
local name = Watcher.targetObject:GetName();
local member = Watcher.playerPartyObject.cache.byName[name];
if (member) then
Watcher.targetObject = member;
end
end
-- Need to get the effects object now so it can start observing effects.
if (Watcher.targetObject and Watcher.targetObject.GetEffects) then
Watcher.targetEffectsObject = Watcher.targetObject:GetEffects();
end
end
local function ItemEquipped(sender, args)
--Puts("ItemEquipped: args = " .. Serialize(args));
local slot, item, name = args.Index, args.Item, args.Item:GetItemInfo():GetName();
Watcher.playerEquipmentObject.cache.byName[name] = { item, 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:GetItemInfo():GetName();
Watcher.playerEquipmentObject.cache.bySlot[slot] = nil;
Watcher.playerEquipmentObject.cache.byName[name] = nil;
DoCallbacks(Watcher, "ItemUnequipped", args);
end
local function ItemAdded(sender, args)
--Puts("ItemAdded: args = " .. Serialize(args));
local slot, item, name = args.Index, args.Item, args.Item:GetItemInfo():GetName();
--Puts("Item " .. name .. " added in slot " .. slot);
Watcher.playerBackpackObject.cache.byName[name] = { item, slot };
Watcher.playerBackpackObject.cache.bySlot[slot] = { item, name };
end
local function ItemRemoved(sender, args)
--Puts("ItemRemoved: args = " .. Serialize(args));
local cache = Watcher.playerBackpackObject.cache;
local slot, name = args.Index, cache.bySlot[args.Index][2];
--Puts("Item " .. name .. " removed from slot " .. slot);
cache.bySlot[slot] = nil;
cache.byName[name] = nil;
end
local function ItemMoved(sender, args)
--Puts("ItemMoved: args = " .. Serialize(args));
local cache = Watcher.playerBackpackObject.cache;
local oldSlot, newSlot = args.OldIndex, args.NewIndex;
if (not args.Item) then
-- Spurious ItemMoved event that sometimes occurs when unequipping an item
return;
end
--Puts("Item " .. cache.bySlot[oldIndex][2] .. " moved from slot " .. oldSlot .. " to slot " .. newSlot);
local item, name = unpack(cache.bySlot[oldSlot]);
cache.bySlot[newSlot] = { item, name };
cache.bySlot[oldSlot] = nil;
DoCallbacks(Watcher, "ItemMoved", args);
end
local function SkillAdded(sender, args)
-- rare occurrence, out-of-combat; efficiency not important
GetSkills();
end
local function SkillRemoved(sender, args)
-- rare occurrence, out-of-combat; efficiency not important
GetSkills();
end
local function PartyMemberAdded(sender, args)
-- rare occurrence, out-of-combat; efficiency not important
GetParty();
end
local function PartyMemberRemoved(sender, args)
-- rare occurrence, out-of-combat; efficiency not important
GetParty();
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
-- Allow save operations to proceed.
Watcher.settingsLoaded = true;
SaveSettings();
--Puts("Load complete.");
end);
end
function Watcher.GetSkillsInfo()
local cache = Watcher.playerTrainedSkills.cache;
if (not cache) then
cache = GetSkills();
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.SkillReady(iconID)
local cache = Watcher.playerTrainedSkills.cache;
if (not cache) then
cache = GetSkills();
end
local skill = cache.byIcon[iconID];
-- return (skill and skill:IsUsable() and (skill:GetResetTime() == -1));
return (skill and (skill:GetResetTime() == -1));
end
local function PlayerHasEffectCategory(category)
if (category) then
return Watcher.playerEffectsObject.activeCategories[category] > 0;
end
end
function Watcher.PlayerHasEffectCategory(category)
-- 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);
-- Next time this function is called, we don't need to do the above again.
Watcher.PlayerHasEffectCategory = PlayerHasEffectCategory;
Watcher.PlayerHasEffect = PlayerHasEffect;
return PlayerHasEffectCategory(category);
end
local function PlayerHasEffect(name)
if (name) then
return Watcher.playerEffectsObject.cache[name];
end
end
function Watcher.PlayerHasEffect(name)
-- 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);
-- Next time this function is called, we don't need to do the above again.
Watcher.PlayerHasEffectCategory = PlayerHasEffectCategory;
Watcher.PlayerHasEffect = PlayerHasEffect;
return PlayerHasEffect(name);
end
function Watcher.TargetHasEffectCategory(category)
-- Target effects tracking is so buggy as to be useless. Hoping Turbine addresses this bug soon.
if (true) then
return false;
end
if (not Watcher.targetEffectsObject) then
-- No target, or target isn't one whose effects can be read.
return false;
end
if (not Watcher.targetEffectsObject.cache) then
-- Bug workaround: Since EffectRemoved event never fires for target
-- effects, we must reread the list every time it is queried.
GetActorEffects(Watcher.targetEffectsObject);
Watcher.window:SetWantsUpdates(true); -- to delete the cache at next Update cycle
end
return Watcher.targetEffectsObject.activeCategories[category] > 0;
end
function Watcher.TargetHasEffect(name)
-- Target effects tracking is so buggy as to be useless. Hoping Turbine addresses this bug soon.
if (true) then
return false;
end
if (not Watcher.targetEffectsObject) then
-- No target, or target isn't one whose effects can be read.
return false;
end
if (not Watcher.targetEffectsObject.cache) then
-- Bug workaround: Since EffectRemoved event never fires for target
-- effects, we must reread the list every time it is queried.
GetActorEffects(Watcher.targetEffectsObject);
Watcher.window:SetWantsUpdates(true); -- to delete the cache at next Update cycle
end
return Watcher.targetEffectsObject.cache[name];
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 = {};
-- Object for monitoring display updates:
Watcher.window = Turbine.UI.Window();
function Watcher.window:Update()
if (Watcher.saveRequested) then
DoSave();
Watcher.saveRequested = false;
end
if (Watcher.targetEffectsObject) then
Watcher.targetEffectsObject.cache = nil;
end
self:SetWantsUpdates(false);
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", GetBackpack);
GetBackpack();
Watcher.playerTrainedSkills = Watcher.playerObject:GetTrainedSkills();
AddCallback(Watcher.playerTrainedSkills, "SkillAdded", PlayerSkillAdded);
AddCallback(Watcher.playerTrainedSkills, "SkillRemoved", PlayerSkillRemoved);
GetSkills();
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