• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] GroupSelectionMimic

Not tested enough: losing selection by remove unit, death or loading did bug the System.
This should be fixed with V1.1

Can Still Bug out, if one uses the SoulStone ability, Through that can be fixed by Hand by calling GroupSelectionMimic.RemoveUnit(GetSpellTargetUnit()).

Fails to detect focus change done by left cklicking an current selected unit in the Group selection Frame , the one at the bottom.
Also Fails to detect losing selection when reincarnation triggers.

V1.2:
Does now filter tab clicks when 1 or no unit is selected to Prevent game freeze by endless loop.

V1.3:
This FirstHandle Caching was bullshit and somehow worked on the test map. In reality non heroes use their unitCode like 'hfoo', while heroes use GetHandleId, If prio values colide.

V1.4:
Creates now Life Drops below equal to 0.45 Life triggers on selection, on deselection destroys them. That way reincarnation is also detected. The any death Event was removed.
Functions names are now all in camelCase, while Other table Values start with an UperCase.

V1.5
requries now Global Initialization by Bribe.

V1.6
uses new Global Init, local for GroupSelectionMimic.DeathTrigger

I wrote this system to know which unit is currently the main focused unit. I needed this feature to display the Name/Icon/hp/mana/exp for the main focused unit for custom UI which still support groupselection.

Lua:
--[[
V1.6
requires: Global Initialization 3.0.0.1 by Bribe https://www.hiveworkshop.com/threads/global-initialization.317099/#post-3535035
GroupSelectionMimic is an system that mimics warcraft 3 group selection order.
That is done so one knows which unit is in current main focus when having multiple units selected.

This is an synced resource hence it might work wrong when having bad latency Through that would have to be tested.

One can get the current focuse selected unit with 
function GroupSelectionMimic.getFocusUnit(player)
--]]

--hook into remove and showUnit
--RemoveUnit and ShowUnit do not throw an deselection event hence hook in.
do
    local realShowUnit = ShowUnit
    function ShowUnit(whichUnit, show)
        if not show then
            GroupSelectionMimic.removeUnit(whichUnit)
        end
        realShowUnit(whichUnit,show)
    end
    local realRemoveUnit = RemoveUnit
    function RemoveUnit(whichUnit)
        GroupSelectionMimic.removeUnit(whichUnit)
        realRemoveUnit(whichUnit)
    end
end

do
    GroupSelectionMimic = {}
    local deathTable
    OnTrigInit(function()
        GroupSelectionMimic.SelectedUnits = {}
        GroupSelectionMimic.DeselectionTrigger = CreateTrigger()
        GroupSelectionMimic.KeyTrigger = CreateTrigger() --pressing tab
        GroupSelectionMimic.LoadedTrigger = CreateTrigger() --needed when units can be loaded into transporters
        GroupSelectionMimic.SelectionTrigger = CreateTrigger()
        GroupSelectionMimic.DeathTrigger = {} --death, reincarnation
        deathTable = GroupSelectionMimic.DeathTrigger
        GroupSelectionMimic.SummonTrigger = CreateTrigger() --needed for storm earth fire

        --add arrays for players and add Events
        ForForce(bj_FORCE_ALL_PLAYERS, function()
            local player = GetEnumPlayer()
            GroupSelectionMimic.SelectedUnits[player] = {Selected = 1, Running = false}
            BlzTriggerRegisterPlayerKeyEvent(GroupSelectionMimic.KeyTrigger, player, OSKEY_TAB, 0, true) 
            BlzTriggerRegisterPlayerKeyEvent(GroupSelectionMimic.KeyTrigger, player, OSKEY_TAB, 1, true) --shift
            BlzTriggerRegisterPlayerKeyEvent(GroupSelectionMimic.KeyTrigger, player, OSKEY_TAB, 2, true) --ctrl
            BlzTriggerRegisterPlayerKeyEvent(GroupSelectionMimic.KeyTrigger, player, OSKEY_TAB, 3, true) --shift + ctrl
            TriggerRegisterPlayerUnitEvent(GroupSelectionMimic.SelectionTrigger, player, EVENT_PLAYER_UNIT_SELECTED, nil)
            TriggerRegisterPlayerUnitEvent(GroupSelectionMimic.DeselectionTrigger, player, EVENT_PLAYER_UNIT_DESELECTED, nil)
        end)
        TriggerRegisterAnyUnitEventBJ(GroupSelectionMimic.LoadedTrigger, EVENT_PLAYER_UNIT_LOADED)
        TriggerRegisterAnyUnitEventBJ(GroupSelectionMimic.SummonTrigger, EVENT_PLAYER_UNIT_SUMMON)
        

        TriggerAddAction(GroupSelectionMimic.KeyTrigger, function()
            local player = GetTriggerPlayer()
            local playerData = GroupSelectionMimic.SelectedUnits[player]    
            if #playerData <= 1 then return end --do nothing, if only one or less is selected
            local currentUnitCode = GetUnitTypeId(playerData[playerData.Selected])
                    
            if BlzBitAnd(BlzGetTriggerPlayerMetaKey(), 1) == 1 then
                if not IsUnitType(playerData[playerData.Selected], UNIT_TYPE_HERO) then
                    --jump to prev group
                    while (GetUnitTypeId(playerData[playerData.Selected]) == currentUnitCode)
                    do
                        playerData.Selected = playerData.Selected - 1
                    end
                else
                    playerData.Selected = playerData.Selected - 1
                end
            else
                if not IsUnitType(playerData[playerData.Selected], UNIT_TYPE_HERO) then
                    --jump to next group
                    while (GetUnitTypeId(playerData[playerData.Selected]) == currentUnitCode)
                    do
                        playerData.Selected = playerData.Selected + 1
                    end
                else
                    playerData.Selected = playerData.Selected + 1
                end
            end
        
            --overflow
            if playerData.Selected > #playerData then playerData.Selected = 1 end
            if playerData.Selected < 1 then playerData.Selected = #playerData end
        
            --debug display the new focus unit
            --print(playerData.Selected, GetUnitName(playerData[playerData.Selected]))
        end)

        TriggerAddAction(GroupSelectionMimic.SelectionTrigger, function()
            --print("Select")
            GroupSelectionMimic.addUnitToPlayer(GetTriggerPlayer(), GetTriggerUnit())
        end)
        
        TriggerAddAction(GroupSelectionMimic.DeselectionTrigger, function()
            local player = GetTriggerPlayer()
            local playerData = GroupSelectionMimic.SelectedUnits[player]
            local triggerUnit = GetTriggerUnit()   
            GroupSelectionMimic.removeUnitFromPlayerData(playerData, triggerUnit)
        end)

        TriggerAddAction(GroupSelectionMimic.LoadedTrigger, function()
            GroupSelectionMimic.removeUnit(GetTriggerUnit())
        end)

        TriggerAddAction(GroupSelectionMimic.SummonTrigger, function()
            if GetUnitCurrentOrder(GetSummoningUnit()) == OrderId("elementalfury") then
                GroupSelectionMimic.removeUnit(GetSummoningUnit())
                GroupSelectionMimic.addUnitToPlayer(GetTriggerPlayer(), GetSummonedUnit(), true)
            end
        end)
    end)

    function GroupSelectionMimic.getFocusUnit(player)
        local playerData = GroupSelectionMimic.SelectedUnits[player]
        return playerData[playerData.Selected]
    end

    function GroupSelectionMimic.debugPrint()
        local playerData = GroupSelectionMimic.SelectedUnits[Player(0)]
        print("Selected: ",playerData.Selected, GetUnitName(playerData[playerData.Selected]))
        for key, value in ipairs(playerData)
        do  
            print(key, GetUnitName(value), BlzGetUnitRealField(value, UNIT_RF_PRIORITY), GroupSelectionMimic.getOrderValue(value))
        end
    end

    function GroupSelectionMimic.getOrderValue(unit)
        --heroes use the handleId
        if IsUnitType(unit, UNIT_TYPE_HERO) then
            return GetHandleId(unit)
        else
        --units use unitCode
        return GetUnitTypeId(unit)
        end
    end
    function GroupSelectionMimic.createDeathDetect(unit)
        if deathTable[unit] then
            deathTable[unit].Counter = deathTable[unit].Counter + 1
        else
            --print("Create DeathTrigger")
            local deathTrigger = CreateTrigger()
            deathTable[unit] = {}
            deathTable[unit].Counter = 1
            deathTable[unit].Trigger = deathTrigger
            deathTable[unit].TriggerAction = TriggerAddAction(deathTrigger, function()
                GroupSelectionMimic.removeUnit(GetTriggerUnit())
            end)
            TriggerRegisterUnitLifeEvent(deathTrigger, unit, LESS_THAN_OR_EQUAL, 0.45)
        end
    end
    function GroupSelectionMimic.destroyDeathDetect(unit)
        if not deathTable[unit] then return end --do nothing, if there is none
        deathTable[unit].Counter = deathTable[unit].Counter - 1
        if deathTable[unit].Counter < 1 then
            --print("Destroy DeathTrigger")
            TriggerRemoveAction( deathTable[unit].Trigger,  deathTable[unit].TriggerAction)
            DestroyTrigger(deathTable[unit].Trigger)
            deathTable[unit].Trigger = nil
            deathTable[unit].TriggerAction = nil
            deathTable[unit].Counter = nil
            deathTable[unit] = nil
        end
    end

    function GroupSelectionMimic.removeUnitFromPlayerData(playerData, unit)
        local unitCode = GetUnitTypeId(unit)
        for key, value in ipairs(playerData)
        do  
            if value == unit then
                --update focus when an unit was removed that has an lower index then the focused unit
                if key == playerData.Selected then
                    if IsUnitType(value, UNIT_TYPE_HERO) then
                        playerData.Selected = 1
                    --have more of that unit?
                    elseif GetUnitTypeId(playerData[playerData.Selected + 1]) == unitCode or GetUnitTypeId(playerData[playerData.Selected - 1]) == unitCode then
                        --do nothing
                    else
                        playerData.Selected = 1
                    end
                elseif key < playerData.Selected then playerData.Selected = playerData.Selected - 1 end
                --pull down the selected index, do not drop below 1
                table.remove(playerData, key)
                GroupSelectionMimic.destroyDeathDetect(value)
                --when the last unit was loaded in and there is an transporter the transporter gets focus without a selection
                if #playerData == 0 and GetTransportUnit() then table.insert(playerData, GetTransportUnit())
                elseif playerData.Selected > #playerData then playerData.Selected = math.max(#playerData,1) end
                break
            end
        end
    end
    function GroupSelectionMimic.removeUnit(unit)
        ForForce(bj_FORCE_ALL_PLAYERS, function()
            local player = GetEnumPlayer()
            local playerData = GroupSelectionMimic.SelectedUnits[player]
            GroupSelectionMimic.removeUnitFromPlayerData(playerData, unit)
        end)
        
    end

    function GroupSelectionMimic.addUnitToPlayer(player, unit, ignoreMultiAdd)
        local playerData = GroupSelectionMimic.SelectedUnits[player]
        local triggerUnitCode = GetUnitTypeId(unit)
        local triggerUnitPrio = BlzGetUnitRealField(unit, UNIT_RF_PRIORITY)

        if not ignoreMultiAdd then
            --start a 0s timer when it not runs already.
            --when another selection happens in that time, then the focus unit is reseted to the first unit.
            --This is done to correct multigroup selection with mouse clicking + drag or shift + double clicking
            if not playerData.Running then
                playerData.Running = true
                TimerStart(CreateTimer(), 0, false, function()
                    playerData.Running = false
                    DestroyTimer(GetExpiredTimer())
                end)
            else
                playerData.Selected = 1
            end
        end
        
            --contains this unit already?
        for key, value in ipairs(playerData)
        do  
            if value == unit then
                return
            end
        end
        
        local added = false
        --Add the unit where it should be prio wise.

        --print("Add",GetUnitName(unit), GroupSelectionMimic.getOrderValue(triggerUnitCode), triggerUnitPrio)
        for key, value in ipairs(playerData)
        do 

            --same prio and trigger units handle is smaller then take values place.
            if BlzGetUnitRealField(value, UNIT_RF_PRIORITY) == triggerUnitPrio and GroupSelectionMimic.getOrderValue(value) > GroupSelectionMimic.getOrderValue(unit) then
                --print("Replace Key",key)
                table.insert( playerData, key, unit)
                added = true
                GroupSelectionMimic.createDeathDetect(unit)

                --update the focus index when this unit was added into an lower index
                if key <= playerData.Selected then playerData.Selected = playerData.Selected + 1 end
                break            
            elseif BlzGetUnitRealField(value, UNIT_RF_PRIORITY) < triggerUnitPrio then
                table.insert( playerData, key, unit)
                added = true
                GroupSelectionMimic.createDeathDetect(unit)

                --update the focus index when this unit was added into an lower index
                if key <= playerData.Selected then playerData.Selected = playerData.Selected + 1 end
                break
            end
        end
        --not added yet?
        if not added then
            --add it at the end
            table.insert(playerData, unit)
            GroupSelectionMimic.createDeathDetect(unit)
        end
    end
end
 
Last edited:
After having read something from Spellbound I had a new Idea to detect the mainfocused Unit, this approach uses ORIGIN_FRAME_PORTRAIT_HP_TEXT and ORIGIN_FRAME_PORTRAIT_MANA_TEXT. it reads the displayed values of both frames and compares that with the values of all units currently selected. The one with the lowest diff should be the mainfocused unit.

But there is a problem with this approach when one has selected units some with mana and others without the game doesn't update the value gained from reading the hidden mana frame. But it is also not possible to check if the frame is visible (because that leads to a crash).
Means when one swaps from priest to footman the mana frame will still return 200 mana although the footman would have 0 mana. The demo code would then give the footman a bigger diff then the priest and return the priest although the footman is the main focused unit.
It also can fail to detect the correct unit when one has multiple units with exactly the same life and mana.

This approach is also async and using the gained unit for actions altering the game state will probably result into a desync.

That's a plug in and play Lua code version of the described technique. One copies it into a map and presses esc (when playing red) to display the name and hero name of the current selected main focus Unit.
Lua:
MainUnitFocusGroup = CreateGroup()
function GetMainFocusUnit()

    local function parseText(text)
        local seperatorIndex = string.find( text, "/",1, true )
        if seperatorIndex then
            return tonumber(string.sub( text, 1, seperatorIndex - 1)), tonumber(string.sub(text, seperatorIndex +1))
        else
            return tonumber(text)
        end
    end

    local unit
    GroupEnumUnitsSelected(MainUnitFocusGroup, GetLocalPlayer(), nil)
    -- only one Unit selected?
    if BlzGroupGetSize(MainUnitFocusGroup) <= 1 then
        unit = FirstOfGroup(MainUnitFocusGroup)
    else
        -- no, get the one with the closest diff to displayed data
        --read the displayed Text
        local displayedHp = BlzFrameGetText(BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT_HP_TEXT, 0))
        local displayedMp = BlzFrameGetText(BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT_MANA_TEXT, 0))
        -- parse the displayed Text
        local hpCurrent, hpMax = parseText(displayedHp)
        local mpCurrent, mpMax = parseText(displayedMp)
        -- find the unit selected with the least total diff
        local diffBest = 99999999
        local mostRelevantUnit
        for index = 0, BlzGroupGetSize(MainUnitFocusGroup) - 1 do
            unit = BlzGroupUnitAt(MainUnitFocusGroup, index)
   
            local diff1 = math.abs(GetWidgetLife(unit) - hpCurrent)
            local diff4 = math.abs(GetUnitState(unit, UNIT_STATE_MANA)- mpCurrent)
            local diff = diff1 + diff4
            -- check only for max hp, if such was parsed
            if hpMax then
                local diff2 = math.abs(BlzGetUnitMaxHP(unit) - hpMax)
                diff = diff + diff2
            end
            -- check only for max mana, if such was parsed
            if mpMax then
                local diff3 = math.abs(BlzGetUnitMaxMana(unit) - mpMax)
                diff = diff + diff3
            end

            if diff < diffBest then
                mostRelevantUnit = unit
                diffBest = diff
            end
        end

        --print("hpCurrent",hpCurrent)
        --print("hpMax",hpMax)
        --print("mpCurrent",mpCurrent)
        --print("mpMax",mpMax)
        unit = mostRelevantUnit
        mostRelevantUnit = nil
    end
    --clear the group to remove all references
    GroupClear(MainUnitFocusGroup)
 
    return unit
end

do
    local real = MarkGameStarted
  function MarkGameStarted()
        real()
    local trigger = CreateTrigger()
   
    TriggerRegisterPlayerEvent(trigger, Player(0), EVENT_PLAYER_END_CINEMATIC)
    TriggerAddAction(trigger, function()
        local unit = GetMainFocusUnit()
        print(GetUnitName(unit), GetHeroProperName(unit))
    end)
  end
end

Edited: Replaced selfexecution with a less problematic approach
 
Last edited:
Top