Window = class(Turbine.UI.Lotro.Window);
local importPath = getfenv(1)._.Name;
local imagePath = string.gsub(string.gsub(importPath, "%.Window$", ""), "%.", "/") .. "/Images/";
function Window:Constructor(plugin)
Turbine.UI.Lotro.Window.Constructor(self);
self:SetResizable(true);
self:SetMinimumSize(200, 150);
self.plugin = plugin;
self:SetText(L:GetText("/Defaults/WindowTitle"));
local b = Thurallor.UI.RoundButton();
self.newEventButton = b;
b:SetParent(self);
b:SetBackground(imagePath .. "plus.tga");
b:SetHighlightedBackground(imagePath .. "plus_highlight.tga");
b:SetMouseDownBackground(imagePath .. "plus_click.tga");
Thurallor.UI.Tooltip(function() return L:GetText("/MainWindow/AddNewReminder"); end):Attach(b);
b.Click = function() self:CreateNewEvent() end;
local b = Thurallor.UI.RoundButton();
self.scopeButton = b;
b:SetParent(self);
self.scopeTooltip = Thurallor.UI.Tooltip(function() return L:GetText("/MainWindow/CurrentScope/" .. self.settings.viewingScope); end);
self.scopeTooltip:Attach(b);
b.Click = function() self:NextScope() end;
b = Thurallor.UI.RoundButton();
self.settingsButton = b;
b:SetParent(self);
b:SetBackground(0x41101833);
b:SetDisabledBackground(0x41101833);
b:SetHighlightedBackground(0x41101832);
b:SetMouseDownBackground(0x41101834);
Thurallor.UI.Tooltip(function() return L:GetText("/MainWindow/OpenOptionsPanel"); end):Attach(b);
b.Click = function() self:ShowOptionsPanel() end;
-- Display a message "(no tabs displayed)" when all tabs are hidden
label = Turbine.UI.Label();
self.noTabsDisplayed = label;
label:SetSize(self);
label:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter);
label:SetForeColor(Turbine.UI.Color.PaleGoldenrod);
label:SetBackColor(Turbine.UI.Color.Black);
label:SetFontStyle(Turbine.UI.FontStyle.Outline);
label:SetOutlineColor(Turbine.UI.Color.Black);
label:SetFont(Turbine.UI.Lotro.Font.Verdana16);
label:SetText(L:GetText("/MainWindow/NoTabsDisplayed"));
label:SetPosition(14, 55);
label:SetMouseVisible(false);
-- Hide window border when mouse is absent
self.backgroundHider = Turbine.UI.Control();
self.backgroundHider:SetParent(self);
self.backgroundHider:SetBackColor(Turbine.UI.Color(0, 0, 0, 0));
self.backgroundHider:SetSize(0, 0);
self.MouseClick = function()
self:Deselect();
end
self:CreateIcon();
self.player = Turbine.Gameplay.LocalPlayer.GetInstance();
self.alerts = {};
self.shimmering = Shimmering();
self.shimmering:SetWantsUpdates(true);
local gameTime = Turbine.Engine.GetGameTime();
self.tickFuncs = {}; -- for event:Tick() functions
self.numTickFuncs = 0;
self.loadTime = gameTime;
self.lastTick = gameTime;
self.nextServerResetTime = Time():GetNextServerResetTime(true):GetGameTime();
self.lastSave = gameTime;
self.chatPaused = true;
self.chatQueue = {};
self.itemRemovedQueue = {};
self.recipes = {
loaded = false;
cooldown = {};
profession = {};
}
self.serverID = Turbine.Engine.GetServerID();
self.charID = self.serverID .. "." .. self.player:GetName();
self.hudVisible = true;
end
function Window:Localize()
_G.PutsPrefix = L:GetText("/PutsPrefix");
self:RegisterShellCommand();
self.icon:Localize();
self:SetText(L:GetText("/Defaults/WindowTitle"));
self:RebuildTabs(true);
end
function Window:ChangeLanguage(oldLanguage, newLanguage)
if (oldLanguage == newLanguage) then
return;
end
-- Rename any categories with default names
local prevContext = L:SetContext("/Defaults/Categories");
local oldCatNames, newCatNames = {}, {};
for _, cat in pairs(L:GetItems()) do
oldCatNames[cat] = L:GetText(cat);
end
L:SetLanguage(newLanguage);
for _, cat in pairs(L:GetItems()) do
newCatNames[cat] = L:GetText(cat);
end
L:SetContext(prevContext);
for cat, oldName in pairs(oldCatNames) do
local newName = newCatNames[cat];
self:RenameCategory(oldName, newName);
end
self.settings.language = newLanguage;
self:SaveSettings();
end
function Window:SetOpacity(opacity)
Turbine.UI.Window.SetOpacity(self, opacity);
for _, tab in pairs(self.tabsByName) do
tab:SetOpacity(opacity);
end
end
function Window:SetVisible(visible)
if (visible and not self.visible) then
EnsureOnScreen(self, true);
end
self.visible = visible;
Turbine.UI.Lotro.Window.SetVisible(self, visible and self.hudVisible);
end
function Window:IsVisible()
return self.visible;
end
function Window:ShellCommand(cmd, args)
--Puts(Serialize({cmd = cmd; args = args}));
subcmd, remainder = string.match(args, "^ *([^ ]+) +(.*)$");
if (subcmd == L:GetText("/ShellCommand/SetLoc")) then
self.location = string.gsub(remainder, "\n", ": ");
if (self.LocationChanged) then
-- Can only be one such callback at a time, for simplicity
self.LocationChanged();
end
return true;
elseif (subcmd == L:GetText("/ShellCommand/Toggle")) then
if (remainder == L:GetText("/ShellCommand/Window")) then
self:SetVisible(not self:IsVisible());
self:SaveSettings();
return true;
elseif (remainder == L:GetText("/ShellCommand/Icon")) then
local visible = not self.icon:IsVisible();
self.icon:SetVisible(visible);
self.settings.icon.hidden = not visible;
self:SaveSettings();
return true;
else
return false;
end
end
return false;
end
function Window:RegisterShellCommand()
if (self.shellCmd) then
Turbine.Shell.RemoveCommand(self.shellCmd);
end
self.shellCmd = Turbine.ShellCommand();
self.shellCmd.window = self;
function self.shellCmd:Execute(cmd, args)
if ((args == L:GetText("/ShellCommand/Help")) or (not self.window:ShellCommand(cmd, args))) then
Turbine.Shell.WriteLine(self:GetHelp());
end
end
self.shellCmd.GetHelp = function()
return L:GetText("/ShellCommand/LongHelp");
end
self.shellCmd.GetShortHelp = function()
return L:GetText("/ShellCommand/ShortHelp");
end
Turbine.Shell.AddCommand(L:GetText("/ShellCommand/Command"), self.shellCmd);
end
function Window:CreateIcon()
self.icon = Icon(self);
self.icon:SetTooltip(function() return self:GetIconTooltip() end);
local function saveIconSettings()
self.settings.icon.position = {self.icon:GetFractionalPosition()};
self:SaveSettings();
end
AddCallback(self.icon, "IconMoved", saveIconSettings);
end
function Window:SetIconSize(size)
self.settings.icon.size = { size, size };
self.icon:SetSize(unpack(self.settings.icon.size));
self.icon:SetFractionalPosition(unpack(self.settings.icon.position));
self:SaveSettings();
end
function Window:IconStateChanged(state)
if (state == "icon") then
-- Window is being minimized
self.shimmering:SetWantsUpdates(false);
for name, tab in pairs(self.tabsByName) do
if (tab.table.marquee) then
tab.table.marquee:SetWantsUpdates(false);
end
end
else -- (state == "window")
-- Window is being restored
self.shimmering:SetWantsUpdates(true);
for name, tab in pairs(self.tabsByName) do
if (tab.table.marquee) then
tab.table.marquee:SetWantsUpdates(true);
end
end
end
end
function Window:LoadSettings()
--Puts("Loading settings...");
-- Check for (obsolete) server-specific settings
local serverSettings, accountSettings;
Turbine.PluginData.Load(Turbine.DataScope.Server, "Reminders", function(serverData)
if (serverData) then
serverSettings = ImportTable(serverData);
end
end);
Turbine.PluginData.Load(Turbine.DataScope.Account, "Reminders", function(accountData)
if (accountData) then
-- Save backup
accountData.backupTime = "#" .. Turbine.Engine.GetGameTime();
local dateInfo = Turbine.Engine.GetDate();
Turbine.PluginData.Save(Turbine.DataScope.Account, "Reminders_backup" .. dateInfo.DayOfWeek, accountData);
-- Workaround for Turbine localization bug
accountSettings = ImportTable(accountData);
end
end);
if (serverSettings) then
-- Upgraded the plugin from a version prior to 2.00. Need to migrate to new settings file format.
return MigrateDialog(self, accountSettings, serverSettings);
elseif (not Turbine.Engine.GetServerName()) then
-- First time the plugin has run on this server. Need to get the server name from the user.
MigrateDialog(self, nil, nil);
end
self.settings = accountSettings or {};
self.settings = self:UpdateSettings(self.settings);
self:SetSize(unpack(self.settings.size));
self:SetPosition(unpack(self.settings.position));
self.icon:SetSize(unpack(self.settings.icon.size));
self.icon:SetFractionalPosition(unpack(self.settings.icon.position));
self.icon:SetMouseLeaveOpacity(self.settings.icon.mouseLeaveOpacity);
self.icon:SetVisible(not self.settings.icon.hidden);
self.icon:SetZOrder(self.settings.icon.zOrder);
if (self.settings.openWindowAtLogin) then
self:SetVisible(true);
self:Activate();
end
self:SetZoomEffectsEnabled(self.settings.zoomEffects);
-- If first login on this character, add character name to list
if (not self.settings.characters[self.charID]) then
self.settings.characters[self.charID] = self:UpdateCharSettings({});
end
self.charSettings = self:UpdateCharSettings(self.settings.characters[self.charID]);
-- If no tabs exist, create a default one
if (#self.settings.tabs == 0) then
table.insert(self.settings.tabs, { name = self.player:GetName() });
end
-- Register to receive combat state changes and chat messages
AddCallback(self.player, "InCombatChanged", function()
self:InCombatChanged();
end);
self.chatCallback = self:CreateChatParser();
AddCallback(Turbine.Chat, "Received", self.chatCallback);
AddCallback(L, "LanguageChanged", function()
self:Localize(false); -- also calls self:RebuildTabs();
if ((L:GetLanguage() ~= Turbine.Language.English) and self.settings.showTranslationMarkers) then
Puts(L:GetText("/TranslationHelpWanted"));
end
end);
self:RegisterShellCommand();
self.optionsPanel = OptionsPanel(self, self.settings);
L:SetShowMarkers(self.settings.showTranslationMarkers);
L:SetLanguage(self.settings.language); -- results in a call to self:RebuildTabs();
-- Start chat parsing, updates, etc.
self:InCombatChanged();
end
-- Fill in any new settings that have been added if the plugin has been updated
function Window:UpdateSettings(settings)
settings.language = settings.language or L:GetLanguage();
if (settings.showTranslationMarkers == nil) then
settings.showTranslationMarkers = true;
end
settings.tabs = settings.tabs or {};
settings.size = settings.size or { 600, 150 };
settings.optionsPanel = settings.optionsPanel or {
size = { 600, 600 };
position = { 50, 50 };
};
settings.questRepeatedDialog = settings.questRepeatedDialog or {};
settings.raidLockDialog = settings.raidLockDialog or {};
settings.position = settings.position or { 100, 100 };
settings.characters = settings.characters or {};
local prevContext = L:SetContext("/Defaults/Categories");
settings.categories = settings.categories or {
{ name = L:GetText("Other"); color = { 1, 192/255, 192/255, 192/255 } }; -- Silver
{ name = L:GetText("Auctions"); color = { 1, 128/255, 0, 0 } }; -- Maroon
{ name = L:GetText("Crafting"); color = { 1, 0, 184/255, 92/255 } }; -- Greenish
{ name = L:GetText("Crates"); color = { 1, 0, 0, 255/255 } }; -- Blue
{ name = L:GetText("Instances"); color = { 1, 255/255, 69/255, 0 } }; -- Orange Red
{ name = L:GetText("Quests"); color = { 1, 0, 206/255, 209/255 } }; -- Dark Turquoise
{ name = L:GetText("Tasks"); color = { 1, 162/255, 105/255, 0 } }; -- Brown
};
settings.defaultCategory = settings.defaultCategory or L:GetText("Other");
L:SetContext(prevContext);
if (not settings.defaultDelay) then
settings.defaultDelay = 0;
settings.defaultDelayMethod = "atNextLogin";
elseif (not settings.defaultDelayMethod) then
self.settings.defaultDelay = self.settings.defaultDelay * 60 * 60; -- in previous versions, stored as a number of hours
self.settings.defaultDelayMethod = "delayFromNow";
end
if (settings.openWindowAtLogin == nil) then
settings.openWindowAtLogin = false;
end
if (settings.zoomEffects == nil) then
settings.zoomEffects = true;
end
settings.icon = settings.icon or { size = { 64, 64 }; position = { 0.5, 0.5 } };
settings.icon.mouseLeaveOpacity = settings.icon.mouseLeaveOpacity or 0.8;
if (self.settings.icon.zOrder == nil) then
self.settings.icon.zOrder = 1;
end
settings.mouseLeaveWindowOpacity = settings.mouseLeaveWindowOpacity or 0.8;
settings.mouseLeaveHideControls = (settings.mouseLeaveHideControls == true);
settings.postpone = settings.postpone or {
method = "AtNextLogin";
delay = 0;
};
settings.postponeMethod = nil; -- obsolete
settings.postponeDelay = nil; -- obsolete
settings.allowNonemptyTabsDeletion = nil; --obsolete
if (settings.pauseDuringCombat == nil) then
settings.pauseDuringCombat = true;
end
if (settings.reuseExpiredReminders == nil) then
settings.reuseExpiredReminders = true;
end
if (settings.showPopupNotifications ~= nil) then
for n, tabSettings in pairs(settings.tabs) do
tabSettings.showPopupNotifications = settings.showPopupNotifications;
end
settings.showPopupNotifications = nil; -- obsolete
end
if (settings.showCascade == nil) then
settings.showCascade = true;
end
if (settings.showNegativeTimes == nil) then
settings.showNegativeTimes = false;
end
if (settings.askBeforeDeleting == nil) then
settings.askBeforeDeleting = true;
end
if (settings.viewingScope == nil) then
settings.viewingScope = "account";
end
self.settings.localTimeOffset = self.settings.localTimeOffset or 0.0;
if (settings.marqueeAntsEnabled == nil) then
settings.marqueeAntsEnabled = true;
end
self:SetLocalTimeOffset(self.settings.localTimeOffset);
-- The gameTime clock differs between servers and can even differ day-to-day. But localTime is predictable.
-- Readjust all expTime fields based on the current (localTime - gameTime) offset.
local gameTimeZero = Turbine.Engine.GetLocalTime() - Turbine.Engine.GetGameTime();
self.settings.gameTimeZero = self.settings.gameTimeZero or gameTimeZero;
local function adjustTime(oldTime)
return oldTime + self.settings.gameTimeZero - gameTimeZero;
end
for _, charSettings in pairs(self.settings.characters) do
local autoReminders = charSettings.autoReminders;
if (autoReminders) then
local tasks = autoReminders.tasks;
if (tasks) then
if (tasks.lastRemindTime) then
tasks.lastRemindTime = adjustTime(tasks.lastRemindTime);
end
end
local quests = autoReminders.quests;
if (quests) then
local questInfo = quests.questInfo;
if (questInfo) then
for questName, info in pairs(questInfo) do
if (info.lastCompleted) then
info.lastCompleted = adjustTime(info.lastCompleted);
end
-- 'resetTime' renamed to 'resetTimes' in v1.51
if (info.resetTime) then
info.resetTimes = info.resetTime;
info.resetTime = nil;
end
end
end
end
end
end
for n, tabSettings in pairs(self.settings.tabs) do
local events = tabSettings.events;
if (events) then
for eventNum, eventInfo in pairs(events) do
if ((eventInfo.expTime) and (eventInfo.expTime > 1000) and (eventInfo.expTime < 10000000000000)) then
eventInfo.expTime = adjustTime(eventInfo.expTime);
end
end
end
end
self.settings.gameTimeZero = gameTimeZero;
return settings;
end
function Window:SetLocalTimeOffset(offsetHours)
self.settings.localTimeOffset = offsetHours;
Time.SetLocalTimeOffset(offsetHours);
self.nextServerResetTime = Time():GetNextServerResetTime():GetGameTime();
end
-- Fill in any new character settings that have been added if the plugin has been updated
function Window:UpdateCharSettings(settings)
local prevContext = L:SetContext("/Defaults/Categories");
settings.autoReminders = settings.autoReminders or {};
settings.autoReminders.raidlocks = settings.autoReminders.raidlocks or
{ enabled = true; tab = nil; category = L:GetText("Instances"); };
settings.autoReminders.raidlocks.lockInfo = settings.autoReminders.raidlocks.lockInfo or {};
if (settings.autoReminders.raidlocks.notesEnabled == nil) then
settings.autoReminders.raidlocks.notesEnabled = true;
end
settings.autoReminders.crafting = settings.autoReminders.crafting or
{ enabled = true; tab = nil; category = L:GetText("Crafting"); };
settings.autoReminders.crates = settings.autoReminders.crates or
{ enabled = true; tab = nil; category = L:GetText("Crates"); };
settings.autoReminders.tasks = settings.autoReminders.tasks or
{ enabled = true; tab = nil; category = L:GetText("Tasks"); };
settings.autoReminders.quests = settings.autoReminders.quests or
{ enabled = true; tab = nil; category = L:GetText("Quests"); questInfo = {} };
L:SetContext(prevContext);
return settings;
end
--function Window:FindMouseClicks(ctl)
-- AddCallback(ctl, "MouseClick", function()
-- Puts("MouseClick found:" .. tostring(ctl));
-- end);
-- local children = ctl:GetControls();
-- Puts(tostring(ctl) .. " has " .. children:GetCount() .. " children:");
-- for c = 1, children:GetCount() do
-- self:FindMouseClicks(children:Get(c));
-- end
--end
function Window:CreateSavedTabs(login)
for t = 1, #self.settings.tabs do
local settings = self.settings.tabs[t];
local tab = self:CreateTab(settings);
if (login) then
tab:Login();
end
end
self:BringTabToFront(self:GetFrontTabName());
self.tabStack:RedistributeTabs();
end
function Window:BringTabToFront(tabName)
if (tabName) then
local tab = self.tabsByName[tabName];
if (tab) then
tab:BringToFront();
end
end
end
function Window:GetFrontTabName()
-- Return the currently-selected tab for this character.
local tabName = self.charSettings.frontTab;
-- If this character never previously logged in, or the character's selected tab was deleted since last login, just use the first tab.
if ((not tabName) or (not self.tabsByName[tabName])) then
tabName = self.settings.tabs[1].name;
end
self.charSettings.frontTab = tabName;
return tabName;
end
function Window:Closing(args)
self:Iconify();
args.Cancel = true;
end
function Window:Iconify()
if (not self.icon:IsIcon()) then
self.icon:Iconify();
end
end
function Window:SaveSettings(now)
if (now) then
--Puts("Saving...");
self.settings.pluginVersion = self.plugin:GetVersion();
-- Workaround for Turbine localization bug
local saveData = ExportTable(self.settings);
Turbine.PluginData.Save(Turbine.DataScope.Account, "Reminders", saveData);
-- , function()
-- Puts("Save complete.");
-- end);
self.lastSave = Turbine.Engine.GetGameTime();
else
--Puts("Deferred save");
self.saveNeeded = true;
end
end
function Window:ShowOptionsPanel()
if (self.optionsPanel:IsVisible()) then
self.optionsPanel:SetVisible(false);
self.zoomer = Thurallor.UI.Zoomer(self.optionsPanel, self.settingsButton);
self.zoomer.ZoomComplete = function()
self.zoomer = nil;
end
else
self.zoomer = Thurallor.UI.Zoomer(self.settingsButton, self.optionsPanel);
self.zoomer.ZoomComplete = function()
self.optionsPanel:Show();
self.zoomer = nil;
end
end
end
function Window:UpdateOptionsPanel()
self.optionsPanel:UpdateDisplay();
end
function Window:Deselect()
-- Disable the marquee if the user clicks in the border area
local tab = self.tabsByName[self:GetFrontTabName()];
tab:Deselect();
end
function Window:GetTab(tabName)
return self.tabsByName[tabName];
end
function Window:GetCharNamesWithServers(full)
-- Create a sorted list of all known character names (with server) and a lookup table to check for existence
local names, exists, name = {}, {};
local charIDs = self:GetCharIDs();
for _, charID in pairs(charIDs) do
local name = self:GetCharNameWithServer(charID, full);
table.insert(names, name);
exists[name] = charID;
end
return names, exists;
end
-- If 'full' is false, the server name will be omitted if it's the currently logged-in server.
-- If 'full' is true, the server name will always be included.
function Window:GetCharNameWithServer(charID, full)
local serverID, charName = charID:match("^(.*)%.(.*)$");
local name;
if ((not full) and (serverID == self.serverID)) then
name = charName;
else
local serverName = Turbine.Engine:GetServerName(serverID) or serverID;
name = L:GetText("/MainWindow/CharacterOnServer"):gsub("<name>", charName):gsub("<server>", serverName);
end
return name;
end
function Window:GetCharIDs()
-- Create a sorted list of character IDs and a lookup table to check for existence
local IDs, exists = {}, {};
for ID, _ in pairs(self.settings.characters) do
table.insert(IDs, ID);
exists[ID] = true;
end
table.sort(IDs);
return IDs, exists;
end
function Window:GetUserTabs()
-- Create a sorted list of tab names and a lookup table to check for existence
local names, exists = {}, {};
for n, s in ipairs(self.settings.tabs) do
table.insert(names, s.name);
exists[s.name] = n;
end
table.sort(names);
return names, exists;
end
function Window:GetNumUserTabs()
return #self.settings.tabs;
end
function Window:GetUserCategories()
-- Create a sorted list of category names and a lookup table to check for existence
local names, exists = {}, {};
for n, s in ipairs(self.settings.categories) do
table.insert(names, s.name);
exists[s.name] = n;
end
table.sort(names);
return names, exists;
end
function Window:RenameCategory(oldName, newName)
local catName, catExists = self:GetUserCategories();
local n, collision = catExists[oldName], catExists[newName];
if ((oldName == newName) or (not n) or collision) then
return;
end
self.settings.categories[n].name = newName;
-- Update default category for new reminders
if (self.settings.defaultCategory == oldName) then
self.settings.defaultCategory = newName;
end
-- Update category for automatic reminders
for _, charSettings in pairs(self.settings.characters) do
for _, reminderTypeSettings in pairs(charSettings.autoReminders) do
if (reminderTypeSettings.category == oldName) then
reminderTypeSettings.category = newName;
end
local individualRules = reminderTypeSettings.questInfo or reminderTypeSettings.lockInfo or {};
for _, individualSettings in pairs(individualRules) do
if (individualSettings.category == oldName) then
individualSettings.category = newName;
end
end
end
end
-- Update category name in existing events
self:ForEachEventDo(function(event)
local cat = event:GetCategory();
if (cat == oldName) then
event:SetCategory(newName);
end
end);
self:SaveSettings();
end
function Window:GetCategoryColor(category)
local catNames, catExists = self:GetUserCategories();
n = catExists[category];
if (not n) then
return Turbine.UI.Color(0.75, 0.75, 0.75);
end
local color = self.settings.categories[n].color;
return Turbine.UI.Color(unpack(color));
end
-- Function must accept a single 'event' argument.
-- Returning nil will continue the loop.
-- Returning 'true' aborts the loop and returns the event object.
function Window:ForEachEventDo(func)
for _, tab in pairs(self.tabsByName) do
local event = tab:ForEachEventDo(func);
if (event) then
return event;
end
end
end
function Window:CreateTab(settings)
local tab = Tab(self, settings);
self.tabStack:AddTabCard(tab);
self.tabsByName[settings.name] = tab;
return tab;
end
function Window:CreateNewTab()
local num = 1;
local name = L:GetText("/Defaults/TabName");
while self.tabsByName[name] do
num = num + 1;
name = L:GetText("/Defaults/TabName") .. " (" .. num .. ")";
end
local settings = { name = name; color = self:GetUniqueColor(); };
table.insert(self.settings.tabs, settings);
local tab = self:CreateTab(settings);
self:BringTabToFront(name);
DoCallbacks(tab, "TabLeftChanged", 0); -- includes RedistributeTabs() and save
self:UpdateOptionsPanel();
tab:OpenRenameDialog();
end
function Window:CloneTab(name)
local oldTab = self.tabsByName[name];
local newSettings = {};
DeepTableCopy(oldTab.settings, newSettings);
newSettings.tabLeft = 0;
local nameStem = string.gsub(name, " %([0-9]+%)$", "");
local num = 1, newName;
repeat
num = num + 1;
newName = nameStem .. " (" .. num .. ")";
until (not self.tabsByName[newName]);
newSettings.name = newName;
table.insert(self.settings.tabs, newSettings);
local newTab = self:CreateTab(newSettings);
self:BringTabToFront(newName);
DoCallbacks(newTab, "TabLeftChanged", 0); -- includes RedistributeTabs() and save
self:SaveSettings();
self:UpdateOptionsPanel();
newTab:OpenRenameDialog();
end
function Window:CreateNewItinerary(events)
-- Fill in the remaining fields
for _, event in pairs(events) do
event.expTime = -1;
event.checked = false;
event.category = self.settings.defaultCategory;
event.charID = self.charID;
end
-- Create a new tab
local num = 1;
local name = L:GetText("/Defaults/ItineraryTabName");
while self.tabsByName[name] do
num = num + 1;
name = L:GetText("/Defaults/ItineraryTabName") .. " (" .. num .. ")";
end
local settings = { name = name; color = self:GetUniqueColor(); events = events };
local tableWidth = self:GetWidth() - 38;
local checkBoxWeight = 43 / tableWidth;
local remain = 1 - checkBoxWeight;
settings.presentation = { columns = {
{ "checkbox", checkBoxWeight }, { "desc", remain * .6 }, { "location", remain * .2, true }, { "distance", remain * .2 }
}};
table.insert(self.settings.tabs, settings);
local tab = self:CreateTab(settings);
self:BringTabToFront(name);
DoCallbacks(tab, "TabLeftChanged", 0); -- includes RedistributeTabs() and save
self:SaveSettings();
self:UpdateOptionsPanel();
end
-- Tries to select a color for a new tab that will contrast with existing tabs
function Window:GetUniqueColor()
-- Strategy:
-- 1. Make a list of the hues of existing tabs.
-- 2. Find the two adjacent hues that are the farthest apart.
-- 3. Use the midpoint between those two hues.
-- Make a sorted list of hues
local hues = {};
for n, tab in ipairs(self.settings.tabs) do
local hue = Thurallor.Utils.Color(unpack(tab.color)):Get("H");
table.insert(hues, hue);
end
table.sort(hues);
table.insert(hues, hues[1] + 1); -- search also needs to wrap around back to the first hue
-- Find the two adjacent hues that are the farthest apart
local maxDistance = 0;
local bestHue = 0;
for n = 1, #hues - 1 do
local a, b = hues[n], hues[n + 1];
local distance = b - a;
if (distance >= maxDistance) then
bestHue = (a + b) / 2;
maxDistance = distance;
end
end
if (bestHue > 1) then
bestHue = bestHue - 1;
end
-- Create color with full saturation and value
return {Thurallor.Utils.Color(0, 0, 0):SetHSV(bestHue, 1, 1):Unpack()};
end
function Window:CreateTabStack()
if (self.tabStack) then
self.tabStack:SetParent(nil);
end
self.tabStack = Thurallor.UI.TabCardStack();
self.tabStack:SetParent(self);
self.tabStack:SetPosition(14, 35);
self.tabsByName = {};
self.tabStack:SetAddTabButtonEnabled(true, L:GetText("/MainWindow/TabContextMenu/CreateTab"));
AddCallback(self.tabStack, "AddTabButtonClicked", function(_, args)
if (args.Button == Turbine.UI.MouseButton.Left) then
self:CreateNewTab();
end
end);
self.tabStack:SetMoreTabsButtonEnabled(true, L:GetText("/MainWindow/MoreTabs"));
AddCallback(self.tabStack, "FrontTabChanged", function(_, tab)
self:_FrontTabChanged(tab);
end);
end
-- Gets called when the user switches tabs, either by clicking it, or with the Filter button
function Window:_FrontTabChanged(tab)
if (tab) then
self.shimmering:Remove(tab.tabText);
self.charSettings.frontTab = tab:GetTabText();
self:SaveSettings();
self.newEventButton:SetVisible(true);
self.noTabsDisplayed:SetParent(nil);
else
self.charSettings.frontTab = nil;
self.newEventButton:SetVisible(false);
self.noTabsDisplayed:SetParent(self);
end
end
function Window:MergeTabs(sourceName, destName)
local sourceTabNum, destTabNum;
for n, settings in ipairs(self.settings.tabs) do
if (settings.name == sourceName) then
sourceTabNum = n;
elseif (settings.name == destName) then
destTabNum = n;
end
end
if (sourceTabNum and destTabNum) then
local sourceEvents = self.settings.tabs[sourceTabNum].events;
local destEvents = self.settings.tabs[destTabNum].events;
for _, eventInfo in ipairs(sourceEvents) do
table.insert(destEvents, eventInfo);
end
self:GetTab(destName):ReapplySort();
self:DeleteTab(sourceName); -- calls RebuildTabs()
-- Update tab targets for automatic reminders
for _, charSettings in pairs(self.settings.characters) do
local autoReminders = charSettings.autoReminders or {};
for _, catSettings in pairs(autoReminders) do
if (catSettings.tab == sourceName) then
catSettings.tab = destName;
end
local itemInfo = catSettings.questInfo or catSettings.raidInfo or {};
for _, itemSettings in pairs(itemInfo) do
if (itemSettings.tab == sourceName) then
itemSettings.tab = destName;
end
end
end
end
self:UpdateOptionsPanel();
end
end
function Window:DeleteTab(name)
local tab = self.tabsByName[name];
tab:ForEachEventDo(function(event)
event:Destroy();
end);
for t = 1, #self.settings.tabs do
if (self.settings.tabs[t].name == name) then
table.remove(self.settings.tabs, t);
break;
end
end
self:RebuildTabs();
self:UpdateOptionsPanel();
self:SaveSettings();
end
function Window:RenameTab(oldName, newName)
for t = 1, #self.settings.tabs do
if (self.settings.tabs[t].name == oldName) then
self.settings.tabs[t].name = newName;
break;
end
end
self:RebuildTabs();
self:UpdateOptionsPanel();
self:SaveSettings();
end
function Window:RebuildTabs(login)
-- Since the tabs are now windows, we need to explicitly close them.
if (self.tabsByName) then
for _, tab in pairs(self.tabsByName) do
tab:Close();
end
end
self.alerts = {};
self.icon:SetDisplayedNumber(0);
self:CreateTabStack();
self:CreateSavedTabs(login);
self:SetScope(self.settings.viewingScope);
self:SizeChanged(); -- includes save
end
function Window:TickAllEvents()
for _, tab in pairs(self.tabsByName) do
tab:ForEachEventDo(function(event)
event:SetWantsTicks(true);
end);
end
end
function Window:SetMarqueeAntsEnabled(enable)
self.settings.marqueeAntsEnabled = enable;
for _, tab in pairs(self.tabsByName) do
local style = tab.table:GetStyle();
style.marquee.AntsEnabled = enable;
tab.table:SetStyle(style);
local selectedEvent = tab:GetSelectedEvent();
if (selectedEvent) then
-- Reselect to update marquee of popup, if any
tab:SelectEvent(selectedEvent);
end
end
self:SaveSettings();
end
function Window:SetZoomEffectsEnabled(enable)
if (enable) then
self.settings.zoomEffects = true;
Thurallor.UI.Zoomer.defaultSpeed = 2;
Thurallor.UI.Zoomer.defaultTrailDuration = 0.25;
else
self.settings.zoomEffects = false;
Thurallor.UI.Zoomer.defaultSpeed = math.huge;
Thurallor.UI.Zoomer.defaultTrailDuration = 0;
end
end
function Window:SizeChanged(noSave)
local width, height = self:GetSize();
if (self.tabStack) then
self.tabStack:SetSize(width - 28, self:GetHeight() - 57);
self.tabStack:RedistributeTabs();
end
self.noTabsDisplayed:SetSize(width - 28, height - 77);
self.newEventButton:SetPosition(30, 0);
self.scopeButton:SetPosition(width - 102, 0);
self.settingsButton:SetPosition(width - 66, 0);
self.settings.size = { width, height };
self:SaveSettings();
end
function Window:PositionChanged(noSave)
local left, top = self:GetPosition();
self.settings.position = { left, top };
self:SaveSettings();
end
function Window:SetControlsVisible(visible)
if (visible) then
self.backgroundHider:SetSize(0, 0);
else
self.backgroundHider:SetSize(self:GetSize());
end
end
function Window:Update()
local gameTime = Turbine.Engine.GetGameTime();
if (self:IsVisible()) then
-- Stuff to do when mouse enters or leaves the window
local x, y = self:GetMousePosition();
local w, h = self:GetSize();
local mouseOutside = ((x < 0) or (y < 0) or (x >= w) or (y >= h));
if (mouseOutside ~= self.mouseWasOutside) then
if (self.settings.mouseLeaveHideControls) then
-- Show/hide all inactive tabs
for name, tab in pairs(self.tabsByName) do
tab:SetVisible(tab:IsInFront() or not mouseOutside);
end
self:SetControlsVisible(not mouseOutside);
self.tabStack:SetAddTabButtonEnabled(not mouseOutside);
self.tabStack:SetMoreTabsButtonEnabled(not mouseOutside);
else
-- Show all inactive tabs
for name, tab in pairs(self.tabsByName) do
tab:SetVisible(true);
end
self:SetControlsVisible(true);
self.tabStack:SetAddTabButtonEnabled(true);
self.tabStack:SetMoreTabsButtonEnabled(true);
end
self:SetOpacity((mouseOutside and self.settings.mouseLeaveWindowOpacity) or 1);
end
self.mouseWasOutside = mouseOutside;
end
-- Check for server reset
if (gameTime > self.nextServerResetTime) then
self:ProcessServerReset();
self.nextServerResetTime = self.nextServerResetTime + Time.lengthOfDay;
end
-- Call Tick() function of each tab, at least once per second, but spread them out over multiple updates.
local elapsed = gameTime - self.lastTick;
local workToDo = math.min(self.numTickFuncs, math.ceil(elapsed * self.numTickFuncs));
--self:SetText(workToDo .. "/" .. self.numTickFuncs);
while (workToDo > 0) do
local tickFunc = next(self.tickFuncs, self.prevTickFunc);
self.prevTickFunc = tickFunc;
if (tickFunc) then
tickFunc(gameTime);
end
workToDo = workToDo - 1;
end
self.lastTick = gameTime;
-- Save settings no more often than once per second.
if (self.saveNeeded and (gameTime - self.lastSave >= 1)) then
self:SaveSettings(true);
self.saveNeeded = false;
end
end
-- This function is called at server reset time
function Window:ProcessServerReset()
-- Generate bubble notification
self:CascadeNotify(L:GetText("/Time/DailyResetTime"), nil, Turbine.UI.Color.Gold);
-- To do: Update reminders which have a "rotation" reset schedule and are no longer available.
-- Also do this at startup.
end
function Window:AddTickFunc(func)
if (func and (not self.tickFuncs[func])) then
self.numTickFuncs = self.numTickFuncs + 1;
self.tickFuncs[func] = true;
end
end
function Window:RemoveTickFunc(func)
if (func and self.tickFuncs[func]) then
self.numTickFuncs = self.numTickFuncs - 1;
self.tickFuncs[func] = nil;
if (self.prevTickFunc == func) then
self.prevTickFunc = nil;
end
end
end
function Window:InCombatChanged()
local inCombat = self.player:IsInCombat();
if (inCombat and self.settings.pauseDuringCombat) then
self:Iconify();
-- Don't process chat messages during combat
self:SetChatParsingPaused(true);
-- Don't update events or save settings during combat
self:SetWantsUpdates(false);
else
self:SetChatParsingPaused(false);
self:SetWantsUpdates(true);
end
self.itemRemovedQueue = {}; -- crafting can't occur during combat
end
-- Function used to temporarily pause chat parsing for performance reasons (during combat)
-- or while waiting for recipe discovery to complete. While paused, chat messages are
-- queued for later processing.
function Window:SetChatParsingPaused(paused)
self.chatPaused = paused;
if (not paused) then
-- Process enqueued messages, if any
for _, args in ipairs(self.chatQueue) do
self.chatCallback(_, args);
self.chatQueue = {};
end
end
end
function Window:CreateChatParser()
-- Get localized regular expressions for chat parsing
self.raidLockDetectRegexp1 = L:GetClientLanguageText("/Chat/RaidLockDetect1");
self.raidLockDetectRegexp2 = L:GetClientLanguageText("/Chat/RaidLockDetect2");
self.raidFavoredCompletionsRegexp = L:GetClientLanguageText("/Chat/RaidFavoredCompletionsDetect");
self.raidFavoredCompletionsNotesRegexp = L:GetClientLanguageText("/Chat/RaidFavoredCompletionsNotes");
self.raidCommonCompletionsRegexp = L:GetClientLanguageText("/Chat/RaidCommonCompletionsDetect");
self.craftingDetectRegexp = L:GetClientLanguageText("/Chat/CraftingDetect");
self.newRecipeRegexp = L:GetClientLanguageText("/Chat/NewRecipeDetect");
self.crateDetectRegexp = L:GetClientLanguageText("/Chat/CrateDetect");
self.questBestowDetectRegexp = L:GetClientLanguageText("/Chat/QuestBestowDetect");
self.questCompleteDetectRegexp = L:GetClientLanguageText("/Chat/QuestCompleteDetect");
self.identifyTaskRegexp = L:GetClientLanguageText("/Chat/IdentifyTaskQuest");
self.itemRemovedRegexp = L:GetClientLanguageText("/Chat/ItemRemovedDetect");
-- Create a Label object for efficiently stripping the "<rgb>" tags from strings
self.colorStripper = Turbine.UI.Label();
-- Create specialized chat parsing function for each channel of interest
local channelParseFunc = {};
channelParseFunc[Turbine.ChatType.Standard] = function(message)
local lockName, completions, days, hours, minutes;
-- "Bratha Tasakh's Chest - Tier 1 resets in: 1d - 4h - 14m."
lockName, days, hours, minutes = string.match(message, self.raidLockDetectRegexp1);
if (lockName) then
return self:RaidLockUpdated(lockName, days, hours, minutes);
end
-- "Throne of the Dread Terror - Flag 1 - Resets in 6d - 3h - 3m."
lockName, days, hours, minutes = string.match(message, self.raidLockDetectRegexp2);
if (lockName) then
return self:RaidLockUpdated(lockName, days, hours, minutes);
end
-- "Shaktur's Mithril Chest: You have 1 favoured completion remaining."
lockName, completions = string.match(message, self.raidFavoredCompletionsRegexp);
if (lockName) then
local notes = string.match(message, self.raidFavoredCompletionsNotesRegexp);
return self:RaidLockUpdated(lockName, 0, 0, 0, completions, notes);
end
-- "Gast Nûl's Gold Chest: You have 18 completions remaining."
lockName, completions = string.match(message, self.raidCommonCompletionsRegexp);
if (lockName) then
return self:RaidLockUpdated(lockName, nil, nil, nil, completions);
end
if (self.charSettings.autoReminders.crafting.enabled) then
-- Check for "crafting complete" message
local recipeName = string.match(message, self.craftingDetectRegexp);
if (recipeName) then
if (self.recipes.loaded) then
-- We have the recipe book. Create reminder if cooldown info is known.
if (self.recipes.cooldown[recipeName]) then
return self:AddCraftingReminder(recipeName);
end
else
-- Need to load the recipe book before processing this recipe completion message.
-- Re-enqueue the current message, so it'll be the first that is processed when chat parsing resumes.
table.insert(self.chatQueue, 1, { ChatType = Turbine.ChatType.Standard; Message = message } );
return self:DiscoverRecipes();
end
end
-- Check for "new recipe learned" message
local recipeName, profName = string.match(message, self.newRecipeRegexp);
if (recipeName and self.recipes.loaded) then
-- Don't bother rereading the recipes, because they won't be updated.
Puts(L:GetText("/Chat/NewRecipeWarning"));
-- self:DiscoverRecipes();
end
end
end
channelParseFunc[Turbine.ChatType.SelfLoot] = function(message)
if (self.charSettings.autoReminders.crates.enabled) then
local crateName = string.match(message, self.crateDetectRegexp);
if (crateName) then
return self:AddCrateReminder(crateName);
end
end
if (self.charSettings.autoReminders.crafting.enabled) then
-- Check for "item removed" messages.
self.colorStripper:SetText(message);
message = self.colorStripper:GetText();
local itemStr = string.match(message, self.itemRemovedRegexp);
if (itemStr) then
return self:ItemRemoved(itemStr);
end
end
end
channelParseFunc[Turbine.ChatType.Quest] = function(message)
local questName = string.match(message, self.questCompleteDetectRegexp);
if (questName) then
return self:QuestCompleted(questName);
end
questName = string.match(message, self.questBestowDetectRegexp);
if (questName) then
return self:QuestBestowed(questName);
end
end
-- Return a function that will be used as a ChatReceived callback
return function(_, args)
local parseFunc = channelParseFunc[args.ChatType];
if (parseFunc) then
if (self.chatPaused) then
-- Enqueue message for later parsing
table.insert(self.chatQueue, args);
else
-- Parse message
parseFunc(args.Message);
end
end
end
end
function Window:QuestCompleted(questName)
if (self:QuestIsTask(questName)) then
-- Quest is a task
local settings = self.charSettings.autoReminders.tasks;
if (settings.enabled) then
self:AddTaskReminder();
end
else
-- Not a task, just a normal repeatable quest
local settings = self.charSettings.autoReminders.quests;
local info = settings.questInfo[questName];
if (not info) then
settings.questInfo[questName] = { lastCompleted = Turbine.Engine.GetGameTime() };
return self:SaveSettings();
end
if (settings.enabled) then
if (info.enabled) then
-- Completed this quest before. User has requested reminders for this quest. Create one.
self:AddQuestReminder(questName);
end
info.lastCompleted = Turbine.Engine.GetGameTime();
self:SaveSettings();
end
end
end
function Window:FindOldestMatchingEvent(tabName, desc, charID, category)
tab = self.tabsByName[tabName];
if (tab) then
return tab:FindOldestMatchingEvent(desc, charID, category);
end
end
function Window:AddQuestReminder(questName, charID, now)
charID = charID or self.charID;
local settings = self.settings.characters[charID].autoReminders.quests;
local info = settings.questInfo[questName];
local tab = self.tabsByName[info.tab] or self.tabsByName[settings.tab] or self.tabsByName[self:GetFrontTabName()];
local desc = info.desc or questName;
local category = info.category or settings.category or self.settings.categories[1].name;
local location = info.location;
local event, expTime, resetTimes, rotation;
if (info.cooldown) then
expTime = info.cooldown + Turbine.Engine.GetGameTime();
elseif (info.resetTimes) then
expTime = Schedule(info.resetTimes):GetNextTime(nil, true):GetGameTime();
resetTimes = info.resetTimes;
elseif (info.rotation) then
expTime = Schedule(info.rotation):GetNextTime(nil, true):GetGameTime();
rotation = info.rotation;
else -- default is next server reset time
expTime = self.nextServerResetTime;
resetTimes = "*:3";
end
if (now) then
expTime = Turbine.Engine.GetGameTime();
end
if (self.settings.reuseExpiredReminders) then
event = tab:FindOldestMatchingEvent(desc, charID, category);
if (event) then
event:SetExpirationTime(expTime);
event:SetResetTimes(resetTimes);
event:SetRotation(rotation);
event:SetLastAutomaticUpdate(charID, "quests", questName);
self:CascadeNotify(L:GetText("/Cascade/ReminderUpdated"), event);
tab:ReapplySort("remaining");
self:SaveSettings();
return event;
end
end
event = tab:CreateNewEvent2({ desc = desc, charID = charID, expTime = expTime, resetTimes = resetTimes,
rotation = rotation, category = category, location = location, checked = false });
if (event) then
if (info.cooldown) then
event.settings.postpone.delay = info.cooldown;
elseif (info.resetTimes or info.rotation) then
event.settings.postpone.method = "ScheduledResetTime";
end
event:SetLastAutomaticUpdate(charID, "quests", questName);
self:CascadeNotify(L:GetText("/Cascade/ReminderCreated"), event);
return event;
end
end
function Window:AddTaskReminder()
local settings = self.charSettings.autoReminders.tasks;
settings.lastRemindTime = settings.lastRemindTime or 0;
local nextResetTime = self.nextServerResetTime;
if (settings.lastRemindTime < nextResetTime) then
settings.lastRemindTime = nextResetTime;
local desc = L:GetText("/Defaults/TaskReminderDesc");
local tab = self.tabsByName[settings.tab] or self.tabsByName[self:GetFrontTabName()];
local category = settings.category or self.settings.categories[1].name;
local location = settings.location;
local event;
if (self.settings.reuseExpiredReminders) then
event = tab:FindOldestMatchingEvent(desc, self.charID, category);
if (event) then
event:SetResetTimes("*:3");
event:SetExpirationTime(nextResetTime);
event:SetLastAutomaticUpdate(self.charID, "tasks");
self:CascadeNotify(L:GetText("/Cascade/ReminderUpdated"), event);
tab:ReapplySort("remaining");
settings.location = event:GetLocation();
return self:SaveSettings();
end
end
event = tab:CreateNewEvent(desc, self.charID, nextResetTime, "*:3", category, location, false);
if (event) then
event:SetLastAutomaticUpdate(self.charID, "tasks");
self:CascadeNotify(L:GetText("/Cascade/ReminderCreated"), event);
end
self:SaveSettings();
end
end
function Window:QuestBestowed(questName)
local settings = self.charSettings.autoReminders.quests;
if (settings.enabled) then
if (self:QuestIsTask(questName)) then
-- Not interested in task quest bestowals.
return;
end
local info = settings.questInfo[questName] or {};
settings.questInfo[questName] = info;
if (info.lastCompleted) then
info.repeated = true;
if (info.enabled == nil) then
QuestRepeatedDialog.GetInstance(self, questName, info, self.charSettings, self.charID);
elseif (info.enabled and info.rotation) then
-- Verify that this quest is being bestowed on an available day, according to
-- the rotation schedule. If not, then the schedule probably changed due to a
-- server reset.
if (not Schedule(info.rotation):AvailableInRotation(Time())) then
UnexpectedBestowalDialog.GetInstance(self, questName, info, self.charSettings, self.charID);
end
end
end
end
end
function Window:QuestIsTask(questName)
if (L:GetClientLanguage() == Turbine.Language.German) then
-- Every task quest name in the game in the German client has the word "Auftrag" in it except these:
if (string.match(questName, "^Eroberung von Gorgoroth:")) then
return true;
end
end
return string.match(questName, self.identifyTaskRegexp);
end
function Window:DiscoverRecipes()
self:SetChatParsingPaused(true);
self.freezeWarning = Thurallor.UI.FreezeWarning(L:GetText("/Defaults/WindowTitle"), L:GetText("/ReadingRecipes"), function()
self.recipes = {
loaded = true;
cooldown = {};
versions = {};
}
local attribs = self.player:GetAttributes();
if (attribs.GetProfessionInfo) then
-- Monster players can't do crafting
for profStr, prof in pairs(Turbine.Gameplay.Profession) do
local profInfo = attribs:GetProfessionInfo(prof);
if (profInfo) then
for r = 1, profInfo:GetRecipeCount() do
local recipe = profInfo:GetRecipe(r);
local cooldown = recipe:GetCooldown();
if (cooldown > 0) then
local recipeName = recipe:GetName();
local versions = self.recipes.versions[recipeName];
if (not versions) then
versions = {};
self.recipes.versions[recipeName] = versions;
end
table.insert(versions, recipe);
-- See if this recipe appears in multiple tiers; if so, then we need to include that in the reminder description.
if (recipe:GetTier() ~= versions[1]:GetTier()) then
versions[1].multiTier = true;
recipe.multiTier = true;
end
-- For now, we assume that recipes with the same name in different professions have the same cooldown.
self.recipes.cooldown[recipeName] = cooldown;
end
end
end
end
end
self:SetChatParsingPaused(false);
self.freezeWarning = nil;
end);
end
-- Identifies the profession, tier, and category of a just-completed recipe, based on the ingredients that were recently consumed
function Window:IdentifyRecipeVersion(recipeName)
local function GetRecipeInfo(recipe)
local prevContext = L:SetContext("/Defaults/Professions");
local profName = L:GetClientLanguageText(tostring(recipe:GetProfession()));
L:SetContext("/Defaults/CraftingTiers");
local tierName = L:GetClientLanguageText(tostring(recipe:GetTier()));
local categoryName = recipe:GetCategoryName();
L:SetContext(prevContext);
return profName, tierName, categoryName, recipe.multiTier;
end
local versions = self.recipes.versions[recipeName];
if (#versions == 1) then
return GetRecipeInfo(versions[1]);
end
-- Otherwise there are multiple recipes with this name (in different professions or tiers of the same profession).
-- Identify which one it is by checking the recently-consumed ingredients.
for _, recipe in pairs(versions) do
-- Make a table of the ingredients for this recipe
local ingredients = {};
for i = 1, recipe:GetIngredientCount() do
local ingredient = recipe:GetIngredient(i);
local quantity = ingredient:GetRequiredQuantity();
local itemInfo = ingredient:GetItemInfo();
table.insert(ingredients, { quantity, itemInfo:GetName(), itemInfo:GetNameWithQuantity() });
end
-- See if the last few items removed match the ingredients for this recipe
for _, itemRemoved in ipairs(self.itemRemovedQueue) do
local quantity, itemName = unpack(itemRemoved);
local continue = false;
for i = 1, #ingredients do
local ingredientQuantity, ingredientName, ingredientNamePlural = unpack(ingredients[i]);
if ((itemName == ingredientName) or (itemName == ingredientNamePlural)) then
ingredientQuantity = ingredientQuantity - quantity;
if (ingredientQuantity < 0) then
-- Used too many ingredients; must be some other recipe.
continue = false;
break;
else
if (ingredientQuantity > 0) then
ingredients[i] = { ingredientQuantity, ingredientName, ingredientNamePlural };
else
table.remove(ingredients, i);
end
-- Found and removed ingredient from the list.
continue = true;
break;
end
end
end
-- Didn't find the item in the ingredient list. Must be some other recipe.
if (not continue) then
break;
end
if (#ingredients == 0) then
-- Removed all ingredients from the list. Recipe found!
self.itemRemovedQueue = {};
return GetRecipeInfo(recipe);
end
end
end
-- Didn't find a matching recipe, probably because Universal Ingredient Packs were used. No way to disambiguate these, so punt.
self.itemRemovedQueue = {};
return nil;
end
function Window:ItemRemoved(itemStr)
local quantity, itemName = string.match(itemStr, "^([0-9]+) (.*)$");
if (quantity) then
quantity = tostring(quantity);
else
quantity = 1;
itemName = itemStr;
end
table.insert(self.itemRemovedQueue, 1, { quantity, itemName });
end
function Window:NextScope()
local scope = self.settings.viewingScope;
if (scope == "server") then
scope = "character";
elseif (scope == "character") then
scope = "account";
else -- (scope == "account")
scope = "server";
end
self.settings.viewingScope = scope;
self.scopeTooltip:Update();
self:RebuildTabs(); -- results in a call to SetScope()
end
function Window:SetScope(scope)
self.settings.viewingScope = scope;
self.scopeButton:SetBackground(imagePath .. "scope_" .. scope .. ".tga");
self.scopeButton:SetHighlightedBackground(imagePath .. "scope_" .. scope .. "_highlight.tga");
self.scopeButton:SetMouseDownBackground(imagePath .. "scope_" .. scope .. "_click.tga");
local frontTabName, newFrontTab = self:GetFrontTabName(), nil;
for name, tab in pairs(self.tabsByName) do
tab:SendToBack();
local hide = self.settings.hideEmptyTabsWhenFiltering and tab:IsEmpty();
tab:SetHidden(hide);
if (not hide) then
newFrontTab = tab;
end
end
local frontTab = self.tabsByName[frontTabName];
if (not frontTab:IsHidden()) then
frontTab:BringToFront();
elseif (newFrontTab) then
newFrontTab:BringToFront();
else
-- No tabs to display; disable the (+) button
DoCallbacks(self.tabStack, "FrontTabChanged", nil);
end
self.tabStack:RedistributeTabs();
end
function Window:CreateNewEvent()
local frontTab = self.tabsByName[self:GetFrontTabName()];
local expTime;
if (self.settings.defaultDelayMethod == "now") then
expTime = Turbine.Engine.GetGameTime();
elseif (self.settings.defaultDelayMethod == "dailyResetTime") then
expTime = self.nextServerResetTime;
elseif (self.settings.defaultDelayMethod == "atNextLogin") then
expTime = 0;
elseif (self.settings.defaultDelayMethod == "never") then
expTime = -1;
else -- (self.settings.defaultDelayMethod == "delayFromNow")
expTime = Turbine.Engine.GetGameTime() + self.settings.defaultDelay;
end
if (not frontTab:IsColumnShowing("remaining")) then
expTime = -1; -- never
end
local catNames, catExists = self:GetUserCategories();
local category = self.settings.defaultCategory;
local location = nil;
local checked = false;
if (not catExists[category]) then
category = self.settings.categories[1].name;
end
local event = frontTab:CreateNewEvent(L:GetText("/Defaults/ReminderDesc"), self.charID, expTime, nil, category, location, checked);
if (event) then
frontTab:SelectEvent(event);
end
self:SaveSettings();
end
function Window:RaidLockUpdated(lockName, days, hours, minutes, completions, notes)
local settings = self.charSettings.autoReminders.raidlocks;
local info = settings.lockInfo[lockName];
if (not info) then
-- Newly discovered raid lock.
info = { resetTimes = "*:3" };
settings.lockInfo[lockName] = info;
if (settings.enabled) then
RaidLockDialog.GetInstance(self, lockName, info, self.charSettings, self.charID, notes);
end
elseif (settings.enabled and info.enabled) then
-- User has requested reminders for this lock. Create one.
self:AddRaidLockReminder(lockName, days, hours, minutes, completions, self.charID, notes);
end
self:SaveSettings();
end
function Window:AddRaidLockReminder(lockName, days, hours, minutes, completions, charID, notes)
charID = charID or self.charID;
local settings = self.settings.characters[charID].autoReminders.raidlocks;
local info = settings.lockInfo[lockName];
local tab = self.tabsByName[info.tab] or self.tabsByName[settings.tab] or self.tabsByName[self:GetFrontTabName()];
local desc = info.desc or lockName;
local category = info.category or settings.category or self.settings.categories[1].name;
local location = info.location;
local resetTimes = info.resetTimes;
local event, expTime;
if (days) then
-- "Bratha Tasakh's Chest - Tier 1 resets in: 1d - 4h - 14m."
-- "Shaktur's Mithril Chest: You have 1 favoured completion remaining."
expTime = Turbine.Engine.GetGameTime() + days * (24 * 60 * 60) + hours * (60 * 60) + minutes * 60;
else
-- "Gast Nûl's Gold Chest: You have 18 completions remaining."
expTime = Schedule(resetTimes):GetNextTime(nil, true):GetGameTime();
end
if (self.settings.reuseExpiredReminders) then
event = tab:FindOldestMatchingEvent(desc, charID, category);
if (event) then
self:UpdateRaidLockNotes(event, notes);
event:SetExpirationTime(expTime);
event:SetResetTimes(resetTimes);
event:SetLastAutomaticUpdate(charID, "raidlocks", lockName);
self:CascadeNotify(L:GetText("/Cascade/ReminderUpdated"), event);
tab:ReapplySort("remaining");
return event;
end
end
event = tab:CreateNewEvent(desc, charID, expTime, resetTimes, category, location, false);
if (event) then
self:UpdateRaidLockNotes(event, notes);
event.settings.postpone.method = "ScheduledResetTime";
event:SetLastAutomaticUpdate(charID, "raidlocks", lockName);
self:CascadeNotify(L:GetText("/Cascade/ReminderCreated"), event);
return event;
end
end
function Window:UpdateRaidLockNotes(event, notes)
if (self.charSettings.autoReminders.raidlocks.notesEnabled) then
local prevNotes = event:GetNotes();
if (prevNotes) then
local regexp = L:GetClientLanguageText("/Chat/RaidFavoredCompletionsNotes") .. "\n?";
if (prevNotes:match(regexp)) then
notes = prevNotes:gsub(regexp, notes or "");
elseif (notes) then
notes = notes .. "\n" .. prevNotes;
else
notes = prevNotes;
end
end
event:SetNotes(notes);
end
end
function Window:AddCraftingReminder(recipeName)
local settings = self.charSettings.autoReminders.crafting;
local tab = self.tabsByName[settings.tab];
if (not tab) then
tab = self.tabsByName[self:GetFrontTabName()];
end
local profName, tierName, categoryName, multiTier = self:IdentifyRecipeVersion(recipeName);
if (profName) then
local desc = profName;
if (multiTier) then
desc = desc .. " (" .. tierName .. ")";
end
if (L:GetClientLanguage() == Turbine.Language.French) then
-- Not using L:GetLanguage() above so desc won't change when user selects a new language.
desc = desc .. " ";
end
local desc = desc .. ": " .. recipeName;
local notes = profName .. " > " .. tierName .. " > " .. categoryName .. " > " .. recipeName;
local expTime = self.recipes.cooldown[recipeName] + Turbine.Engine.GetGameTime();
local category = settings.category or self.settings.categories[1].name;
local location, event = settings.location;
if (self.settings.reuseExpiredReminders) then
event = tab:FindOldestMatchingEvent(desc, self.charID, category);
if (event) then
event:SetExpirationTime(expTime);
event:SetLastAutomaticUpdate(self.charID, "crafting", recipeName);
self:CascadeNotify(L:GetText("/Cascade/ReminderUpdated"), event);
tab:ReapplySort("remaining");
settings.location = event:GetLocation();
return self:SaveSettings();
end
end
event = tab:CreateNewEvent(desc, self.charID, expTime, nil, category, location, false);
if (event) then
event:SetNotes(notes);
event:SetLastAutomaticUpdate(self.charID, "crafting",recipeName);
self:CascadeNotify(L:GetText("/Cascade/ReminderCreated"), event);
end
self:SaveSettings();
end
end
function Window:AddCrateReminder(crateName)
local settings = self.charSettings.autoReminders.crates;
local tab = self.tabsByName[settings.tab];
if (not tab) then
tab = self.tabsByName[self:GetFrontTabName()];
end
local cooldown = 22 * 60 * 60;
local expTime = cooldown + Turbine.Engine.GetGameTime();
local category = settings.category or self.settings.categories[1].name;
local location = settings.location;
local desc, event = L:GetText("/Defaults/CrateReminderDesc");
desc = string.gsub(desc, "<cratename>", crateName);
if (self.settings.reuseExpiredReminders) then
event = tab:FindOldestMatchingEvent(desc, self.charID, category);
if (event) then
event:SetExpirationTime(expTime);
event:SetLastAutomaticUpdate(self.charID, "crates", crateName);
self:CascadeNotify(L:GetText("/Cascade/ReminderUpdated"), event);
tab:ReapplySort("remaining");
settings.location = event:GetLocation();
return self:SaveSettings();
end
end
event = tab:CreateNewEvent(desc, self.charID, expTime, nil, category, location, false);
if (event) then
event:SetLastAutomaticUpdate(self.charID, "crates", crateName);
self:CascadeNotify(L:GetText("/Cascade/ReminderCreated"), event);
end
self:SaveSettings();
end
-- If 'startup' is true, then no bubble notification will be generated
function Window:AddAlert(event, startup)
self:RemoveAlert(event); -- guard against duplicates
local tabSettings = event.tab.settings;
if (tabSettings.showPopupNotifications) then
event:ShowPopUp();
end
if (tabSettings.updateIcon) then
table.insert(self.alerts, event);
self.icon:SetDisplayedNumber(#self.alerts);
if (not self:IsVisible()) then
self.icon:DoShockwave();
end
end
if (not startup) then
self:CascadeNotify("", event, event:GetColor());
end
end
function Window:CascadeNotify(prefix, event, color)
if (not self.settings.showCascade) then
return;
end
if (not color) then
color = Turbine.UI.Color.White;
end
local text = prefix;
if (event) then
if (not event:IsMine()) then
text = text .. "[" .. self:GetCharNameWithServer(event:GetCharacterID()) .. "] ";
end
text = text .. event:GetDescription();
end
self.icon:AddCascadeMessage(text, color);
end
function Window:RemoveAlert(event)
for n, e in ipairs(self.alerts) do
if (e == event) then
table.remove(self.alerts, n);
break;
end
end
event:HidePopUp();
self.icon:SetDisplayedNumber(#self.alerts);
end
function Window:GetIconTooltip()
if (#self.alerts == 0) then
return self:GetText();
else
local function AddLabel(parent, top, prevMaxWidth, text, color)
local label = Turbine.UI.Label();
label:SetFont(Turbine.UI.Lotro.Font.Verdana14);
label:SetForeColor(color);
label:SetText(text);
label:AutoSize();
label:SetParent(parent);
label:SetPosition(3, top);
return top + 14, math.max(prevMaxWidth, label:GetWidth());
end
-- Make a list of up to ten expired events.
local control, label = Turbine.UI.Control();
local top, width = 3, 0;
for n = #self.alerts, math.max(1, #self.alerts - 9), -1 do
local event = self.alerts[n];
local color = event:GetColor();
local desc = event:GetDescription();
local charID = event:GetCharacterID();
if (not event:IsMine()) then
desc = "[" .. self:GetCharNameWithServer(charID) .. "] " .. desc;
end
top, width = AddLabel(control, top, width, desc, color);
end
if (#self.alerts > 10) then
top, width = AddLabel(control, top, width, "(+" .. (#self.alerts - 10) .. ")", Turbine.UI.Color.White);
end
control:SetSize(width, top + 3);
control:SetBackColor(Turbine.UI.Color(0.9, 0, 0, 0));
return control;
end
end
function Window:AddShimmer(controls)
for c, ctl in pairs(controls) do
self.shimmering:Add(ctl);
end
end
-- Dims the window and disables it until the specified child window closes.
function Window:WaitForChild(child)
if (child and self.mask) then
-- Child changed. This will probably never happen, but whatever.
-- Remove previous child.
self:WaitForChild(nil);
end
if (child) then
self:SetOpacity(0.75);
self.mask = Turbine.UI.Control();
self.mask:SetParent(self);
self.mask:Focus();
self.mask:SetSize(self:GetSize());
self.mask:SetZOrder(2147483647);
self.mask:SetBackColor(Turbine.UI.Color(0.25, 0, 0, 0));
self.mask:SetBackColorBlendMode(Turbine.UI.BlendMode.Overlay);
self.mask.child = child;
self.mask.MouseClick = function(ctl)
local x, y = Turbine.UI.Display.GetMousePosition();
local width, height = ctl.child:GetSize();
local left = math.max(0, x - width / 2);
local top = math.max(0, y - height/ 2);
ctl.child:SetPosition(left, top);
end
self.mask.callback = function()
self:WaitForChild(nil);
end
AddCallback(child, "Closing", self.mask.callback);
else
RemoveCallback(self.mask.child, "Closing", self.mask.callback);
self:SetOpacity(1);
self.mask:SetParent(nil);
self.mask = nil;
end
end