[BNW] LUA - Popup Management Problem

sman1975

Emperor
Joined
Aug 27, 2016
Messages
1,376
Location
Dallas, TX
Hello,

I'm working on a function that cycles through all a player's units. For certain type of units, e.g. Mech Infantry, the function should pop up a window asking for an "Action A" or an "Action B" button to be pressed. Based on which button is pressed, a different function would be called.

So far, the popup looks good, so I think it's put together properly.

The issue is, when I cycle through my units, using:
Code:
for pUnit in pPlayer:Units() do
  if pUnit:GetUnitType() == GameInfoTypes.UNIT_MECHANIZED_INFANTRY then
       ContextPtr:SetHide(false)
   end
end

The button callbacks look like this:
Code:
function OnRehire()
  -- Processing Rehire request code
  ContextPtr:SetHide(true)
end
Controls.Rehire:RegisterCallback(Mouse.eLClick, OnRehire)

function OnDisband()
  -- Processing Disband request code
  ContextPtr:SetHide(true)
end
Controls.Disband:RegisterCallback(Mouse.eLClick, OnDisband)

The for-loop dutifully cycles through all my units, and when it encounters a Mech Infantry unit, it shows the popup for the first unit. But then the for loop continues through all the units and ends - before I've pressed any button on the first popup.

So, I then press either ButtonA or ButtonB, and the correct function is called for that first unit. Once this ends, game play continues as normal, and no additional popups are called, even though there are several Mech Infantry units in play.

So, I guess my question is - how do I force the for loop to wait for the button press before iterating to the next unit?
 
Last edited:
What you could do is store all the IDs in a table during the iteration, and then "iterate with wait" over that table:
Code:
local iMechIndex = 0;

function PopupsForAll(pPlayer)
  local tMechs = {};
  for pUnit in pPlayer:Units() do
    if pUnit:GetUnitType() == GameInfoTypes.UNIT_MECHANIZED_INFANTRY then
        tMechs[#tMechs + 1] = pUnit:GetID()
    end
  end
  if #tMechs>0 then
      iMechIndex = 0;
      ContextPtr:SetHide(false);
  end
end

function OnRehire()
  -- Processing Rehire request code (base this around iMechIndex)
  ContextPtr:SetHide(true)
    cycleMechs()
end
Controls.Rehire:RegisterCallback(Mouse.eLClick, OnRehire)

function OnDisband()
  -- Processing Disband request code (base this around iMechIndex)
  ContextPtr:SetHide(true)
   cycleMechs()
end
Controls.Disband:RegisterCallback(Mouse.eLClick, OnDisband)

function cycleMechs()
  if iMechIndex >= #tMechs then return end --we are at the last unit (or beyond!), so we don't need to cycle
  iMechIndex = iMechIndex + 1
  ContextPtr:SetHide(false);
  --you'll probably also want the update/refresh/Init-Popup()-function you probably have somewhere in your code here
end
You can retrieve the actual mech unit by using the player pointer: pPlayer:GetUnitByID(tMechs[iMechIndex])
So basically what happens is that the first mech popup is loaded, and when we press one of the buttons, the next mech (if any) will be loaded.


(You could also use UI.LookAt(pPlot,bZoomIn) for a cool effect when doing the 'iteration with wait' ;))
 
Last edited:
Thanks, @Troller0001 - I'm playing with the code, but believe there's a scope problem with the tMechs table in the cycleMechs function.

I'm playing around with making the table global (like the index) and "clearing" it each time a new player is evaluated.

Also - I've never heard of the PopupInit() function - couldn't find it in the modiki. Do you have a link where I could find out more about it?
 
It's working! Troller, you're a genius! :) Great tip on the UI.LookAt statement. Really dresses up the function!

The only adjustments I made were to globalize the tMech table, then clear it in the PopupsForAll function, where the original tMech table was declared. I used this statement:
Code:
--local tMechs = {}
for i, v in ipairs(tMechs) do tMechs[i] = nil end

I also changed the statment in the same function to start the index at 1 vice 0, as I was getting a duplicate popup for the first unit:
Code:
 if #tMechs>0 then
      -- iMechIndex = 0;
       iMechIndex = 1;
      ContextPtr:SetHide(false);
  end

Still experimenting with the function, but overall it looks like something I can work with.

Man, I really appreciate the help! Would have taken me a year of work to not figure out how to do this... :crazyeye:
 
Last edited:
Here is the current version of the experimental code. Still a work in progress, but at least the controlling logic seems to work correctly.

I know, I know, my LUA sucks... If anyone has any suggestions on how to make my code more "elegant" or at least not look like a 4 year old did it, I'm much obliged for any suggestions!

Code:
-- ============================================================================================
--   Includes
include("IconSupport")
include("MercFunctionGlobals.lua")

-- ============================================================================================
--   Globals
local g_PlayerID = 0
local g_UnitID = -1

local iMercIndex = 0
local tMercs = {}

-- ============================================================================================
--   Callbacks
function OnRehire()
  print("Processing rehire button for unitID: " .. g_UnitID)
  -- Processing Rehire request code
  ContextPtr:SetHide(true)
  cycleMercs()
end
Controls.Rehire:RegisterCallback(Mouse.eLClick, OnRehire)

function OnDisband()
  print("Processing disband button for unitID: " .. g_UnitID)
  -- Processing Disband request code
  ContextPtr:SetHide(true)
  cycleMercs()
end
Controls.Disband:RegisterCallback(Mouse.eLClick, OnDisband)

Events.GameplaySetActivePlayer.Add(OnRehire)
Events.GameplaySetActivePlayer.Add(OnDisband)

IconHookup(2, 80, "EXPANSION_TECH_ATLAS_1", Controls.DialogTopIcon)
ContextPtr:SetHide(true)

-- ============================================================================================
--   Support Functions
function RefreshMercFunPopup()
    local pPlayer = Players[g_PlayerID]
    local pUnit = pPlayer:GetUnitByID(tMercs[iMercIndex])
    local goldOnHand = pPlayer:GetGold()        
    local goldNeeded = pPlayer:GetCapitalCity():GetUnitPurchaseCost(pUnit:GetUnitType())                    
    local unitName = pUnit:GetName()        
    Controls.Message2:LocalizeAndSetText("TXT_KEY_MERC_FUN_MESSAGE_2", unitName)
    Controls.Message3:LocalizeAndSetText("TXT_KEY_MERC_FUN_MESSAGE_3", goldNeeded)
    Controls.Message4:LocalizeAndSetText("TXT_KEY_MERC_FUN_MESSAGE_4", goldOnHand)

    UI.LookAt(pUnit:GetPlot(), 0)
end

function BuildMercTable(pPlayer)
    --local tMechs = {}
    for i, v in ipairs(tMercs) do tMercs[i] = nil end                                -- Start each process with an empty tMercs table

    for pUnit in pPlayer:Units() do
        --   Conditional statement inactive for testing - want to examine all units initially!!!!
        --if pUnit:GetUnitType() == GameInfoTypes.UNIT_MECHANIZED_INFANTRY then
            tMercs[#tMercs + 1] = pUnit:GetID()
        --end
    end
    if #tMercs>0 then                    
        iMercIndex = 1                                                                    -- Reset merc table index
        RefreshMercFunPopup(pPlayer)
        ContextPtr:SetHide(false);
    end
end

function cycleMercs()
  if iMercIndex >= #tMercs then return end                    --we are at the last unit (or beyond!), so we don't need to cycle
  iMercIndex = iMercIndex + 1
  RefreshMercFunPopup(pPlayer)
  ContextPtr:SetHide(false);
end

-- ============================================================================================
--   Main Function
function MercenaryProcessing (playerID)
    local pPlayer = Players[playerID]
    local gameTurn = Game.GetGameTurn()    
 
    if pPlayer:IsHuman() then                                                                -- Process for mercs is different for human player than AI
        print("Player is human.  Game Turn: " .. gameTurn)
        BuildMercTable(pPlayer)        
    else                                                                                    -- AI merc processing
        -- AI code TBD
    end
end
GameEvents.PlayerDoTurn.Add( MercenaryProcessing )
 
Last edited:
Also - I've never heard of the PopupInit() function - couldn't find it in the modiki.
What I meant was your (self-written) 'RefreshMercFunPopup()'-function. So an 'initialization/refresh function' that sets all the button-texts, icons, etc. that rely on which unit was selected.
I realize that I might not have explained that the clearest way, but you found your way around it! (PopupInit() indeed sounds pretty general :crazyeye:)

I also changed the statment in the same function to start the index at 1 vice 0, as I was getting a duplicate popup for the first unit:
That's what I get for writing javascript just a few minutes before posting :lol:


(And your code looks well structured; only thing you could add are comments)
 
I understand the PopupInit() advice now. This is the first real popup code I've written in LUA, although I've repurposed some windows in the past, but ones that didn't require much change and weren't demanding in the first place. Am learning a completely different way of thinking when it comes to getting these objects to behave the way I'd like. It ain't easy...

Yeah, I vaguely rememberhow hard switching languages was. But after all these years, I'm having problems with just one...

Thanks for the suggestions on the code. I don't tend to document code until it's done, for some reason. There's probably a very good argument to do just the opposite, but as I make so many mistakes and have so many restarts, it probably doesn't make sense for me to change at this late date. I'd be doing little else besides writing and deleting comment statements! :mischief:

And thanks again for all the help getting this working. It isn't as intuitive for us "procedural" types to figure out this kind of paradigm.
 
@Troller0001 - One thing I've been putzing with that is starting to annoy...
Code:
UI.LookAt(pUnit:GetPlot(), 0)

This moves the camera as expected, but at the start of the turn after it executes, the game will then move the LookAt to the "active" unit, even though the popup is visible and the screen is darkened. The active unit being the one shown with the yellow circle around it on the map, and its info in the lower left part of the screen. The one you normally start your turn moving/attacking, etc. After this first unit, if there are more than one unit in the tMercs table, the LookAt works fine. It's just at turn start where it's kinda hinky.

Do you know how to make a given unit the "active" one? I've tried:
Code:
Events.SerialEventUnitFlagSelected(Game:GetActivePlayer(), pUnit:GetID())

and:
Code:
UI.LookAt(pUnit:GetPlot(), 0)
UI.SelectUnit(pUnit);

But they didn't work. Have looked through the Events, UI, Unit, and Player methods but can't find anything leaping out at me.
 
Maybe the game is 'overriding' your new selection in ActionInfoPanel.lua. I'm not sure when the event here fires though, or if that even is the issue...
Spoiler :

Code:
function OnEndTurnBlockingChanged(ePrevEndTurnBlockingType, eNewEndTurnBlockingType)
 
    local pPlayer = Players[Game.GetActivePlayer()];
    if (pPlayer ~= nil) then
        if pPlayer:IsTurnActive() then
     
            -- If they have auto-unit-cycling off, then don't change the selection.
            if (not OptionsManager.GetAutoUnitCycle()) then
                return;
            end
         
            local pSelectedUnit = UI.GetHeadSelectedUnit();
            if (pSelectedUnit ~= nil) then
                if (not pSelectedUnit:IsAutomated() and not pSelectedUnit:IsDelayedDeath() and pSelectedUnit:IsReadyToMove()) then
                    return;
                end
            end
         
            -- GetFirstReadyUnit can return a unit that has automation and we don't want to select and center on that, manually look for a unit     
            for pUnit in pPlayer:Units() do
                if (pUnit ~= nil) then
                    if (not pUnit:IsAutomated() and not pUnit:IsDelayedDeath() and pUnit:IsReadyToMove()) then
                        local pPlot = pUnit:GetPlot();
                        UI.LookAt(pPlot, 0);
                        UI.SelectUnit(pUnit);
                        return;
                    end
                end
            end
        end
    end
end
Events.EndTurnBlockingChanged.Add( OnEndTurnBlockingChanged );


That of course doesn't solve your issue, but if you're lucky then the 'start of turn unit cycle' fires this event, meaning that you could subscribe to it and do a UI.LookAt(..) inside that subscribed function:
Code:
Events.UnitSelectionChanged(iPlayer, iUnit,    hexposition.x,    hexposition.y,    integer of some sort (usually 0), boolean of some sort,    bIsSelected);
It fires whenever the active human player selects a new unit (I.e. clicks their flag).
I'm not sure what the last five parameters are, and Firaxis's Lua files didn't really help there (they usually end up not being used either).
The 3rd and 4th parameters have something to do with world anchors from the looks of it?
It can be invoked like any GameEvents-hook such as GameEvents.PlayerDoTurn

Maybe you can also try subscribing to the EndTurnBlockingChanged-event to see what that exactly does.
Or maybe try using Events.ActivePlayerTurnStart() (no parameters here as you can retrieve the active player with Game.GetActivePlayer())

It's all a guessing game for me too, as I've never really messed with this kind of UI stuff (apart from the basic UI.LookAt(..)s)
 
@Troller0001 - I think you're 100% correct about the 'overriding.' If I look closely enough, the camera does a little jump towards the unit I want it to go to first, then 1/4 second later, it changes course to the last unit I moved the previous turn. So, during my GameEvents.PlayerDoTurn it starts to do the LookAt, only to be hijacked by the normal game processes.

I've experimented with different methods using "Events.ActivePlayerTurnEnd " - including:
- SerialEventUnitFlagSelected
- UnitSelectionChanged
- UI.SelectUnit

At the end of turn, I can see the unit I want to be the first activated next turn - it's actually selected with a spinning yellow circle around it. Looks good, but at the start of the next turn, the first unit "activated" is the last unit I moved previous turn, not the unit I wanted.

At this point, it's a pretty minor nit, and I'm about ready to call it "good enough" until I can get some spare time to delve into this. The problem isn't probably worth the x (where x= too many) hours I've invested in this today.

Appreciate the advice - I'll try to experiment with some more of these later.

You need to set up a Paypal account, so I can send a little Xmas cheer your way for all the "tutoring!" :D
 
Game.GetActivePlayer() always returns the Human Player's ID # in a single-player game, and whichever Human Player is currently processing in a hotseat MP game.

Not sure about a regular MP game.

It does not return the ID # of the player currently taking their turn (ie, never returns the ID# for an AI player). I thought it did so for the longest time, but no, this is not correct.

----------------------------------------

Events.ActivePlayerTurnEnd() fires only for the human player a la the way Game.GetActivePlayer() works. Same I believe for Events.ActivePlayerTurnStart()
 
Game.GetActivePlayer() always returns the Human Player's ID # in a single-player game, and whichever Human Player is currently processing in a hotseat MP game.

Not sure about a regular MP game.

It does not return the ID # of the player currently taking their turn (ie, never returns the ID# for an AI player). I thought it did so for the longest time, but no, this is not correct.

----------------------------------------

Events.ActivePlayerTurnEnd() fires only for the human player a la the way Game.GetActivePlayer() works. Same I believe for Events.ActivePlayerTurnStart()
That is 100% correct, though UI mods like this are usually not compatible in Multiplayer anyway (how are you going to "send" the choice of the active player to the other players to prevent desyncs? I know Vice released a system that somehow handles this, but in cases like this (where there can be over thousands of combinations of PlayerID's and UnitID's) will not be able to handle that well. This is why, afaik, whoward's 'choose your Unit Upgrade' doesn't work in multiplayer.
I believe sman's mod gives the player the choice to rehire (I.e. pay gold?) or disband (so actually disband with pUnit:Kill()) a unit based upon the player's choice, which is pretty similar to whoward's upgrade mod)

That all being said, maybe there's a secret tool that I don't know of

You need to set up a Paypal account, so I can send a little Xmas cheer your way for all the "tutoring!" :D
The release of cool mods is what makes me the most happy! A (perhaps early) Merry Christmas to you (and of course to everyone else reading this!)! :snowgrin:
 
Well, I'm hoping it will be cool. It's adding some "exotic units" - ones with great artwork to add some color and give some alternative unit choices throughout the tech tree.

Currently, there are 8 (soon to be 12) units buildable by all civs, plus 31 "mercenary" units that can be hired for a specified period if you have enough gold. The advantage of the merc is they cost a fraction of a normal unit's gold purchase price (although they have a shelf life). They also require no gold maintenance per turn, although they do count against supply. Finally, they don't require any strategic resources to hire. Only gold. So if you have no Iron, you can at least hire some competitive units in a pinch.

Here is the current stable of new units:

https://forums.civfanatics.com/thre...laboration-thread.625074/page-7#post-14942022

Thanks again for helping me get this working!
 
Back
Top Bottom