vanilla-wow-addons – Rev 1

Subversion Repositories:
Rev:
-- AUTHOR: Paranoidi

-- $LastChangedDate: 2006-08-14 18:34:44 +0300 (Mon, 14 Aug 2006) $
-- $LastChangedBy: marko $
-- $Rev: 247 $

--[[

                        local px,py = GetPlayerMapPosition("player");
                        local tx,ty = GetPlayerMapPosition("raid"..idx);
                        local xdist = tx-px;
                        local ydist = ty-py;
                        local dist = sqrt(math.pow(xdist,2)+math.pow(ydist,2));
                        
                        http://intern.darklegion.de/interface/profiler.jpg
                        
                        testaa profileria?
                        
                        BUG: jonku pelaajan targettaaminen ei nollaa järjestystä (kuten tyhjän valinta) !
                        
--]]


-- integration to myAddons
SmartAssistDetails = {
                name = "SmartAssist",
                description = "SmartAssist is an addon which improves default assisting system in groups.",
                version = "1.2.9",
                releaseDate = string.sub("$LastChangedDate: 2006-08-14 18:34:44 +0300 (Mon, 14 Aug 2006) $", 18, 28),
                author = "paranoidi",
                email = "paranoidi@gmx.net",
                category = MYADDONS_CATEGORY_COMBAT,
                frame = "SmartAssistFrame",
                optionsframe = "SAOptionsFrame"
};

-- options
SA_OPTIONS_WRAPPER = {};
SA_OPTIONS = nil;

SA_MARKEDCACHE = {};
SA_PASSIVECACHE = {};
ASSIST_EMOTES = {};
PREVIOUS_ASSISTS = {};
PREVIOUS_NEAREST = false;
PREVIOUS_FALLBACK = false;
PREVIOUS_ASSIST_TIME = 0;
PREVIOUS_NEAREST_TIME = 0;
TARGET_CHANGED = false;

SMARTASSIST_CORE_REV = tonumber(string.sub("$Rev: 247 $", 7, -3));
BINDING_HEADER_SMARTASSIST = "SmartAssist";
BINDING_NAME_SASET = "Set puller";
BINDING_NAME_SAASSIST = "Assist";

SA_RUNNING = false;             -- used to disable sounds and detect target changes
PASSIVE_WARN_FLAG = false;      -- is passive warning visible

COLOR_DEFAULT = {r=0, g=0.8, b=1};
COLOR_ALERT = {r=0.9, g=0, b=0};

StaticPopupDialogs["SA_NO_BINDINGS"] = {
        text = TEXT("You must bind SmartAssist key before being able to use this addon!"),
        button1 = TEXT(ACCEPT),
        OnAccept = function()
                printInfo("Tip: Press ESC key and go to 'bind keys' options. SmartAssist keys should be there at somewhere near bottom of the list.");
        end,
        timeout = 0,
        whileDead = 1,
};

StaticPopupDialogs["SA_UPGRADE_RESETPOS"] = {
        text = TEXT("SmartAssist will need to reset frame positons due changes in the way scaling is used."),
        button1 = TEXT(ACCEPT),
        OnAccept = function()
                SA_ResetFramePos();             
        end,
        timeout = 0,
        whileDead = 1,
};

StaticPopupDialogs["SA_AUTOCONF"] = {
        text = TEXT("Auto configure Smart Actions?"),
        button1 = TEXT(ACCEPT),
        button2 = TEXT(CANCEL),
        OnAccept = function()
                SA_AutoConfigureSmartActions();
                printInfo("You should see SmartAssist options and verify detected settings. Use command /sa to access configuration.");
        end,
        timeout = 0,
        whileDead = 1,
};

StaticPopupDialogs["SA_DISABLE_ATTACK"] = {
        text = TEXT("You have 'attack on assist' enabled in interface options which will cause problems with SmartAssist.\n\nAllow SmartAssist to turn it off (recommended) ?"),
        button1 = TEXT(YES),
        button2 = TEXT(NO),
        OnAccept = function()
                SetCVar("assistAttack", "0");
        end,
        OnCancel = function()
                SA_OPTIONS.AutoAttackWhineIgnored = true;
        end,
        timeout = 0,
        whileDead = 1,
};

function SA_OnLoad()
        SLASH_SMARTASSIST1 = "/smartassist";
        SLASH_SMARTASSIST2 = "/sa";
        SlashCmdList["SMARTASSIST"] = function(msg)
                SA_Command(msg);
        end
end

function SA_InitOption(option, value)
        if (SA_OPTIONS[option]==nil) then SA_OPTIONS[option]=value; end;
end

------------------------------------------------------------------------------
-- event: variables loaded
------------------------------------------------------------------------------

function SA_VariablesLoaded()
        -- Register the addon in myAddOns
        if(myAddOnsFrame_Register) then
                myAddOnsFrame_Register(SmartAssistDetails);
        end
        printInfo("SmartAssist "..SmartAssistDetails.version.." loaded.")

        -- add item to every possible context menu      
        table.insert(UnitPopupMenus["PARTY"], "PULLER");
        table.insert(UnitPopupMenus["PLAYER"], "PULLER");
        table.insert(UnitPopupMenus["RAID"], "PULLER");
        
        UnitPopupButtons["PULLER"] = { text = TEXT("Set as puller"), dist = 0 };
                
        SA_Init();
end


function SA_Init() 

        -- load realm & char specific configs
        local id = SA_GetAccountID();
        if (SA_OPTIONS_WRAPPER[id]==nil) then
                printInfo("SmartAssist created new default settings for "..id);
                SA_OPTIONS_WRAPPER[id] = {};
        end
        SA_OPTIONS = SA_OPTIONS_WRAPPER[id];
                
        -- set default values to option variables if they do not exist

        -- before rev 66 we had no stored rev number so we need to make sure it's in such cases atleast zero
        SA_InitOption("Rev", 0);

        -- special cases where we need to reset variables when upgrading the addon from older version
        if (SA_OPTIONS.Rev<240 and SA_OPTIONS.Rev>0) then
                StaticPopup_Show("SA_UPGRADE_RESETPOS");        
        end
        
        -- basic options
        SA_InitOption("VisualWarning", true);
        SA_InitOption("AssistOnEmote", true);
        SA_InitOption("PriorizeHealth", true);
        SA_InitOption("PriorizeHealthValue", 35);
        SA_InitOption("FallbackTargetNearest", true);
        SA_InitOption("CheckNearest", false);
        SA_InitOption("NearestMustBePvP", false);
        SA_InitOption("NearestMustBeTargetting", true);
        SA_InitOption("AssistOnlyNearest", false);
        -- available assist list
        SA_InitOption("ShowAvailable", true);
        SA_InitOption("PreserveOrder", false);
        SA_InitOption("OutOfCombat", false);
        SA_InitOption("TankMode", false); -- todo: class based defaults
        SA_InitOption("ClassIconMode", 1);
        SA_InitOption("TargetIconMode", 3);
        SA_InitOption("HuntersMarkIconMode", 1);
        SA_InitOption("AddMyTarget", false);
        SA_InitOption("ListWidth", 150);
        SA_InitOption("ListScale", 1.0);
        SA_InitOption("ListOOCAlpha", 0.4);
        SA_InitOption("ListSpacing", -5);
        SA_InitOption("ListHorizontal", false);
        SA_InitOption("ListTwoRow", false);
        SA_InitOption("LockList", false);
        SA_InitOption("HideTBY", false);
        SA_InitOption("HideTitle", false);
        SA_InitOption("AudioWarning", true); -- todo: class based defaults
        SA_InitOption("LostAudioWarning", false); -- todo: class based defaults
        SA_InitOption("VerboseAcquiredAggro", true);
        SA_InitOption("VerboseLostAggro", false);
        SA_InitOption("TextAlpha", 0.9);
        SA_InitOption("IntelligentSplit", false);
        -- advanced
        SA_InitOption("DisableSliderValue", 15);
        SA_InitOption("DisableTargetNearest", true);
        SA_InitOption("DisablePriorityHealth", true);
        SA_InitOption("AssistKeyMode", 1);
        SA_InitOption("DisableAutoCastKeyMode", 2);
        SA_InitOption("VerboseAssist", true);
        SA_InitOption("VerboseIncoming", false);
        SA_InitOption("VerboseNearest", true);
        SA_InitOption("VerboseUnableToAssist", false);
        SA_InitOption("DisableAssistWithoutPuller", false);
        SA_InitOption("PauseResetsOrder", false);
        -- smart actions
        SA_InitOption("AssistSpells", {});
        SA_InitOption("TriggerAssist", true);
        -- geek (not in options)
        SA_InitOption("SoundLoseAggro", "Sound\\Spells\\EyeOfKilroggDeath.wav");
        SA_InitOption("SoundIncomingWoldBoss", "Sound\\Spells\\PVPFlagTaken.wav");
        SA_InitOption("SoundGainAggro", "AuctionWindowClose");
        -- development
        SA_InitOption("DebugLevel", 0);
        
        -- display auto configuration dialog on first run
        SA_InitOption("AutoConfiguredV2", false);
        if (not SA_OPTIONS.AutoConfiguredV2) then
                SA_OPTIONS.AutoConfiguredV2 = true;
                StaticPopup_Show("SA_AUTOCONF");
        end
                
        -- init auto cast on assist defaults for hunters
        if (SA_OPTIONS["AutoAssist"]==nil and UnitClass("player") == CLASSNAME_HUNTER) then
                SA_OPTIONS.AutoAssist = true;
                SA_OPTIONS.FallbackOnBusy = true;
                SA_OPTIONS.AutoAssistTexture = "Interface\\Icons\\INV_Weapon_Rifle_08";
                SA_OPTIONS.AutoAssistName = SPELL_AUTOSHOT;
        end
        SA_InitOption("AutoAssist", false);
        SA_InitOption("AutoPetAttack", false);
        SA_InitOption("AutoPetAttackBusy", false);

        -- check if should display 'attack on assist' disable dialog if not ignored
        SA_InitOption("AutoAttackWhineIgnored", false);
        SA_CheckForAssistAttack();
        
        -- prefered assitance order (just a guess, got better one?)
        SA_InitOption("ClassOrder",{    CLASSNAME_WARRIOR, CLASSNAME_HUNTER, 
                                                                        CLASSNAME_ROGUE, CLASSNAME_PALADIN, 
                                                                        CLASSNAME_DRUID, CLASSNAME_SHAMAN, CLASSNAME_MAGE, 
                                                                        CLASSNAME_WARLOCK, CLASSNAME_PRIEST } );
                                                                        
        -- store revision number to configuration, this can be used to detect some special cases where we need to reset
        -- exsisting options (ie. bug in assistance order list) in new releases
        SA_OPTIONS.Rev = SMARTASSIST_CORE_REV;
        
        ASSIST_EMOTES = { EMOTE_EVERYONE_ATTACK, EMOTE_ATTACK, EMOTE_HELP };
        
        if (SA_OPTIONS.ShowAvailable) then
                SAListFrame:Show();
        else
                SAListFrame:Hide();
        end
end

-- check if assist attack is enabled
function SA_CheckForAssistAttack()
        if (not SA_OPTIONS.AutoAttackWhineIgnored) then
                if (GetCVar("assistAttack")~="0" and SA_OPTIONS.AutoAssist and SA_OPTIONS.AutoAssistName) then
                        StaticPopup_Show("SA_DISABLE_ATTACK");
                end
        end
end

-------------------------------------------------------------------------------
-- WARNING ICON EVENTS
-------------------------------------------------------------------------------

SA_WARN_SPEED = 0.07;
SA_WARN_REFRESH = SA_WARN_SPEED;
SA_WARN_ALPHA = 0.4;
SA_WARN_ALPHA_UP = true;

function SA_WarnOnUpdate(elapsed)
        if (not PASSIVE_WARN_FLAG) then return; end;
        SA_WARN_REFRESH = SA_WARN_REFRESH - elapsed;
        if (SA_WARN_REFRESH > 0) then
                return;
        end
        SA_WARN_REFRESH = SA_WARN_SPEED;
        
        -- if target target is != nil (attacking someone) then REMOVE the icon
        if (UnitName("targettarget")~=nil) then
                SA_Debug("target target != nil .. remove the warning", 1);
                SA_ResetWarning();
                SA_AggroedTargetMsg();
                return;
        end

        if (SA_WARN_ALPHA_UP) then
                SA_WARN_ALPHA = SA_WARN_ALPHA + 0.1;
        else
                SA_WARN_ALPHA = SA_WARN_ALPHA - 0.1;
        end
        if (SA_WARN_ALPHA<0.4) then
                SA_WARN_ALPHA_UP = true;
                SA_WARN_ALPHA = 0.5;
        end
        if (SA_WARN_ALPHA>1.0) then
                SA_WARN_ALPHA_UP = false;
                SA_WARN_ALPHA = 0.9;
        end
        SAWarningFrame:SetAlpha(SA_WARN_ALPHA);
end

-------------------------------------------------------------------------------
-- handles smart assist slash commands
-------------------------------------------------------------------------------

function SA_Command(msg)
        local _,_,value = string.find(msg, "debug (%d)");
        if (value) then
                SA_OPTIONS.DebugLevel = tonumber(value);
                printInfo("Setting debug level to "..tostring(SA_OPTIONS.DebugLevel));
        elseif (msg=="available") then
                SA_ToggleAvailable();
        elseif (msg=="assist") then
                DoSmartAssist();
        elseif (msg=="reset" or msg=="reset all") then
                -- resets frame positions
                SA_ResetFramePos();
                                
                -- clears this character settings and initializes defaults
                if (msg=="reset all") then
                        printInfo("Reseting SmartAssist settings for all characters");
                        SA_OPTIONS_WRAPPER = {};
                else
                        printInfo("Reseting SmartAssist settings for this character");
                        local id = SA_GetAccountID();
                        SA_OPTIONS_WRAPPER[id] = {};
                end
                SA_Init();
        elseif (msg=="rev" or msg=="ver") then
                if (SMARTASSIST_REV) then
                        printInfo("SmartAssist revision "..SMARTASSIST_REV);
                else
                        printInfo("Unofficial or broken build! Revision number missing! Core rev "..SA_OPTIONS.Rev);
                end
        elseif (msg=="dump") then
                if (not DevTools_Dump) then
                        printInfo("Requires devtools");
                else
                        printInfo("Dumping internal variables:");
                        printInfo("PREVIOUS_ASSISTS: ");
                        DevTools_Dump(PREVIOUS_ASSISTS);
                        printInfo("PREVIOUS_NEAREST: "..tostring(PREVIOUS_NEAREST));
                        printInfo("TARGET_CHANGED: "..tostring(TARGET_CHANGED));
                        printInfo("SA_RUNNING: "..tostring(SA_RUNNING));
                end
        else
                printInfo("Available parameters:");
                printInfo("ver - Display addon version information.");
                printInfo("assist - Execute SmartAssist for macros. Or call DoSmartAssist()");
                printInfo("reset - Reset character settings and frame positions.");
                printInfo("reset all - Reset all characters settings and frame positions.");
                SA_ShowOptions();
        end
end

function SA_ResetFramePos()
        SAListFrame:ClearAllPoints();
        SAListFrame:SetPoint("TOP", "UIParent", "TOP", 0, -20);
        SAListFrame:StopMovingOrSizing();
        SAWarningFrame:ClearAllPoints();
        SAWarningFrame:SetPoint("TOP", "UIParent", "TOP", 5, -25);
        SAWarningFrame:StopMovingOrSizing();
        SAOptionsFrame:ClearAllPoints();
        SAOptionsFrame:SetPoint("CENTER", "UIParent", "CENTER", 0, 0);
        SAOptionsFrame:StopMovingOrSizing();
end

function SA_ShowOptions()
        -- open config, or warn about key bindings
        local key1, key2 = GetBindingKey("SAASSIST");
        if (key1==nil and key2==nil) then
                StaticPopup_Show("SA_NO_BINDINGS");
        else
                SAOptionsFrame:Show();
        end
end

-------------------------------------------------------------------------------
-- SMART ACTIONS
-------------------------------------------------------------------------------

local old_UseAction = UseAction;
function UseAction(m1, m2, m3)

        local name = SA_GetSlotName(m1);
        local assistAction = false;
        if (SA_TableIndex(SA_OPTIONS.AssistSpells, name) ~= -1) then
                assistAction = true;
        end

        -- no target selected and attack action
        -- treat targetting player as no target
        if (SA_OPTIONS.TriggerAssist and (UnitName("target")==nil or UnitIsUnit("target","player")) and assistAction) then
                SA_Debug("no target, initiating assist by action!", 1);
                FindTarget(false, false);
                -- target is not player selected so don't attack unless target already in combat
                if (not UnitAffectingCombat("target")) then
                        SA_Debug("not in combat, aborting action", 1);
                        SA_ShowWarning();
                        return; 
                end
        end
        
        -- if trying to attack friendly unit, initiate smart action
        
        -- UnitIsFriend bugs when dueling people in your party
        -- Added UnitPlayerControlled because on pve server as non flagged attacking the opposite faction initiated assist!
        if (assistAction and not UnitCanAttack("target", "player") and not (UnitPlayerControlled("target") and UnitFactionGroup("player")~=UnitFactionGroup("target"))) then
                SA_Debug("friendly target, assistAction = "..tostring(assistAction), 1);
                if (UnitCanAttack("player", "targettarget")) then
                        SA_Debug("friendly target, assisting", 1);
                        AssistUnit("target");
                else
                        -- supresses annoyance when attacking and ally has no target
                        if (assistAction) then 
                                SA_Debug("aborting action, supressing annoyance", 1);
                                return; 
                        end
                end
        end
        
        return old_UseAction(m1, m2, m3);
end

-------------------------------------------------------------------------------
-- implements shift click assist
-- should work with ANYWHERE where you can select unit
-------------------------------------------------------------------------------

local SA_PREV_ASSIST_TIME = 0;
function SA_PlayerTargetChanged()
        TARGET_CHANGED = true;

        -- target lost
        if (not UnitExists("target")) then
                SA_ResetWarning();
                if (not SA_RUNNING) then
                        PREVIOUS_ASSISTS = {};
                        PREVIOUS_NEAREST = false;
                end
                SA_List_Refresh();
                return; -- fixes issue when we have target already (fires two events, on lost and on gain)
        end     
        
        -- asisst by unit selection & modifier key
        if ( (IsShiftKeyDown() and SA_OPTIONS.AssistKeyMode==1) or 
             (IsControlKeyDown() and SA_OPTIONS.AssistKeyMode==2) or
             (IsAltKeyDown() and SA_OPTIONS.AssistKeyMode==3) ) 
        then
                -- this time check (hoax) removes looping, when selecting player that has friendly taget. Without this 
                -- it goes there and starts to assist him
                -- todo: this can be done better .. if we check what's the target!
                
                local now = time();
                local dif = now - SA_PREV_ASSIST_TIME;
                if (dif == 0) then return; end
                SA_PREV_ASSIST_TIME = now;

                if (UnitCanAssist("player", "target") and
                        UnitCanAttack("player", "targettarget"))
                then
                        SAMsgFrame:AddMessage("Assisting "..UnitName("target"), 0.0, 0.8, 0.0, 1, 2.5);
                        AssistUnit("target");
                        SA_PostAssist();
                else
                        if (UnitExists("target") and not UnitIsDead("target")) then
                                UIErrorsFrame:AddMessage("Unable to assist", 1.0, 0, 0, 1.0, 1.5);
                        end
                end
        end
        
        SA_List_Refresh();
end

------------------------------------------------------------------------------
-- credits to popup assist author, refactored a bit tough :)
------------------------------------------------------------------------------

local old_UnitPopup_OnClick = UnitPopup_OnClick;
function UnitPopup_OnClick(index)
        local dropdownFrame = getglobal(UIDROPDOWNMENU_INIT_MENU);
        local unit = dropdownFrame.unit;
        if ( this.value == "PULLER" ) then
                SA_SetPuller(unit);
        end
        return old_UnitPopup_OnClick(index);
end

------------------------------------------------------------------------------
-- param: unit - ID to be set as puller
-- set unit as puller
------------------------------------------------------------------------------

function SA_SetPuller(unit)
        if (UnitIsFriend(unit, "player")) then
                local candidates,_ = SA_GetCandidates(true);
                -- check that current puller exists in party, if not clear
                if (not candidates[UnitName(unit)]) then
                        SAMsgFrame:AddMessage("Puller must be in party / raid", 1.0, 1.0, 1.0, 1, 3);
                else
                        SA_OPTIONS["puller"] = UnitName(unit);
                        SAMsgFrame:AddMessage("The assigned puller is now "..SA_OPTIONS["puller"], 1.0, 1.0, 1.0, 1, 1.5);
                end
        else
                SA_OPTIONS["puller"] = nil;
                SAMsgFrame:AddMessage("Assigned puller cleared", 1.0, 1.0, 1.0, 1, 1.5);
        end
        -- just incase that option window is visible, update the text
        SA_Options_UpdatePullerText();
end

------------------------------------------------------------------------------
-- credits to mozz
-- this is used to disable some anying unit deselect sounds while assisting 
-- (we have to clear the selected unit in some cases)
------------------------------------------------------------------------------

local old_PlaySound = PlaySound;
function PlaySound(name)
    if (not SA_RUNNING) then
        return old_PlaySound(name);
    end
        if (name ~= "INTERFACESOUND_LOSTTARGETUNIT" and name ~= "igCharacterNPCSelect" ) then
                return old_PlaySound(name);
        end
end

------------------------------------------------------------------------------
-- main method, this is called when smartassist does its thing
------------------------------------------------------------------------------

function DoSmartAssist()
    SA_RUNNING = true;
    -- if has friendly target, straight assist it
    local f = true;
    if (UnitExists("target")) then
            if (not UnitCanAttack("target", "player") and not (UnitPlayerControlled("target") and UnitFactionGroup("player")~=UnitFactionGroup("target"))) then
                if (SA_OPTIONS.VerboseAssist) then
                        SA_Verbose("Assisting selected player "..UnitName("target"));
                end
                AssistUnit("target");
                f = false;
            end
        end
        if (f) then
                FindTarget(true, false);
        end
        SA_RUNNING = false;
        SA_PostAssist();
        SA_List_Refresh();
end

------------------------------------------------------------------------------
-- post assist logic (after target has been selected)
------------------------------------------------------------------------------

function SA_PostAssist()
        -- auto-assist, disable if certain modifier key is down
        if ( ((not IsShiftKeyDown() and SA_OPTIONS.DisableAutoCastKeyMode==1) or 
              (not IsControlKeyDown() and SA_OPTIONS.DisableAutoCastKeyMode==2) or
              (not IsAltKeyDown() and SA_OPTIONS.DisableAutoCastKeyMode==3)) and 
              UnitExists("target") and 
                  UnitAffectingCombat("target") ) 
        then
                if (SA_OPTIONS.AutoAssist and SA_OPTIONS.AutoAssistName) 
                then
                        SA_Debug("Target already in combat, assisting with "..SA_OPTIONS.AutoAssistName, 3);
                        CastSpellByName(SA_OPTIONS.AutoAssistName); -- casts highest level
                end
                if ((SA_OPTIONS.AutoPetAttack and UnitExists("pet") and not UnitIsDead("pet") and not UnitExists("pettarget")) or
                        (SA_OPTIONS.AutoPetAttack and UnitExists("pet") and not UnitIsDead("pet") and SA_OPTIONS.AutoPetAttackBusy))
                then
                        SA_Debug("Attacking with pet", 3);
                        PetAttack();
                end
        end
        -- visual alert, if needed
        SA_ShowWarning();
end

------------------------------------------------------------------------------
-- receives all emote events, implements assist by emote
------------------------------------------------------------------------------

function SA_EmoteAssist(arg1)
        if (not SA_OPTIONS.AssistOnEmote) then
                return;
        end
        -- todo idea: hitting assist key should go back to previous target (if possible)
        -- parse emotes
        for k,v in ASSIST_EMOTES do
                local sp, ep, name = string.find(arg1, "(%w+) "..v);
                if (name ~= nil) then
                        SA_Debug("assist emote detected, name="..name, 1);
                end
                -- if name exists, we have a match
                if name and name ~= EMOTE_OWN then
                        local candidates,_ = SA_GetCandidates(true);
                        if (candidates[name]~=nil) then
                                SAMsgFrame:AddMessage("Assisting "..name, 0.0, 0.8, 0.0, 1, 2.5);
                                AssistUnit(candidates[name].unitId);
                                return true;
                        end
                        SA_Debug("not assisting "..name..", not in party/raid", 1);
                end
        end
end

function SA_UnitHealth(arg1)
        -- flag is used to reduce overheading, this gets called a lot ..
        if (not PASSIVE_WARN_FLAG) then
                return;
        end
        if (arg1=="target") then
                SA_ResetWarning();
                -- if someone else attacked the target, show another text saying its ok to start blasting
                -- this is not idiot proof check, seems to work fine tough
                if (not UnitIsUnit("targettarget", "pet") and not UnitIsUnit("targettarget", "player")) then
                        SA_Debug("Someone else has attacked our target, show notification", 4);
                        SA_AggroedTargetMsg();
                else
                        SA_Debug("player has most likelly attacked the target", 4);
                        -- todo: perhaps send pull message to party?
                end
        end
        -- todo idea: we could watch other players health here and suggest assist
end

-----------------------------------------------------------------
-- show warning if target is not in combat and feature is enabled
-----------------------------------------------------------------
function SA_ShowWarning()
        if (not UnitAffectingCombat("target") and UnitExists("target") and 
                UnitName("targettarget")==nil and SA_OPTIONS.VisualWarning and
                not UnitPlayerControlled("target")) 
        then
                PASSIVE_WARN_FLAG = true;
                SAWarningFrame:Show();
                return true;
        end
end

function SA_AggroedTargetMsg()
        -- the old msg was annoying as hell, replaced with SCT message, only if available
        -- todo: add as option!
        if (SCT_Display) then
                SCT_Display(message, COLOR_DEFAULT);
        end
end

function SA_ResetWarning()
        PASSIVE_WARN_FLAG = false;
        SAWarningFrame:Hide();
end

-------------------------------------------------------------
-- get distance unit is in, 0 if out of range
-- this funnly looking nesting makes minimal calls to api.
-- ie. No need to check for closer ranges if unit is not even
-- in 28yard range
-------------------------------------------------------------

-- TODO: NOT USED ATM!

function SA_GetDistance(unit)
        if (CheckInteractDistance(unit, 4)) then
                if (CheckInteractDistance(unit, 3)) then
                        if (CheckInteractDistance(unit, 2)) then
                                if (CheckInteractDistance(unit, 1)) then
                                        return 1;
                                end
                                return 2;
                        end
                        return 3;
                end
                return 4;
        end
        return -1;
end

-----------------------------------------------------------------------------------------
-- construct and return one candidate
-- if unit has invalid flag it should not be used because it does not contain all fields
-- foexample pets that are very far away do not have all fields present
-----------------------------------------------------------------------------------------

function SA_GetCandidate(unit, i)
        local candidate = {};
        candidate["unitName"] = UnitName(unit..i);
        candidate["unitId"] = unit..i;
        candidate["target"] = unit..i.."target";
        candidate["health"] = ceil( UnitHealth( unit..i ) / UnitHealthMax( unit..i ) * 100 );
        candidate["class"] = UnitClass(unit..i);
        candidate["dead"] = UnitIsDead(unit..i);
        if (string.find(unit, "pet") ~= nil) then
                candidate["pet"] = true;
        end
        if (candidate.unitName == nil) then 
                --SA_DebugCandidate(candidate);
                candidate["invalid"] = true; 
        end
        if (candidate.class == nil) then
                --SA_DebugCandidate(candidate);
                candidate["invalid"] = true; 
        end
        if (candidate.health == nil) then
                --printInfo("Problem with smartassist; unit health is unknown!");
                candidate["invalid"] = true; 
                SA_DebugCandidate(candidate);
        end
        return candidate;
end

function SA_GetPlayerAsCandidate()
        local candidate = {};
        candidate["unitName"] = UnitName("player");
        candidate["unitId"] = "player";
        candidate["target"] = "target";
        candidate["health"] = ceil( UnitHealth( "player" ) / UnitHealthMax( "player" ) * 100 );
        candidate["class"] = UnitClass("player");
        candidate["dead"] = UnitIsDead("player");
        return candidate;
end

------------------------------------------------------------------
-- return iteration info: is this party or raid, how many members
------------------------------------------------------------------

function SA_GetIterInfo()
        local mode, members = nil;
        if (GetNumRaidMembers()>0) then
                mode = "raid";
                members = GetNumRaidMembers();
        elseif (GetNumPartyMembers()>0) then
                mode = "party";
                members = GetNumPartyMembers();
        end
        return mode, members;
end

------------------------------------------------------------------
-- converts candidate list to map version
-- this is used in some places where we need list AND map versions
------------------------------------------------------------------

function SA_ConvertCandidateListToMap(candidates)
        local map = {};
        for k,v in candidates do
                map[v.unitName] = v;
        end     
        return map;
end

-------------------------------------------------------------
-- params: if map is true then as a map where key is unitName
--         if map is false then as list
-- return list of possible assistable units
-------------------------------------------------------------

local UNIQUE_WARNED = false;
function SA_GetCandidates(map)
        local candidates = {};
        local mode, members = SA_GetIterInfo(); -- got stack overflow once here
        local pullerAdded = false;

        -- for soloing and party, add our pet
        if (UnitExists("pet")) then
                local op = SA_GetCandidate("pet", "");
                if (not op.invalid and not op.dead) then 
                        -- fixes problem when there are multiple pets with same name
                        if (SA_OPTIONS["puller"] == op.unitName) then
                                pullerAdded = true;
                        end
                        
                        if (map) then
                                candidates[op.unitName] = op;
                        else
                                table.insert(candidates, op);
                        end
                end
        end
        
        -- if no party/raid, abort here
        if (members==nil) then return candidates, 0; end;

        local myname = UnitName("player");

        for i = 1, members do
                local candidate = SA_GetCandidate(mode, i);
                -- do not add invalid, dead or myself
                if (not candidate.invalid and not candidate.dead and candidate["unitName"] ~= myname) then
                        if (map) then
                                candidates[candidate.unitName] = candidate;
                        else
                                table.insert(candidates, candidate);
                        end
                
                        -- add pet to list if has one
                        if (UnitExists(mode.."pet"..i)) then
                                local pcandidate = SA_GetCandidate(mode.."pet", i);
                                if (not pcandidate.invalid and not pcandidate.dead) then
                                        -- hotfix for unique pet puller problem
                                        -- players are unique by server so we don't have to worry about them
                                        if (SA_OPTIONS["puller"] == pcandidate.unitName and pullerAdded) then
                                                if (not UNIQUE_WARNED) then
                                                        printInfo("SMARTASSIST PROBLEM: Puller is not unique!");
                                                        UNIQUE_WARNED = true;
                                                end
                                        else
                                                if (map) then
                                                        candidates[pcandidate.unitName] = pcandidate;
                                                else
                                                        table.insert(candidates, pcandidate);
                                                end
                                                
                                                -- if added puller unit, set flag that no other unit with same name can be added
                                                if SA_OPTIONS["puller"] == pcandidate.unitName then
                                                        pullerAdded = true;
                                                end
                                        end                             
                                end
                        end
                end
        end
        
        return candidates, members;
end

------------------------------------------------------------------------------
-- if we have enabled filtering targets, remove those candidates who have 
-- something else. It was much more convient to implement this way rather 
-- than to modify get candidates and gazillion other places
------------------------------------------------------------------------------

function SA_FilterCandidates(candidates, map)
        local filtered = {};
        for key,candidate in candidates do
                if (UnitExists(candidate.target) and string.find(string.lower(UnitName(candidate.target)), SA_OPTIONS.Filter)) then
                        if (map) then
                                filtered[key] = candidate;
                        else
                                table.insert(filtered, candidate);
                        end
                end
        end
        return filtered;
end

function SA_FilterCandidatesByDistance(candidates, map)
        --debugprofilestart();
        
        local filtered = {};
        for key,candidate in candidates do
                if (CheckInteractDistance(candidate.unitId, 4)) then
                        if (map) then
                                filtered[key] = candidate;
                        else
                                table.insert(filtered, candidate);
                        end
                end
        end
        
        --SA_Debug("filtering by distance took "..debugprofilestop().." ms", 1);
        return filtered;
end

------------------------------------------------------------------------------
-- sort method for candidates
------------------------------------------------------------------------------

local detected_bug = false;
function SA_SortCandidate(a, b, members)
        -- TODO: INVESTIGATE, but HOW?
        -- this is added to debug odd behaviour in molten core where crash occured (b was nil, crashed at priorize health)
        -- OKAY: found out the cause, if there are two units with same name (pet. ie. Cat) and your own is set as puller, 
        -- this should be fixed in getCandidates but is UNTESTED, remove this when tested
        -- Update 20.6.2006 - still bugs on VERY rare occasions!
        if (detected_bug==false and (a==nil or b==nil)) then
                printInfo("?? BUG IN SMARTASSIST:");
                local cand,_ = SA_GetCandidates(false);
                local old_state = SA_OPTIONS.Debug;
                SA_OPTIONS.Debug = true;
                SA_DebugCandidates(cand);
                SA_OPTIONS.Debug = old_state;
                detected_bug = true;
        end
        
        -- priority health always first
        -- TODO: DOES NOT TAKE ACCOUNT THE MEMBERS > n DISABLING !!! <----------------xxxxxxxxxxxxxxxxxxxxx---------------xxxxxxxxxxxxxxxxxxxx------------xxxxxxxxxxxxxx
        -- SHOULD DO NOW! TESTING!!! xxxxxxxxxxxxxxx
        local priorize = SA_OPTIONS.PriorizeHealth;
        if (members > SA_OPTIONS.DisableSliderValue and SA_OPTIONS.DisablePriorityHealth) then
                priorize = false;
        end
        if (priorize) then
                if (a.health < SA_OPTIONS.PriorizeHealthValue or b.health < SA_OPTIONS.PriorizeHealthValue)
                then
                        return a.health < b.health;
                end
        end
        
        -- depriorize players having passive target
        if (SA_PASSIVECACHE[a.unitName] and not SA_PASSIVECACHE[b.unitName]) then return false; end;
        if (SA_PASSIVECACHE[b.unitName] and not SA_PASSIVECACHE[a.unitName]) then return true; end;
        
        -- our puller should be always top priority
        if (a.unitName == SA_OPTIONS["puller"]) then return true; end
        if (b.unitName == SA_OPTIONS["puller"]) then return false; end
        
        -- priorize players whose target is marked
        if (SA_MARKEDCACHE[a.unitName] and not SA_MARKEDCACHE[b.unitName]) then return true; end;
        if (SA_MARKEDCACHE[b.unitName] and not SA_MARKEDCACHE[a.unitName]) then return false; end;
        
        -- de-priorize pets
        if (a.pet and not b.pet) then return false; end;
        if (b.pet and not a.pet) then return true; end;
        
        -- CT RaidAssist support, if we have main tanks set in CT RaidAssist prefer them
        if (CT_RA_MainTanks ~= nil) then
                local a_tank, b_tank = false;
                for _,v in CT_RA_MainTanks do
                        if (a.unitName == v) then a_tank=true; end
                        if (b.unitName == v) then b_tank=true; end
                end
                -- if both are CTRA tanks, sort by priority (mt first!) -- TODO: perhaps put OT first ?
                if (a_tank and b_tank) then 
                        return SA_TableIndex(CT_RA_MainTanks, a.unitName) < SA_TableIndex(CT_RA_MainTanks, b.unitName); 
                end
                
                if (a_tank) then return true; end
                if (b_tank) then return false; end
        end
        
        -- if we have same class, sort secondary by health
        -- TODO: add option to disable this and use alphabetic order instead (should keep the list more stable)
        -- perhaps use alphabetic order when in groups larger than > n
        -- 20.6.2006 - testing out disabling if larger than > n
        if (a.class == b.class) then
                if (priorize) then
                        return a.health < b.health;
                else
                        return a.unitName < b.unitName;
                end
        end
        
        -- and last, teh normal case where sorted by class
        return SA_TableIndex(SA_OPTIONS.ClassOrder, a.class) < SA_TableIndex(SA_OPTIONS.ClassOrder, b.class);
end

------------------------------------------------------------------------------
-- Makes sure that the puller is in the party and if not clear / set to pet.
-- This gets called everytime assist is used
------------------------------------------------------------------------------

function SA_RefreshPuller(candidatelist)
        -- check that current puller exists in party, if not clear
        local candidates = SA_ConvertCandidateListToMap(candidatelist);
        if (SA_OPTIONS["puller"]~=nil and candidates[SA_OPTIONS["puller"]]==nil) then
                SA_Debug("Puller does not exist in candidates, clearing", 3);
                SA_OPTIONS["puller"]=nil;
        end
        
        -- if there is no puller and we have pet, set it as one
        if (SA_OPTIONS["puller"]==nil and UnitExists("pet") and not UnitIsDead("pet")) then
                SA_Debug("no puller set, pet exists -> setting it as one", 3);
                SA_OPTIONS["puller"] = UnitName("pet");
        end
end

------------------------------------------------------------------------------------------
-- params:
-- allowNearest = allow fallback to target nearest
-- recursive = is this recursive call, if it is we cannot add outside targets to skip list
-- Todo: better way for recursive?
------------------------------------------------------------------------------------------

function FindTarget(allowNearest, recursive)
        -- reset PREVIOUS_FALLBACK, store real value in local variable. Easier this way than to handle all returns ...
        local previous_fallback = PREVIOUS_FALLBACK;
        PREVIOUS_FALLBACK = false;

        local candidates, members = SA_GetCandidates(false);
        
        -- reset order if target is kept over 3s
        if (time() - PREVIOUS_ASSIST_TIME > 3 and SA_OPTIONS.PauseResetsOrder) then
                SA_Debug("over 3s since last assist, reseting PREVIOUS_ASSISTS", 2);
                PREVIOUS_ASSISTS = {};
                PREVIOUS_ASSIST_TIME = time();
        end
        
        -- allow targeting nearest attacking again
        if (time() - PREVIOUS_NEAREST_TIME > 5) then
                SA_Debug("over 5s since last assist, reseting PREVIOUS_NEAREST", 2);
                PREVIOUS_NEAREST = false;
                PREVIOUS_NEAREST_TIME = time();
        end
        
        if (SA_OPTIONS.Filter) then
                SA_Debug("filtering with "..tostring(SA_OPTIONS.Filter), 2);
                candidates = SA_FilterCandidates(candidates, false);
        end
        
        -- filter out candidates out of range if assisting only members nearby
        if (SA_OPTIONS.AssistOnlyNearest) then
                candidates = SA_FilterCandidatesByDistance(candidates, false);
        end
        
        SA_RefreshPuller(candidates);
        
        -- if we should not assist anyone from raid without puller, clear the candidates list
        if (SA_OPTIONS.DisableAssistWithoutPuller and SA_OPTIONS["puller"]==nil) then
                SA_Debug("DisableAssistWithoutPuller and no puller -> clearing candidates list", 2);
                candidates = {};
        end
        
        -- try to target nearest before going to assist from raid, note that this is different from allowNearest
        if (SA_OPTIONS.CheckNearest and not PREVIOUS_NEAREST and not recursive) then
                --local pre_valid = isValidTarget("target");
                -- okei, eli jos target nearest ei vaiha targettia niin se tuo myöhempi else palauttaa edellisen assistauksen targetin -> bug bug!
                TARGET_CHANGED = false;
                TargetNearestEnemy();
                SA_Debug("check nearest got = "..tostring(UnitName("target")),1);
                local valid = isValidTarget("target") and UnitAffectingCombat("target");
                if (SA_OPTIONS.NearestMustBePvP) then
                        if (not UnitPlayerControlled("target")) then 
                                valid = false;
                        end
                end
                if (SA_OPTIONS.NearestMustBeTargetting) then
                        --if (UnitIsUnit("targettarget", "player")) then
                        if (UnitName("targettarget") ~= UnitName("player")) then
                                valid = false;
                        end
                end
                if (valid) then
                        SA_Debug("*** found good target from check nearest target="..tostring(UnitName("target")),1);
                        if (SA_OPTIONS.VerboseNearest) then
                                SA_Verbose("Targeted nearest", ALERT_COLOR);
                        end
                        PREVIOUS_NEAREST = true;
                        return;
                else
                        if (TARGET_CHANGED) then
                                SA_Debug("invalid check nearest, restored target = "..tostring(UnitName("target")),1);
                                TargetLastTarget();
                        end
                end
        end
        PREVIOUS_NEAREST = false;

        -- store table size to variable because iterating it multiple times is no good
        table.sort(candidates, function(a,b) return SA_SortCandidate(a,b,members) end);
        
        for _,candidate in candidates do
                -- this is ingenious loop which determines if current candidate target has been targetted on previous assists
                -- not a idiot proof check since previous party members might have changed target since then, but works amazingly well
                -- atleast preventing situation where multiple members have same target and you press assist key multiple times
                
                -- 18.1.2006 - changed to show already targetted msg to test if its futile
                -- 21.1.2006 - this is triggered _multiple_ times per assist on some occasions, seems to be when there is only one target..
                
                local previously_targetted = false;
                for assisted,_ in PREVIOUS_ASSISTS do
                        if (UnitExists(assisted)) then
                                if (UnitIsUnit(candidate.target, assisted.."target")) then
                                        --SA_Debug("** already targetted once "..tostring(UnitName(assisted)));
                                        previously_targetted = true;
                                        break;
                                end
                        end
                end
                
                -- if current target is same as our candidate, consider it "assisted" (in other words: skip it)
                -- added 24.1.2006 - this should make cycling trough enemies work more smoothly
                if (UnitExists("target") and UnitIsUnit(candidate.target, "target")) then
                        SA_Debug(tostring(candidate.unitId).." has same target as we ("..tostring(UnitName("target")).."), consider this unit as assisted", 3);
                        PREVIOUS_ASSISTS[candidate.unitId] = true;
                end
                
                -- test each candidate, skips previously assisted UNLESS it has health below critical value
                local priority_health = candidate.health < SA_OPTIONS.PriorizeHealthValue and SA_OPTIONS.PriorizeHealth;
                
                if (members > SA_OPTIONS.DisableSliderValue and SA_OPTIONS.DisablePriorityHealth) then
                        priority_health = false;
                end
                
                if ( (PREVIOUS_ASSISTS[candidate.unitId]==nil or priority_health) and (not previously_targetted) ) 
                then
                        -- test if candidate (partyN, pet, raidN etc) has valid target
                        if (UnitCanAssist("player", candidate.unitId) and isValidTarget(candidate.target)) 
                        then
                                if (SA_OPTIONS.VerboseAssist) then
                                        if (priority_health) then
                                                SA_Verbose("Priority assisting "..candidate.unitName.." ("..candidate.health.."%)", COLOR_ALERT);
                                        else
                                                SA_Verbose("Assisting "..candidate.unitName);
                                        end
                                end
                                SA_Debug("Found a good target from "..candidate.unitName.." ("..candidate.unitId..")", 3);
                                AssistUnit(candidate["unitId"]);
                                PREVIOUS_ASSISTS[candidate.unitId] = true;
                                return;
                        end
                else
                        SA_Debug("** Skipping "..candidate.unitName.." ("..candidate.unitId..")", 4);
                end
        end
        
        -- if we have skiplist, now is good time to clear it and try again with all members, recursive call is only made once
        if (SA_TableSize(PREVIOUS_ASSISTS)>0 and not recursive) then
                SA_Debug("** Unable to assist anyone but we have skiplist ("..SA_TableSize(PREVIOUS_ASSISTS)..") -> clearing it and trying again..", 3);
                PREVIOUS_ASSISTS = {};
                return FindTarget(allowNearest, true);
        end
        
        -- we might had good target already when assist was used, if there are no other targets available we must exit now
        -- falling back to target nearest in that case would be idiotic
        -- 29.7.2006 - do not abort on good target if previously acquired target using fallback to nearest, 
        -- this allows toggling targets using smartassist key like TAB key.
        if (isValidTarget("target") and not previous_fallback) then
                SA_Debug("had already good target", 3);
                return;
        end
        
        if (SA_OPTIONS.FallbackTargetNearest and allowNearest) then
                if (SA_OPTIONS.DisableTargetNearest and members > SA_OPTIONS.DisableSliderValue) then
                        if (SA_OPTIONS.VerboseUnableToAssist) then
                                printInfo("Unable to assist anyone. Targetting nearest is suspended in groups this large.");
                        end
                        return;
                end
                if (SA_OPTIONS.VerboseUnableToAssist) then
                        printInfo("Unable to assist anyone. Trying to target nearest enemy.");
                end
                TargetNearestEnemy();
                if (not isValidTarget("target")) then
                        ClearTarget();
                else
                        PREVIOUS_FALLBACK = true;
                        SA_Debug("fallback to target nearest found good target", 2);
                end
        end     
end