Results 1 to 4 of 4

Thread: Rolling your own Action Queue

  1. #1
    Basic Member
    Join Date
    Dec 2016
    Posts
    731

    Rolling your own Action Queue

    I was toying around with this idea and so far implemented Action_MoveToLocation() and Action_UseAbilityOnLocation() as a test to see if it works, and it does. The question is, is there a need if Valve's implementation will give us everything we need? That's what I am currently debating.

    Here is the code:
    Code:
    -------------------------------------------------------------------------------
    --- AUTHOR: Nostrademous
    --- GITHUB REPO: https://github.com/Nostrademous/Dota2-FullOverwrite
    -------------------------------------------------------------------------------
    
    local DEFAULT_METHOD = true
    
    X = {}
    
    function X.InitHeroVar(pID)
        if X[pID] == nil then
            X[pID] = {}
            X[pID].actionQueue = {}
            X[pID].currentAction = {}
        end
    end
    
    function X.HasID(pID)
        return X[pID] ~= nil
    end
    
    function X.SetVar(pID, var, value)
        X[pID][var] = value
    end
    
    function X.GetVar(pID, var)
        return X[pID][var]
    end
    
    function X.SetGlobalVar(var, value)
        X[var] = value
    end
    
    function X.GetGlobalVar(var)
        return X[var]
    end
    
    function X.SetHeroActionQueue(pID, aq)
        X[pID].actionQueue = {unpack(aq)}
    end
    
    function X.GetHeroActionQueue(pID)
        return X[pID].actionQueue
    end
    
    function X.GetHeroActionQueueSize(pID)
        return #X[pID].actionQueue
    end
    
    function X.GetHeroPrevAction(pID)
        return X[pID].prevAction or {}
    end
    
    function X.SetHeroPrevAction(pID, action)
        X[pID].prevAction = {unpack(action)}
    end
    
    function X.GetHeroCurrentAction(pID)
        return X[pID].currentAction
    end
    
    function X.SetHeroCurrentAction(pID, action)
        X[pID].currentAction = {unpack(action)}
    end
    
    local function checkSleep(bot, ca)
        if #ca > 0 and ca[1] == "Sleep" then
            local pID = bot:GetPlayerID()
            if GameTime() < ca[2] then
                return true
            else
                X.SetHeroPrevAction(pID, ca)
                X[pID].currentAction = {}
            end
        end
        return false
    end
    
    function X.HeroMoveToLocation(bot, loc)
        local pID = bot:GetPlayerID()
        local ca = X.GetHeroCurrentAction(pID)
        
        if checkSleep(bot, ca) then return end
        
        if #ca == 0 or not (ca[1] == "MoveToLocation" and ca[2] == loc) then
            if DEFAULT_METHOD then
                bot:Action_MoveToLocation(loc)
            else
                --print(pID .. " set MoveToLocation: " .. loc[1] .. ", " .. loc[2])
                X[pID].prevAction = {unpack(X[pID].currentAction)}
                X[pID].currentAction = {}
                X[pID].actionQueue = {{[1]="MoveToLocation", [2]=loc}}
            end
        end
    end
    
    function X.HeroAttackUnit(bot, hTarget, bOnce)
        local pID = bot:GetPlayerID()
        local bOnce = bOnce or true
        local ca = X.GetHeroCurrentAction(pID)
    
        if checkSleep(bot, ca) then return end
        
        if #ca == 0 or not (ca[1] == "AttackUnit" and ca[2] == hTarget and ca[3] == bOnce) then
            if DEFAULT_METHOD then
                X[pID].currentAction = {[1]="Sleep", [2]=GameTime()+bot:GetAttackPoint()}
                bot:Action_AttackUnit(hTarget, bOnce)
            else
                print(pID .. " AttackUnit " .. hTarget:GetUnitName())
                X[pID].prevAction = {unpack(X[pID].currentAction)}
                X[pID].currentAction = {}
                X[pID].actionQueue = {{[1]="AttackUnit", [2]=hTarget, [3]=bOnce}}
            end
        end
    end
    
    function X.HeroUseAbilityOnLocation(bot, ability, loc, range)
        local pID = bot:GetPlayerID()
        local range = range or 0
        local ca = X.GetHeroCurrentAction(pID)
        
        if checkSleep(bot, ca) then return end
        
        if #ca == 0 or not (ca[1] == "UseAbilityOnLocation" and ca[2] == ability and ca[3] == loc) then
            if DEFAULT_METHOD then
                bot:Action_UseAbilityOnLocation(ability, loc)
            else
                --print(pID .. " set UseAbilityOnLocation: " .. loc[1] .. ", " .. loc[2])
                X[pID].prevAction = {unpack(X[pID].currentAction)}
                X[pID].currentAction = {}
                X[pID].actionQueue = {{[1]="UseAbilityOnLocation", [2]=ability, [3]=loc, [4]=range}}
            end
        end
    end
    
    function X.HeroPushUseAbilityOnLocation(bot, ability, loc, range)
        local pID = bot:GetPlayerID()
        local range = range or 0
        local ca = X.GetHeroCurrentAction(pID)
        
        if checkSleep(bot, ca) then return end
        
        if #ca == 0 or not (ca[1] == "UseAbilityOnLocation" and ca[2] == ability and ca[3] == loc) then
            if DEFAULT_METHOD then
                bot:ActionPush_UseAbilityOnLocation(ability, loc)
            else
                --print(pID .. " set PushUseAbilityOnLocation: " .. loc[1] .. ", " .. loc[2])
                X[pID].prevAction = {unpack(X[pID].currentAction)}
                X[pID].currentAction = {}
                table.insert(X[pID].actionQueue, 1, {[1]="UseAbilityOnLocation", [2]=ability, [3]=loc, [4]=range})
            end
        end
    end
    
    function X.ExecuteHeroActionQueue(bot)
        if DEFAULT_METHOD then return end
        
        local pID = bot:GetPlayerID()
        
        if not bot:IsAlive() then
            X[pID].currentAction = {}
            X[pID].actionQueue = {}
            return
        end
    
        local ca = X.GetHeroCurrentAction(pID)
        
        if #ca == 0 then
            if X.GetHeroActionQueueSize(pID) == 0 then return end
    
            ca = X.GetHeroActionQueue(pID)[1]
            X.SetHeroCurrentAction(pID, ca)
            table.remove(X[pID].actionQueue, 1)
    
            if ca[1] == "MoveToLocation" then
                bot:Action_MoveToLocation(ca[2])    
            elseif ca[1] == "UseAbilityOnLocation" then
                bot:Action_UseAbilityOnLocation(ca[2], ca[3])
            elseif ca[1] == "AttackUnit" then
                bot:Action_AttackUnit(ca[2], ca[3])
            end
        end
        
        if #ca > 1 then
            if ca[1] == "MoveToLocation" then
                if GetUnitToLocationDistance(bot, ca[2]) < 1.0 then
                    X.SetHeroPrevAction(pID, ca)
                    X[pID].currentAction = {}
                end
            elseif ca[1] == "UseAbilityOnLocation" then
                if ca[4] > 0 and GetUnitToLocationDistance(bot, ca[3]) < ca[4] then
                    X.SetHeroPrevAction(pID, ca)
                    X[pID].currentAction = {}
                end
            elseif ca[1] == "AttackUnit" then
                if not ca[2]:IsNull() and ca[2]:IsAlive() then
                    if ca[3] and GetUnitToUnitDistance(bot, ca[2]) < bot:GetAttackRange() then
                        X.SetHeroPrevAction(pID, ca)
                        X[pID].currentAction = {[1]="Sleep", [2]=GameTime()+bot:GetAttackPoint()}
                    end
                else
                    X.SetHeroPrevAction(pID, ca)
                    X[pID].currentAction = {}
                end
            end
        end
    end
    
    return X
    To use in any bot/file you just need to:
    a) require the file in each file/module where you want to use it "local gHeroVar = require( GetScriptDirectory().."/global_hero_data" )" for example
    b) initialize the hero data by calling gHeroVar.InitHeroVar(GetBot():GetPlayerID()) before you use it (just needs to be done once in 1 file relating to your hero)
    c) replace the Action_MoveToLocation() or Action_UseAbilityOnLocation() with the appropriate calls. Take note that my UseAbilityOnLocation() takes an extra parameter which is the cast range of the ability so it knows when to use it (which I believe Action_UseAbilityOnLocation() does under-the-hood for us).
    d) call gHeroVar.ExecuteHeroActionQueue(GetBot()) at some point after you make all your decisions to execute the code

    SO WHY USE THIS OVER VALVE'S IMPLEMENTATION?
    Possibly, no reason what-so-ever. It honestly depends on Valve's implementation. If they offer everything you need then there is NO reason. If you want to be able to cater the implementation to how you want it or add features to it then you can USE this. However, this implementation is in LUA, which is slower than the C++ backend implementation.

    So what features are missing? That's what I'm contemplating. One thing I can think of is that I might want "conditional" actions. As an example consider two heroes fighting an enemy they caught out of position. Both the good heroes have a stun but you don't want to overlap them. So you would want to be able to write something like "Action_ConditionalUseAbilityOnEntity(STUN, enemy, not enemy:IsStunned())" and put that into an Action Queue which would keep that action at the front of the queue and skip it while condition is false, and enact it as soon as condition is met. This currently does not (and as far as I know is not planned for) to exist in the Action_*() API ever. So the only way for you to prevent from overlapping is to be making a frame-by-frame decision and rebuilding the action queues. This is just one example I came up with on the fly, I'm sure there are others.
    Last edited by nostrademous; 02-12-2017 at 09:59 AM.

  2. #2
    Basic Member
    Join Date
    Dec 2016
    Posts
    731
    I have updated the code in the original post after testing some stuff. Added a test for using "PushUseAbilityOnEntity" to simulate ActionPush_*().

  3. #3
    Basic Member
    Join Date
    Dec 2016
    Posts
    731
    Update the CODE in the Original Post. We now have a DEFAULT_METHOD flag at top of file which controls which method is used. "true" means just use Valve's actionQueue and "false" is to use my actionQueue (which wraps around Valve's).

    I have also implemented Action_AttackUnit() and made it know very precisely when the attack animation is complete so you can do something else (this is the one rare case now where I actually still just slightly modify Valve's pure method to do animation canceling and prevent other AttackUnit, MoveToLocation or UseAbilityOnLocaiton actions from interrupting me.

  4. #4
    Basic Member
    Join Date
    Dec 2016
    Posts
    731
    I have to say... the improved last hitting code with animation canceling is kind of insane. My mid Lina was 15 LH, 14 Denies at 3:30 into the game against the Unfair default Zeus bot that was 3 LH, 2 Denies

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •