Can You Make Improvements Obsolete? Solved, thanks to Mr. WHoward!

sman1975

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

I'm working on a mod that adds a lot of content in the early game, including a few new improvements.

Unfortunately, in the second half of the game (Industrial Era and later), building many of these improvements don't make sense and harm the immersive qualities of the mod. I mean, who besides Americans in Las Vegas need to be able to construct a Ziggurat in the Information Era?

I've looked for an equivalent game event like "PlayerCanBuild" that you could use like you do when you prevent Buildings or Promotions. So, far, nothing looks promising.

Found the "Unit:CanBuild" method, but that looks more like a Boolean return that informs the code if a specific unit can perform a specific build action type on a certain plot. I'm trying to prevent the build icon from appearing in as a unit action in the first place. Agent Ransack tells me this method isn't used at all in the /Assets folders of the game.

Would appreciate it if anyone has a suggestion on preventing obsolete improvements from appearing in the Unit Action popup that Workers see when loitering on an unimproved tile.

Thanks!


P.s. I've seen the game event PlayerCanBuild - have written a function to try and return false for specific improvements, but it never seems to fire.

And yes, I know I could rewrite UnitPanel.LUA, but was hoping to find something a bit more on the nose. Thanks.
 
Last edited:
Without a modded DLL the short answer is no. The longer, player only, answer you already know (hint it involves editing lua)
 
Thanks, @whoward69 - you always know these things.

I really dislike replacing game UIs. Not simply because it limits compatibility with other mods that might replace the same UI, but mostly because when I look at those original UIs, it makes my head spin. Some of the most elegant and ugliest coding you'll ever see. Sometimes in the same file! :crazyeye: And UnitPanel.lua is no exception. It doesn't help that GameInfoActions, which that file uses liberally, is a stub in the Modiki...

Appreciate the answer, good Sir!
 
GameInfoActions is an internal table containing entries from many database tables, Builds, Missions, etc.

If the SubType for an entry in GameInfoActions is ActionSubTypes.ACTIONSUBTYPE_BUILD then you have an entry from the Builds database table.

Starting around line 250 you have

Code:
            elseif( (action.SubType == ActionSubTypes.ACTIONSUBTYPE_BUILD or action.Type == "INTERFACEMODE_ROUTE_TO") and hasPromotion == false) then

           ***BIG BLOCK OF CODE***

            elseif( action.OrderPriority > 100 ) then

You need

Code:
            elseif( (action.SubType == ActionSubTypes.ACTIONSUBTYPE_BUILD or action.Type == "INTERFACEMODE_ROUTE_TO") and hasPromotion == false) then

           if (not (action.SubType == ActionSubTypes.ACTIONSUBTYPE_BUILD and IsBuildInvalid(action.Type))) then
               ***BIG BLOCK OF CODE***
           end

            elseif( action.OrderPriority > 100 ) then

and then write the IsBuildInvalid(iActionType) function - note the inverted logic, return true if the build is INVALID now, otherwise false


(And bear in mind it's years since I've written Lua, so it may be rusty!)

HTH

W
 
Last edited:
I'm on it! Really appreciate the headstart you've given me. I'll post the end product here when it's working in case anyone has a similar issue in the future. You're the best, Mr. WHoward!
 
OK, code is working. It required an additional step below the code fragment in post #4. Basically, after the large "IF" statement that starts the action validation (line 231 in the BNW version of UnitPanel.lua), the function assumes the action is valid, and this large IF statement is to decide what kind of instance the UnitPanel will create. It expects a non-nil instance value.

So, I created a variable (bIsActionValid) that defaults to TRUE at the start of each action iteration in the outer FOR statement (line 215). I then added a test after the large IF statement ends.

I realize this is a bit confusing, so perhaps this code fragement will show better what my words fail to do:

Code:
-- loop over all the game actions
for iAction = 0, #GameInfoActions, 1
do

    . . .

    local bIsActionValid = true                                                                -- Used to test valid/invalid build actions, defaults TRUE, assuming all actions valid unless changed later in this IF statemen (sman:20210204)

    . . .

    -- test CanHandleAction w/ visible flag (ie COULD train if ... )
    if( bShowActionButton and action.Visible and Game.CanHandleAction( iAction, 0, 1 ) )
    then
 
        . . .
 
        elseif( (action.SubType == ActionSubTypes.ACTIONSUBTYPE_BUILD or action.Type == "INTERFACEMODE_ROUTE_TO") and hasPromotion == false) then
   
            if (not (action.SubType == ActionSubTypes.ACTIONSUBTYPE_BUILD and IsBuildInvalid(pActivePlayer, action))) then

                bIsActionValid = true

                ***BIG BLOCK OF CODE***

            else
                bIsActionValid = false

            end
         
        elseif( action.OrderPriority > 100 ) then


        . . .
    end

    if bIsActionValid then                                                            -- Used to bypass instances for invalid/expired improvements (sman:20210204)

        -- test w/o visible flag (ie can train right now)
        if not Game.CanHandleAction( iAction ) then
            bDisabled = true;
            instance.UnitActionButton:SetAlpha( 0.4 );          
            instance.UnitActionButton:SetDisabled( true );          
        else
            instance.UnitActionButton:SetAlpha( 1.0 );
            instance.UnitActionButton:SetDisabled( false );          
        end
       
        if(instance.UnitActionIcon ~= nil) then
            HookupActionIcon(action, actionIconSize, instance.UnitActionIcon);
        end
        instance.UnitActionButton:RegisterCallback( Mouse.eLClick, OnUnitActionClicked );
        instance.UnitActionButton:SetVoid1( iAction );
        instance.UnitActionButton:SetToolTipCallback( TipHandler )
    end

end


What's not showing is the details of the IsBuildInvalid function, but those are straightforward. I modified the schema of the Builds database to include a ObsoleteTech column. The function compares this tech to the active player's IsHasTech results, and returns T/F accordingly.

So, if in the distant google future, if someone wants to know how to "expire" an improvement, this approach works. It will conflict with any other mod that also replaces the UnitPanel UI, however. There's no avoiding that.

At any rate, you have Mr. WHoward to thank for finding the solution!

:)


EDIT: I wanted to add, each version of the game (Vanilla, G&K, BNW) have a different version of the UnitPanel.lua. The solution described above is written with the BNW version. I'm not sure how the older versions of the UI will fare.
 
Last edited:
In case anyone is interested in the completed IsBuildInvalid function, here it is.


The first change I made was to alter the Builds database schema, with this line in SQL:

Code:
ALTER TABLE Builds ADD COLUMN ObsoleteTech TEXT DEFAULT null;                                                    -- Add an Obsolete Tech element to the table - allows stopping early Era improvements from showing in the Unit Panel popup



Here is the (verbose) version of the function:

Code:
function IsBuildInvalid(pActivePlayer, tAction)
    local iBuildID = GameInfo.Builds[tAction.Type].ID
    local sObsoleteTech = GameInfo.Builds[iBuildID].ObsoleteTech or "None"
    
    if sObsoleteTech == "None" then
        print("Build " .. tAction.Type .. " does not have an obsolete tech.  Returning false (meaning not invalid)")
        return false
    else
        local iObsoleteTech = GameInfo.Technologies[sObsoleteTech].ID

        if (Teams[pActivePlayer:GetTeam()]:IsHasTech(iObsoleteTech)) then
            print("Player: " .. pActivePlayer:GetCivilizationShortDescription() .. " has discovered obsolete tech for " .. tAction.Type .. ": " .. sObsoleteTech .. ".  Returning true (meaning invalid)")
                return true

        else
            print("Player: " .. pActivePlayer:GetCivilizationShortDescription() .. " has not discovered obsolete tech for " .. tAction.Type .. ": " .. sObsoleteTech .. ".  Returning false (meaning not invalid)")
            return false
        end
    end
        
    print("Reached the end of function - should never happen.  Build: " .. tAction.Type .. "     Just in case, returning false (meaning not invalid)")
    return false   
end


Probably a few ways to optimize, but have been short of time. Reminds me of that quote by Mark Twain who was apologizing to a corresponded for his verbosity, "If I had had more time, I would have written a shorter letter..."

At any rate, it works...

Again, Mr. WHoward, you are a godsend...
 
For completeness I'll add that if you're using a modded DLL, you'll almost certainly have access to

Code:
// Events sent by plots (v30)
//   GameEvents.PlayerCanBuild.Add(function(iPlayer, iUnit, iX, iY, iBuild) return true end)
//   GameEvents.PlotCanImprove.Add(function(iX, iY, iImprovement) return true end)
//   GameEvents.PlayerBuilding.Add(function(iPlayer, iUnit, iX, iY, iBuild, bStarting) end) (v46)
//   GameEvents.PlayerBuilt.Add(function(iPlayer, iUnit, iX, iY, iBuild) end) (v46)
#define MOD_EVENTS_PLOT                             gCustomMods.isEVENTS_PLOT()
 
Top Bottom