-- Singleton object for efficiently observing and keeping track of game state,
-- providing convenient functions and events.
local Watcher = {};
Thurallor.Utils.Watcher = Watcher;
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
--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 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
-- Allow save operations to proceed.
Watcher.settingsLoaded = true;
SaveSettings();
--Puts("Load complete.");
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 EffectAdded(effectsObject, args)
AddActorEffect(effectsObject, effectsObject:Get(args.Index));
end
local function EffectRemoved(effectsObject, args)
RemoveActorEffect(effectsObject, args.Effect);
end
local function EffectsCleared(effectsObject, args)
GetActorEffects(effectsObject);
end
local function PlayerTargetChanged()
Watcher.targetObject = Watcher.playerObject:GetTarget();
if (Watcher.targetObject and Watcher.targetObject.GetEffects) then
Watcher.targetEffectsObject = Watcher.targetObject:GetEffects();
AddCallback(Watcher.targetEffectsObject, "EffectAdded", EffectAdded);
AddCallback(Watcher.targetEffectsObject, "EffectRemoved", EffectRemoved);
AddCallback(Watcher.targetEffectsObject, "EffectsCleared", EffectsCleared);
GetActorEffects(Watcher.targetEffectsObject);
else
Watcher.targetEffectsObject = nil;
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; efficiency not important
GetSkills();
end
local function SkillRemoved(sender, args)
-- rare occurrence; efficiency not important
GetSkills();
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));
end
function Watcher.PlayerHasEffectCategory(category)
if (category) then
return Watcher.playerEffectsObject.activeCategories[category] > 0;
end
return false;
end
function Watcher.PlayerHasEffect(name)
return Watcher.playerEffectsObject.cache[name];
end
function Watcher.TargetHasEffectCategory(category)
if (Watcher.targetEffectsObject and category) then
return Watcher.targetEffectsObject.activeCategories[category] > 0;
end
return false;
end
function Watcher.TargetHasEffect(name)
return (Watcher.targetEffectsObject and 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();
-- Watcher.window:SetWantsUpdates(true);
-- Object for monitoring player events
Watcher.playerObject = Turbine.Gameplay.LocalPlayer:GetInstance();
AddCallback(Watcher.playerObject, "TargetChanged", PlayerTargetChanged);
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.playerEffectsObject = Watcher.playerObject:GetEffects();
AddCallback(Watcher.playerEffectsObject, "EffectAdded", EffectAdded);
AddCallback(Watcher.playerEffectsObject, "EffectRemoved", EffectRemoved);
AddCallback(Watcher.playerEffectsObject, "EffectsCleared", EffectsCleared);
GetActorEffects(Watcher.playerEffectsObject);
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();