• 🏆 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] GetMainSelectedUnit

A System using the new Framechild natives (added in 1.32.6) to know the current main selected Unit.

How this works?
This System finds the index of the first shown yellow groupunitbutton background, this number is taken. Then the current selected units are enumed and ordered in the way the game displays them, this 2 combined are the mainselected unit.

When one has the System code in your map use GetMainSelectedUnitEx() to get the mainselected unit.
Lua:
do
    -- returns the local current main selected unit, using it in a sync gamestate relevant manner breaks the game.
    function GetMainSelectedUnitEx()
        return GetMainSelectedUnit(GetSelectedUnitIndex())
    end
   

    local containerFrame
    local frames = {}
    local group
    local units = {}
    local filter = Filter(function()
        local unit = GetFilterUnit()
        local prio = BlzGetUnitRealField(unit, UNIT_RF_PRIORITY)
        local found = false
        -- compare the current unit with allready found, to place it in the right slot
        for index, value in ipairs(units) do
            -- higher prio than this take it's slot
            if BlzGetUnitRealField(value, UNIT_RF_PRIORITY) < prio then
                table.insert(units, index, unit)
                found = true
                break
            -- equal prio and better colisions Value
            elseif BlzGetUnitRealField(value, UNIT_RF_PRIORITY) == prio and GetUnitOrderValue(value) > GetUnitOrderValue(unit) then
                table.insert( units, index, unit)
                found = true
                break
            end
        end
        -- not found add it at the end
        if not found then
            table.insert(units, unit)
        end

        unit = nil
        return false
    end)

   
    function GetSelectedUnitIndex()
        -- local player is in group selection?
        if BlzFrameIsVisible(containerFrame) then
            -- find the first visible yellow Background Frame
            for int = 0, #frames do
                if BlzFrameIsVisible(frames[int]) then
                    return int
                end           
            end
        end

        return nil
    end

    function GetUnitOrderValue(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 GetMainSelectedUnit(index)
        if index then
            GroupEnumUnitsSelected(group, GetLocalPlayer(), filter)
            local unit = units[index + 1]
            --clear table
            repeat until not table.remove(units)
            return unit
        else
            GroupEnumUnitsSelected(group, GetLocalPlayer(), nil)
            return FirstOfGroup(group)
        end
    end

    --init
    do
        local real = MarkGameStarted
      function MarkGameStarted()
            real()
        local console = BlzGetFrameByName("ConsoleUI", 0)
        local bottomUI = BlzFrameGetChild(console, 1)
        local groupframe = BlzFrameGetChild(bottomUI, 5)
        --globals
        containerFrame = BlzFrameGetChild(groupframe, 0)
        group = CreateGroup()
        -- give this frames a handleId
        for int = 0, BlzFrameGetChildrenCount(containerFrame) - 1 do
            local buttonContainer = BlzFrameGetChild(containerFrame, int)
            frames[int] = BlzFrameGetChild(buttonContainer, 0)
        end
      end
    end
end

A example code: it prints the name of the current main Selected Unit once per second
Lua:
--demo
    do
        local real = MarkGameStarted
      function MarkGameStarted()
            real()
        TimerStart(CreateTimer(), 1, true, function()
            local u = GetMainSelectedUnit(GetSelectedUnitIndex())
            if IsUnitType(u, UNIT_TYPE_HERO) then
                print(GetUnitName(u), GetHeroProperName(u))
            else
                print(GetUnitName(u))
            end
        end)
      end
    end
end

Edited: Replaced selfexecution with a less problematic approach
 
Last edited:
It could look somehow like this, haven't done so much testing with it.
JASS:
library GetMainSelectedUnit initializer init_function

globals
    private framehandle containerFrame
    private framehandle array frames
    private group Group = CreateGroup()
    private unit array units
    private integer unitsCount = 0
    private filterfunc filter
endglobals

function GetUnitOrderValue takes unit u returns integer
    //heroes use the handleId
    if IsUnitType(u, UNIT_TYPE_HERO) then
        return GetHandleId(u)
    else
    //units use unitCode
        return GetUnitTypeId(u)
    endif
endfunction

private function FilterFunction takes nothing returns boolean
    local unit u = GetFilterUnit()
    local real prio = BlzGetUnitRealField(u, UNIT_RF_PRIORITY)
    local boolean found = false
    local integer loopA = 1
    local integer loopB = 0
    // compare the current u with allready found, to place it in the right slot
    loop
        exitwhen loopA > unitsCount
        if BlzGetUnitRealField(units[loopA], UNIT_RF_PRIORITY) < prio then
            set unitsCount = unitsCount + 1
            set loopB = unitsCount
            loop
                exitwhen loopB <= loopA
                set units[loopB] = units[loopB - 1]
                set loopB = loopB - 1
            endloop
            set units[loopA] = u
            set found = true
            exitwhen true
        // equal prio and better colisions Value
        elseif BlzGetUnitRealField(units[loopA], UNIT_RF_PRIORITY) == prio and GetUnitOrderValue(units[loopA]) > GetUnitOrderValue(u) then
            set unitsCount = unitsCount + 1
            set loopB = unitsCount
            loop
                exitwhen loopB <= loopA
                set units[loopB] = units[loopB - 1]
                set loopB = loopB - 1
            endloop
            set units[loopA] = u
            set found = true
            exitwhen true
        endif
        set loopA = loopA + 1
    endloop
   
    // not found add it at the end
    if not found then
        set unitsCount = unitsCount + 1
        set units[unitsCount] = u
    endif

    set u = null
    return false
endfunction

    function GetSelectedUnitIndex takes nothing returns integer
        local integer i = 0
        // local player is in group selection?
        if BlzFrameIsVisible(containerFrame) then
            // find the first visible yellow Background Frame
            loop
                exitwhen i > 11
                if BlzFrameIsVisible(frames[i]) then
                    return i
                endif
                set i = i + 1
            endloop
        endif
        return -1
    endfunction  

    function GetMainSelectedUnit takes integer index returns unit
        if index >= 0 then
            call GroupEnumUnitsSelected(Group, GetLocalPlayer(), filter)
            set bj_groupRandomCurrentPick = units[index + 1]
            //clear table
            loop
                exitwhen unitsCount <= 0
                set units[unitsCount] = null
                set unitsCount = unitsCount - 1
            endloop
            return bj_groupRandomCurrentPick
        else
            call GroupEnumUnitsSelected(Group, GetLocalPlayer(), null)
            return FirstOfGroup(Group)
        endif
    endfunction

    // returns the local current main selected unit, using it in a sync gamestate relevant manner breaks the game.
    function GetMainSelectedUnitEx takes nothing returns unit
        return GetMainSelectedUnit(GetSelectedUnitIndex())
    endfunction

    private function init_functionAt0s takes nothing returns nothing
        local integer i = 0
        local framehandle console = BlzGetFrameByName("ConsoleUI", 0)
        local framehandle bottomUI = BlzFrameGetChild(console, 1)
        local framehandle groupframe = BlzFrameGetChild(bottomUI, 5)
        local framehandle buttonContainer
        //globals
        set containerFrame = BlzFrameGetChild(groupframe, 0)
        set Group = CreateGroup()
        // give this frames a handleId
        loop 
            exitwhen i >= BlzFrameGetChildrenCount(containerFrame) - 1
            set buttonContainer = BlzFrameGetChild(containerFrame, i)
            set frames[i] = BlzFrameGetChild(buttonContainer, 0)
            set i = i + 1
        endloop
        call DestroyTimer(GetExpiredTimer())
    endfunction

    private function demoFunction takes nothing returns nothing
        local unit u = GetMainSelectedUnitEx()
        if IsUnitType(u, UNIT_TYPE_HERO) then
            call BJDebugMsg(GetUnitName(u) + " " + GetHeroProperName(u))
        else
            call BJDebugMsg(GetUnitName(u))
        endif
        set u = null
    endfunction

    private function init_function takes nothing returns nothing
        set filter = Filter(function FilterFunction)
        call TimerStart(CreateTimer(), 0, false, function init_functionAt0s)
        //demo
        call TimerStart(CreateTimer(), 1, true, function demoFunction)
    endfunction
endlibrary
 
Instead of checking the visibility of the yellow Background one could look for the UnitButtons Size. The main selected Units have a bigger size than the others (0.028 <-> 0.021875). This was told to me by someone on discord, some Time ago. But it should be mentioned in a public findable place. The benefit of checking for the Size is that it also works without having the default UI shown. If one want to do that one should change the selected FrameChild Index of the UnitGroupButton from 0 to 1.
JASS:
set frames[i] = BlzFrameGetChild(buttonContainer, 0)
->
set frames[i] = BlzFrameGetChild(buttonContainer, 1)
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Apart from some minor efficiency gains which could be done by caching some of the loop function calls to locals when you enum the selected units, the only reasonable change I can propose is adjusting this:

Lua:
        local real = MarkGameStarted
      function MarkGameStarted()

To use Global Initialization's "OnGameStart" method instead. MarkGameStarted seems to be part of some kind of external declaration that you use that probably is hooking InitBlizzard and starting a timer.
 
Level 20
Joined
Jul 10, 2009
Messages
477
@Bribe
Just for my own understanding, why do you prefer using OnGameStart instead of hooking MarkGameStarted?
MarkGameStarted is called directly from InitBlizzard via 0.01-timer without any hooks, so hooking into MarkGameStarted prevents unnecessary dependency to Global Init, which sounds reasonable to me (although I believe 99% of Lua maps are using Global Init anyway, but still...).

Lua:
function MarkGameStarted()
    bj_gameStarted = true
    DestroyTimer(bj_gameStartedTimer)
end

-- ===========================================================================

function DetectGameStarted()
    bj_gameStartedTimer = CreateTimer()
    TimerStart(bj_gameStartedTimer, bj_GAME_STARTED_THRESHOLD, false, MarkGameStarted)
end

-- ===========================================================================

function InitBlizzard()
    ConfigureNeutralVictim()
    InitBlizzardGlobals()
    InitQueuedTriggers()
    InitRescuableBehaviorBJ()
    InitDNCSounds()
    InitMapRects()
    InitSummonableCaps()
    InitNeutralBuildings()
    DetectGameStarted() -- Called Here
end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
@Bribe
Just for my own understanding, why do you prefer using OnGameStart instead of hooking MarkGameStarted?
MarkGameStarted is called directly from InitBlizzard via 0.01-timer without any hooks, so hooking into MarkGameStarted prevents unnecessary dependency to Global Init, which sounds reasonable to me (although I believe 99% of Lua maps are using Global Init anyway, but still...).

Lua:
function MarkGameStarted()
    bj_gameStarted = true
    DestroyTimer(bj_gameStartedTimer)
end

-- ===========================================================================

function DetectGameStarted()
    bj_gameStartedTimer = CreateTimer()
    TimerStart(bj_gameStartedTimer, bj_GAME_STARTED_THRESHOLD, false, MarkGameStarted)
end

-- ===========================================================================

function InitBlizzard()
    ConfigureNeutralVictim()
    InitBlizzardGlobals()
    InitQueuedTriggers()
    InitRescuableBehaviorBJ()
    InitDNCSounds()
    InitMapRects()
    InitSummonableCaps()
    InitNeutralBuildings()
    DetectGameStarted() -- Called Here
end
Honestly, I didn't know that was a thing. It was proposed back in 2019 to use a timer, but since that already gets the job done then it's worthwhile to switch. Thanks to Tasyen for finding that and for you for identifying it for me!
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've consolidated this code into a much simpler script that also only occupies one slot in the _G table.

Lua:
function GetMainSelectedUnit(...)
    --initialize on the first call...        group frame:      bottom UI:               console:
    local containerFrame = BlzFrameGetChild(BlzFrameGetChild(BlzFrameGetChild(BlzGetFrameByName("ConsoleUI", 0), 1), 5), 0)
    
    local function getUnitSortValue(unit)
                --heroes use handleId                                    units use type ID
        return IsUnitType(unit, UNIT_TYPE_HERO) and GetHandleId(unit) or GetUnitTypeId(unit)
    end
    local units
    local function getUnitAt(index)
        return units[index + 1]
    end
    local filter        = Filter(function()
        local unit      = GetFilterUnit()
        local prio      = BlzGetUnitRealField(unit, UNIT_RF_PRIORITY)
        local pos       = #units + 1
        -- compare the current unit with already found, to place it in the right slot
        for i = 1, pos - 1 do
            local value = units[i]
            -- higher prio than this; take it's slot                    equal prio and better colisions Value
            if BlzGetUnitRealField(value, UNIT_RF_PRIORITY) < prio or (BlzGetUnitRealField(value, UNIT_RF_PRIORITY) == prio and getUnitSortValue(value) > getUnitSortValue(unit)) then
                pos = i
                break
            end
        end
        table.insert(units, pos, unit)
    end)
    -- give each frame a unique ID
    local frames = {}
    for int = 0, BlzFrameGetChildrenCount(containerFrame) - 1 do
        local buttonContainer = BlzFrameGetChild(containerFrame, int)
        frames[int + 1] = BlzFrameGetChild(buttonContainer, 0)
    end
    ---@param atIndex? integer
    ---@param async? boolean --if no atIndex is specified but this is true, returns the local current main selected unit's index. Beware: using it in a sync gamestate relevant manner breaks the game.
    function GetMainSelectedUnit(atIndex, async) --re-declare itself once it was called the first time.
        if async and not atIndex then
            -- local player is in group selection?
            if BlzFrameIsVisible(containerFrame) then
                -- find the first visible yellow Background Frame
                for i,frame in ipairs(frames) do
                    if BlzFrameIsVisible(frame) then
                        atIndex = i - 1
                        break
                    end
                end
            end
        end
        local whichFilter
        local getUnit   = FirstOfGroup
        if atIndex then
            units       = {}
            whichFilter = filter
            getUnit     = getUnitAt
        end
        GroupEnumUnitsSelected(bj_lastCreatedGroup, GetLocalPlayer(), whichFilter)
        return getUnit(atIndex or bj_lastCreatedGroup)
    end
    return GetMainSelectedUnit(...) --return the product of the newly-declared function.
end
 
Last edited:
Level 8
Joined
Aug 5, 2014
Messages
192
Im really sorry for asking that question, but is there a possibility to get something similar for the old warcraft version without this new natives?
 
Level 17
Joined
Apr 13, 2008
Messages
1,597
Hello sir Tasyen,

I implemented your save / load bug workaround into this JASS code, so people using your system will no longer crash upon loading a saved game and using the function.

Just added 3 lines, a new trigger that loads the 0s function when triggered by a game loaded event.

JASS:
library GetMainSelectedUnit initializer init_function // By Tasyen

    globals
        private trigger LoadBugTrigger = CreateTrigger()
        private framehandle containerFrame
        private framehandle array frames
        private group Group = CreateGroup()
        private unit array units
        private integer unitsCount = 0
        private filterfunc filter
    endglobals
    
    function GetUnitOrderValue takes unit u returns integer
        //heroes use the handleId
        if IsUnitType(u, UNIT_TYPE_HERO) then
            return GetHandleId(u)
        else
        //units use unitCode
            return GetUnitTypeId(u)
        endif
    endfunction
    
    private function FilterFunction takes nothing returns boolean
        local unit u = GetFilterUnit()
        local real prio = BlzGetUnitRealField(u, UNIT_RF_PRIORITY)
        local boolean found = false
        local integer loopA = 1
        local integer loopB = 0
        // compare the current u with allready found, to place it in the right slot
        loop
            exitwhen loopA > unitsCount
            if BlzGetUnitRealField(units[loopA], UNIT_RF_PRIORITY) < prio then
                set unitsCount = unitsCount + 1
                set loopB = unitsCount
                loop
                    exitwhen loopB <= loopA
                    set units[loopB] = units[loopB - 1]
                    set loopB = loopB - 1
                endloop
                set units[loopA] = u
                set found = true
                exitwhen true
            // equal prio and better colisions Value
            elseif BlzGetUnitRealField(units[loopA], UNIT_RF_PRIORITY) == prio and GetUnitOrderValue(units[loopA]) > GetUnitOrderValue(u) then
                set unitsCount = unitsCount + 1
                set loopB = unitsCount
                loop
                    exitwhen loopB <= loopA
                    set units[loopB] = units[loopB - 1]
                    set loopB = loopB - 1
                endloop
                set units[loopA] = u
                set found = true
                exitwhen true
            endif
            set loopA = loopA + 1
        endloop
       
        // not found add it at the end
        if not found then
            set unitsCount = unitsCount + 1
            set units[unitsCount] = u
        endif
    
        set u = null
        return false
    endfunction
    
        function GetSelectedUnitIndex takes nothing returns integer
            local integer i = 0
            // local player is in group selection?
            if BlzFrameIsVisible(containerFrame) then
                // find the first visible yellow Background Frame
                loop
                    exitwhen i > 11
                    if BlzFrameIsVisible(frames[i]) then
                        return i
                    endif
                    set i = i + 1
                endloop
            endif
            return -1
        endfunction  
    
        function GetMainSelectedUnit takes integer index returns unit
            if index >= 0 then
                call GroupEnumUnitsSelected(Group, GetLocalPlayer(), filter)
                set bj_groupRandomCurrentPick = units[index + 1]
                //clear table
                loop
                    exitwhen unitsCount <= 0
                    set units[unitsCount] = null
                    set unitsCount = unitsCount - 1
                endloop
                return bj_groupRandomCurrentPick
            else
                call GroupEnumUnitsSelected(Group, GetLocalPlayer(), null)
                return FirstOfGroup(Group)
            endif
        endfunction
    
        // returns the local current main selected unit, using it in a sync gamestate relevant manner breaks the game.
        function GetMainSelectedUnitEx takes nothing returns unit
            return GetMainSelectedUnit(GetSelectedUnitIndex())
        endfunction
    
    
        private function init_functionAt0s takes nothing returns nothing
            local integer i = 0
            local framehandle console = BlzGetFrameByName("ConsoleUI", 0)
            local framehandle bottomUI = BlzFrameGetChild(console, 1)
            local framehandle groupframe = BlzFrameGetChild(bottomUI, 5)
            local framehandle buttonContainer
            //globals
            set containerFrame = BlzFrameGetChild(groupframe, 0)
            set Group = CreateGroup()
            // give this frames a handleId
            loop 
                exitwhen i >= BlzFrameGetChildrenCount(containerFrame) - 1
                set buttonContainer = BlzFrameGetChild(containerFrame, i)
                set frames[i] = BlzFrameGetChild(buttonContainer, 0)
                set i = i + 1
            endloop
            call DestroyTimer(GetExpiredTimer())
        endfunction
    
    
        private function init_function takes nothing returns nothing
            set filter = Filter(function FilterFunction)
            call TimerStart(CreateTimer(), 0, false, function init_functionAt0s)
            call TriggerRegisterGameEvent(LoadBugTrigger, EVENT_GAME_LOADED)
            call TriggerAddAction(LoadBugTrigger, function init_functionAt0s)
        endfunction
endlibrary
 
Top