[BNW] How to force a function to wait for the results of a popup?

sman1975

Emperor
Joined
Aug 27, 2016
Messages
1,370
Location
Dallas, TX
Hi,

I'm working on a LUA function that wants to attempt to destroy a building in an enemy city. Part of that process is to popup a window that contains a menu listing all the non-wonder buildings in the city. The goal is to let the player select a building from that list, then once the selection is made, continue the main function to determine if the attempt was successful or not.

Unfortunately, I'm having an issue with the timing - the "main process" code is obviously procedural (top to bottom) in nature, but waiting for the GUI to return a building ID value is not. I've tried several ways to synchronize these two functions, but so far getting no where.

Since a picture is worth a 1000 words...

upload_2018-2-19_3-49-54.jpeg


The Main Process code works - the popup window is called and does display and work properly. However, the Main Process code immediately proceeds to the "print" statement and continues executing normally, even though no real action has occurred between the player and the "Select Building" popup.

I realize there needs to be some kind of feature/design approach - that will "force" the Main Process to wait on a proper value returned from the popup. Unfortunately, everything I've tried so far just hasn't worked.

Actual code for the "Select Building" popup:

Code:
-- ====================================================================================================================================
--        Select Building popup - returns a building ID or -1 (random building)

local g_SortTable                                                                            -- Global table used in sort function

function OnCOSBOK()                                                                            -- "Confirm" button pressed, building selection is final
    Controls.COSBPopup:SetHide(true)                                                        -- Close the popup
    if (not g_SelectedBuildingID) or (not g_SelectedBuilding) or (g_SelectedBuilding == "") then    -- If there's ANY problem with the selected building, return "random building"
        g_SelectedBuildingID = -1                                                            -- Random building "ID"       
    end
    print("Exiting menu.  Selected building ID: " .. g_SelectedBuildingID)                    -- Display the selected building ID
end
Controls.COSBOK:RegisterCallback(Mouse.eLClick, OnCOSBOK)                                    -- Register the callback

function SortByName(a, b)                                                                    -- QUAD sort function
    local sNameA = g_SortTable[tostring(a)].Name
    local sNameB = g_SortTable[tostring(b)].Name
    return sNameA < sNameB
end

function OnBuildingSelected(iBuildingID)                                                    -- "Building has been selected" callback - "selected" but not confirmed!
    local sBuildingName = "Random Building"                                                  -- Default selected building name is "Random"
    if not iBuildingID then iBuildingID = -1 end                                            -- If not a good ID, then force it to -1 (Random)
    if iBuildingID ~= -1 then                                                                -- If the building ID isn't random, get the actual building name
        sBuildingName = Locale.ConvertTextKey(GameInfo.Buildings[iBuildingID].Description)    -- Localize the string
    end     
    Controls.COSBMenu:GetButton():SetText(sBuildingName)                                    -- Set the "button" to the building name so it displays properly
    local sAtlas = "CIV_COLOR_ATLAS"                                                        -- This atlas/portrait combo is for the "Question Mark" icon
    local iPortraitIDX = 23
    if iBuildingID ~= -1 then                                                                -- If the building isn't random, get the actual icon for the building
        sAtlas = GameInfo.Buildings[iBuildingID].IconAtlas
        iPortraitIDX = GameInfo.Buildings[iBuildingID].PortraitIndex
    end
    IconHookup( iPortraitIDX, 128, sAtlas, Controls.SelectBuildingCenterIcon )                -- Display the building/question mark in the center of the popup           
    print("Selected building name: " .. sBuildingName .. "    ID: " .. iBuildingID .. "    Atlas: " .. sAtlas .. "    Portrait Index: " .. iPortraitIDX)
    g_SelectedBuildingID = iBuildingID                                                        -- Set the PROPOSED return value to the currently selected building   
end

function UpdateBuildingList(pPlayer, pTargetCity)                                            -- Construct a list of non-wonder buildings in pTargetCity
    Controls.COSBMenu:ClearEntries()                                                        -- Clear any previous values that might have been there
    g_SortTable = {}                                                                        -- Clear the SortTable
    local sCOSBTitle = Locale.ConvertTextKey("TXT_KEY_CO_COSB_MESSAGE", pTargetCity:GetName())    -- Update the "Message" on the popup (e.g. "Attack in Omaha")
    Controls.COSBMessage:LocalizeAndSetText(sCOSBTitle)                                        -- Set the title (inside top green bar) for the popup
    IconHookup(23, 128, "CIV_COLOR_ATLAS", Controls.SelectBuildingCenterIcon)                -- Initial center icon will be the question mark
    local tEntry = {}                                                                        -- Create an entry in the Pulldown Stack
    Controls.COSBMenu:BuildEntry("InstanceOne", tEntry)                                   
    g_SortTable[tostring(tEntry.Button)] = {Name="Random Building"}                            -- Add "Random Building" to SortTable
    tEntry.Button:SetVoid1(-1)                                                                -- Void1 on this entry
    tEntry.Button:SetText("Random Building")                                                -- Set the text value for the menu item
    for row in GameInfo.Buildings() do                                                        -- Scroll through all buildings in the Buildings table
        if pTargetCity:GetNumRealBuilding(row.ID) ~= 0  then                                -- If the city has more than zero of these building types, then...
            local BClass = GameInfo.Buildings[row.ID].BuildingClass                            -- Get the Building class of the building in the city
            local BWonder = GameInfo.BuildingClasses["" .. BClass .. ""].MaxGlobalInstances        -- Get MGI amount; normal buildings = 0; global wonders ~= 0
            local BNational = GameInfo.BuildingClasses["" .. BClass .. ""].MaxPlayerInstances    -- Get MPI amount; normal buildings = 0; national wonders ~= 0
            if (BWonder == -1) and (BNational == -1) then                                    -- If not a Global or National wonder then...
                local sBuildingName = Locale.ConvertTextKey(row.Description)                -- Get the name of this building
                local entry = {}                                                            -- Create an entry in the Pulldown Stacl
                Controls.COSBMenu:BuildEntry("InstanceOne", entry)                       
                g_SortTable[tostring(entry.Button)] = {Name=sBuildingName}                    -- Add this building name to the SortTable in the right location
                entry.Button:SetVoid1(row.ID)                                                -- Void1 on this entry
                entry.Button:SetText(sBuildingName)                                            -- Set the text value for the menu item
            end
        end
    end   
    Controls.COSBMenuStack:SortChildren(SortByName)                                            -- Sort items in the menu stack
    Controls.COSBMenu:GetButton():LocalizeAndSetText("TXT_KEY_CO_COSB_MENU_CHOOSE")            -- Prompt string for the pulldown menu
    Controls.COSBMenu:CalculateInternals()                                                    -- Popup bookkeeping
    Controls.COSBMenu:RegisterSelectionCallback(OnBuildingSelected)                            -- Register the OnBuildingSelected callback
end

function ProcessSelectBuildingPopup(pPlayer, pTargetCity)                                    -- Return a building ID or -1 for Random Building
    UpdateBuildingList(pPlayer, pTargetCity)                                                -- Get a list of all non-wonder buildings in pTargetCity
    Controls.COSBPopup:SetHide(false)                                                        -- Display the building list popup

    return g_SelectedBuildingID                                                                -- THIS IS THE PROBLEM!

end

Obviously, there's a design approach to handle these kinds of problems - I'm just somehow completely missing the boat on how it's done.

Thanks!
 
SetHide(false) returns immediately - end of story. If you want to do something after the user has dismissed the popup, you'll need to place the code in the show/hide listener for the popup. There is no "wait for popup to close". You could write something to check for a global changing value (set it to 0 before displaying the popup, and set it to 1 as the popup closes), but that's going to lock up the CPU/Game and depending on the threading model used by the game (which I've forgotten) may not work anyway. Much better to place the processing into the listener.
 
The Legend! Appreciate the suggestions.

You're absolutely correct about the changing global value check - have tried 5-6 variations of this approach and all of them completely locked up the game.

If I understand your suggestion, basically anything in the "Main Process" function would need to move inside of the "Select Building" popup code - between the SetHide(false) and SetHide(true) logic? That makes pretty good sense in most cases.

The difficulty here is that the code needed after the popup closes is dependent on the return value from the popup. So - there is a need to wait for a user response - wherever the waiting occurs.

The need to get a building ID is determined by things that are happening in a different place in the mod. It is merrily executing when it determines it needs to get a buildingID from the player. Until that ID is returned, the procedural code is at a stopping point. It can't go forward until the user has provided the data it needs.

These kinds of synchronizing issues are a tripping point for us old-timers with C in our DNA. Perhaps that's why I'm having such a difficult time seeing a way forward with what seems to me to be a pretty straightforward, commonly needed capability.

Thanks.

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

BTW - thanks for all you've done over the years in providing sooooo much tutoring on how to mod Civ V. You've quite literally saved me 100s, if not 1000s of hours of painful "discovery learning" - and the examples in your mods are about the best "starting place" any modder could need. Cannot thank you enough!!!!!

:worship:
 
You're (as you've implied) trying to use a procedural/functional coding pattern within an event driven UI model - and the two will never mesh. Everything in the UI is designed around "put something up and wait for the user to select to do something", where that "something" may or may not happen.

Your pseudo-code is along the lines of
Do A
Put up dialog
Wait for response
if user selected X then
Do B
else
Do C
endif
Exit

You'll need to change this to
Do A
Put up dialog
Exit

And within the dialog code
If user selects X do B
If user doesn't select X do C


And if you're trying to inject that into existing game core code, you're in for a world of pain!

W
 
World of Pain? Point taken. You're obviously quite correct. Transitioning from a Procedural to OO/Event-Driven paradigm is quite painful for us old dogs trying to learn a few new tricks. We simply do not see the problem in the same way others do, unfortunately. Appreciate your taking the time to walk me through this problem.

In theory, I understand the solution you described. The difficulty in the current problem set is there is a vast function of existing/tested/working procedural code representing 16 different "covert operations" in the mod - each processed in a very top-down manner.

Then the "Good Idea Fairy" pops up out of nowhere says, "Let's add a popup to only one of those 16 covert ops so the player can choose a specific building to target instead of a random one..."

Sounded fairly benign, and crafting a working popup in a test mod took barely an hour (THANKS TO YOUR TUTORIALS, BTW!!!). Then the dreaded code-merge began.... :wallbash:

Trying to conceive of a way ahead for this exceptional case that somehow removes "player input required in the middle of the process" is so far eluding me. Perhaps because this little popup is called from within the middle of a block of procedural code, and creates a dependency for the lower part of the procedural code.

I'm at a conceptual impasse of conceiving what "within the dialog code" is supposed to be happening before the player hits the CONFIRM button. Obviously, the entire design of the larger function needs reworking - to somehow isolate this exceptional case.

Or, I suppose, sometimes the better answer is to simply tell the GIF to sod off....

Anyways, thanks again for all the advice - it is truly appreciated!
 
Functions are your friend.

Change the existing code to stop using globals for the building Id and pass it as a parameter to a function that does the same work.

Then for the exceptional case that requires a user to select a building (id), after they've made their selection, just call the function with the chosen building Id.

I'd add "simples" ... but it probably isn't!
 
Saints be praised! I figured out a solution! :banana:

Looking at your suggestions, I changed the way the "Main Process" was called. Normally, after the mission was chosen in the "Mission Selection Menu" popup, I simply called the function, passing along the mission number (1..16) and that Main Process code executed it top-down fashion.

I changed this - now in the "Mission Selection Menu" popup - when player chooses a mission then hits the "Confirm" button, I test to see if the "Destroy Building" mission was selected - INSIDE the Confirm button callback. If the mission is Destroy Building, then launch the "Select Building" Popup. If not, then call the "Main Process" function normally.

Inside the "Select Building" popup - I added the "Main Process" function call to the "Confirm" button callback for that popup - it gets called when the player selects a building, then presses the Confirm button. Works like a charm.

I guess my "lesson learned" in this process: I used to look at the callbacks that close popups as simple, one-liners that simply hid the window then exited. Nothing to see here, move along....

Now - I'm starting to see them as actually the "top level" (logically speaking) of a lot of the procedural-like things I want to do after the window has been processed....

To me, it's quite a counter-intuitive way of looking at things, but I suppose to a lot of you younger types - you probably learned stuff like this on the first day of class...?

At any rate, thank you soooooo much for pointing me in the right direction on this. I really, really, really appreciate it!!!! :worship:
 
Top Bottom