Districts and Roads in LUA

bcbarrett

Chieftain
Joined
Jun 19, 2019
Messages
14
Hi all,

I'm trying to make a Railyard district, and as part of this I want to convert all of a city's routes to railroads once construction of the Railyard is complete.

My LUA script is using stuff I've cobbled together from a few different places, it's included at the end.

I'm registering for the following event: "OnDistrictProgressChanged", then checking to see if the percent complete is 100. I'm uncertain whether the arguments I have for "OnDistrictFinished()" are accurate or not. I'm also uncertain exactly what the "districtType" argument is, whether percentComplete is from 0-100 or 0-1, etc. I think what I'm trying to do is going to work, I just need to get all the syntax and data types correct...

Essentially, this script should be simple. At 100%, I want to get all the tiles which are roads in that city and upgrade them to railroads. If pillaged, revert the roads to the player's current level. Also if the city adds a tile, it should check to see whether it's a road and make it a railroad, provided the district isn't pillaged. (These last two are probably easy if i can get the first working)

Thanks!

Edit: Also, that print statement at the beginning of "OnDistrictFinished()" isn't outputting to LUA.log when a district completes...I have the script included as an In-Game Action of type "AddGameplayScripts".

Script:
Code:
local iRailyard = GameInfo.Districts["DISTRICT_RAILYARD"].Index
local AllRoutes = GameInfo.Routes()
local Railroad = AllRoutes[5]

function OnDistrictFinished(playerID, districtID, cityID, X, Y, districtType, era, civilization, percentComplete, Appeal, isPillaged)
   print("District Type: "..tostring(districtType))
   if isRailyard(districtType) then
       if (percentComplete == 100) then
           local city = CityManager.GetCity(playerID, cityID)
           BuildRailroadsInCity(city)
       end
   end
end

function UpdateRailroads(owner, cityID)
   local city = CityManager.GetCity(playerID, cityID)
   if (city:GetDistricts:HasDistrict(DistrictType.DISTRICT_RAILYARD) then
       BuildRailroadsInCity(city)
   end
end

function BuildRailroadsInCity(_city)
   for _ , plot in _city:GetOwnedPlots() do
       if plot:isRoute() then
           RouteBuilder.SetRouteType(plot, Railroad.Index)
       end
   end
end

function isRailyard(districtType)
   return districtType == "DISTRICT_RAILYARD"
end

function OnDistrictPillaged()

end

Events.DistrictBuildProgressChanged.Add(OnDistrictFinished)
Events.DistrictPillaged.Add(OnDistrictPillaged)
--Events.CityTileOwnershipChanged.Add(UpdateRailroads)
 
Last edited:
No thoughts?

For anyone interested I'm referencing the LUA code from the "City Roads" mod that makes builders build a road anytime they build an improvement.

I'm also curious how one might add a cost of X Iron and Coal to build a Railyard, if that's maybe a little easier to answer.
 
Might have found one issue....

Code:
for _, plot in _city:GetOwnedPlots() do
seems like it probably should be:
Code:
for plot in _city:GetOwnedPlots() do
 
No thoughts?
From what I read here and on reddit, I'm afraid that there are less and less people interested in gameplay modding for civ6 (IDK for Discords, @LeeS @Deliverator ?)

It's a shame, because even if civ6 is more limited than civ5/civ4 on that side (and NOT only because of the lack of DLL source release), there is still a lot of potential using Lua scripts for scenarios and mods such as what you want to do.

And I hope it will change, else civ7 may have even less capabilities if Firaxis think there is no need for gameplay scripts.

Because even if in my opinion they've caused the shortage of gameplay modders by their own actions (source code and gameplay method added later in civ5 development cycle, and no road map for modding tools, not enough gameplay related methods exposed, no source code release for civ6) maybe they're interpreting it the other way (not a lot of gameplay mods for civ5/6 = don't waste development time for them in civ7)

Now, back on your issue, I can't help on specifics, but when I was modding civ6, I used "print" statement everywhere in my scripts to debug them, you could add one at the beginning and the end to check if it's at least fully loaded in the lua.log
 
Yea I've read the same, and I agree. I'm a little disappointed I've gotten into it so late.

In any case, I found out that I was missing a line in the LUA log where it was failing to even load the script due to syntax errors, so I fixed all of those and my print statements are working successfully. The line currently giving me issues is commented:

Code:
function OnDistrictFinished(playerID, districtID, cityID, X, Y, districtType, era, civilization, percentComplete, Appeal, isPillaged)
    print("District Type: "..districtType)
    if isRailyard(districtType) then
        --print("District is a Railyard! PercentComplete = "..percentComplete)
        if (percentComplete == 100) then
            local player = Players[playerId]
            local city = player:GetCities():FindID(cityID)     -- THIS LINE IS FAILING!!
            --local city = CityManager.GetCity(player, cityID)
            BuildRailroadsInCity(city)
        end
    end
end

The error I'm getting from the LUA log is: attempt to index a nil value.

Perhaps my LUA is a little incomplete, but I'm not sure exactly what I'm indexing here. I suppose it's probably either the function "GetCities()" or "FindID()". I copied a similar bit of code from another mod that looks like this:

Code:
local player = Players[playerId];
local unit = player:GetUnits():FindID(unitId);

The modding companion guide I'm working with suggests that the syntax should be exactly the same for finding a city.
 
Okay, so with the ability to troubleshoot I have been able to successfully make it build railroads upon district completion!

In the off chance that anyone needs this information let me know...I'll be happy to help you out. It was mostly a matter of syntax and making sure I'm using the objects properly.

ANYWHO...Gonna try and make it cost coal and iron now...............................
 
A few updates for anyone interested.

I did manage to get this all working fairly well, but have settled on a strategy where finishing the railyard creates a unit called the "Railroad Builder" which constructs railroads as it moves and expires after building X amount. It doesn't require using charges, so you can basically just give it a destination and forget about it. This works pretty well, and I can even enforce a condition that new railroads must be built adjacent to existing railroads.

Here's the code for this function:
Code:
function OnUnitMoved(playerID:number, unitID, tileX, tileY)
    local unit = getPlayer(playerID):GetUnits():FindID(unitID);
    local unitType = GameInfo.Units[unit:GetType()];

    if (unitType.UnitType == "UNIT_RAILROAD_BUILDER") then
               
        local plot = Map.GetPlot(tileX, tileY);
        local routeType = plot:GetRouteType();
        
        if (not plot:IsWater() and not(routeType == Railroad.Index) and HasAdjacentRailroad(plot)) then
            RouteBuilder.SetRouteType(plot, Railroad.Index);
            local charges = buildTracker[playerID][unitID]
            buildTracker[playerID][unitID] = charges - 1;
            if (charges <= 0) then               
                print("Destroying Rail Builder");
                UnitManager.Kill(unit);
                buildTracker[playerID][unitID] = nil;
            else
                print("Rail Builder has "..buildTracker[playerID][unitID].." charges remaining");
            end
        end
    end
end

With my success here, I'm emboldened to try more...and have decided to add a new route type called the "Subway", which is a building option as part of the railroad. Building the subway will connect all districts in the city borders which are less than or equal to 3 tiles apart by the new route type. Subways have the same movement cost as railroads, but I needed a way to distinguish them from railroads because of the second building type.

The second building is a Monorail, which is faster than the railroad (0.1 as opposed to 0.25). Constructing the Monorail will convert all railroads within X tiles to a Monorail, but requires power to maintain (requires power as long as there's an event which is something like "OnDistrictUnPowered".)

I'm adding these mostly to improve my capabilities with LUA scripting, but also because it sounds kinda cool, if ultimately useless. To make them less useless, I'm open to suggestions on other fun things that could be done with a Railyard district.

Additionally, and this is probably the whole point of this post, I'm hoping someone can point me to a reference on how to navigate the map. For example, with the Subway, I can easily get the X and Y locations of two districts, but I'm not sure how to go about getting the optimal route between the two tiles. I can get the distance easy enough, however. I'm going to play around with it for a little bit and see whether I can fudge something to work, but I suspect there's a few map modders out there who might be able to help!

Thanks!
-Ben
 
I'm familiar with A*, I'll take a look at your example and see if it helps, thanks!
 
Code:
local unit = Players[playerID]:GetUnits():FindID(unitID);
Firaxis provides an internal lua table "Players" from which the player object can be directly retrieved without needing to go through a function that will return the same data as the Players[PlayerID] methodology. The only reason Firaxis lua scripts at times use a special function instead of directly using the Players[PlayerID] method is that in certain scenario code they don't as yet have anything passed from the game engine to an "event" function, and so need to make a lookup method for finding the active player in for example a multiplayer game. Or they need to get the proper Player ID # for (as example) "CIVILIZATION_PERSIA" when initializing a scenario. For any game engine event that passes a PlayerId #, you can just directly use the Players[PlayerID] method to get the player object.

This
Code:
(routeType ~= Railroad.Index)
not
Code:
not(routeType == Railroad.Index)
Less typing and less chance of confusing the equality conditions with knots of not.

A plot that has no "route" will return "-1" for plot:GetRouteType(), so in no case should a route-less tile ever be seen as being equal to Railroad.Index

Since your code eventually is otherwise going to be making frequent database lookup calls, cache as much data as possible at game loading and then reference that cached localized data rather than making frequent database lookup calls:


Code:
local iRailBuilderIndex = GameInfo.Units["UNIT_RAILROAD_BUILDER"].Index;

function OnUnitMoved(playerID:number, unitID, tileX, tileY)
	local unit = Players[playerID]:GetUnits():FindID(unitID);
	if (unit:GetType() == iRailBuilderIndex) then
		local plot = Map.GetPlot(tileX, tileY);
        
		if (not plot:IsWater() and (plot:GetRouteType() ~= Railroad.Index) and HasAdjacentRailroad(plot)) then
			RouteBuilder.SetRouteType(plot, Railroad.Index);
			local charges = buildTracker[playerID][unitID]
			buildTracker[playerID][unitID] = charges - 1;
			if (charges <= 0) then               
				print("Destroying Rail Builder");
				UnitManager.Kill(unit);
				buildTracker[playerID][unitID] = nil;
			else
				print("Rail Builder has "..buildTracker[playerID][unitID].." charges remaining");
			end
		end
	end
end
  1. Since you are accessing plot:GetRouteType() one time only within the function, you really don't need to cache the data.
  2. Since the suggested alteration contains this variable localization at the root scoping level of the file
    Code:
    local iRailBuilderIndex = GameInfo.Units["UNIT_RAILROAD_BUILDER"].Index;
    the data retrieved for that UnitType's Index # from table <Units> will be available to all functions within the same file since all these functions will be operating at a "sub" scoping level to the "root" level of the script. By placing such variable caching statements at the top of an lua script you can more easily find them if you need to adjust them, or want to use one script as a starting template for another lua script. Additionally, localizing the data into an lua variable in this way allows you to more easily and seemlessly ensure that all your functions within the script are refering to and using the same data.

    Just beware localizing object variables for Players, Cities, Units, etc., in this manner because for example an object varialble for player # 0 (the human player in a single-player game) like this at the top of a script
    Code:
    local pHuman = Players[0]
    will not always work properly for you if you later do something like this
    Code:
    if Players[PlayerID] == pHuman then
    because in the interim between caching the data into the Object Variable and the instant the equality evaluation is conducted the data for the Human Player can have changed and in such cases the two sets of data will often not be seen as "equal".

Remember that during the mid and late game any function that is assigned as a listener to Events.UnitMoved is going to fire a lot especially if the player allowed Barbarians to be part of the game, and the map is a larger one.
 
Last edited:
Thanks Lee!

Optimization is on my radar, but for now I'm in the "Get it working first," mindset. I think I can probably use the trader mechanic to create railroads without requiring the "OnMoved" event, but I'm still not sure exactly how that would work at the moment. I just found a "RouteAddedToMap" event, so perhaps this would be of use. Give Railroad Builders the trader mechanic but let the user move it around. When a route is added to the map, check to see if the plot contains a unit with the name "Railroad Builder", then convert it to a railroad.

As to some of what you said about caching variables, I only included the one function, my full script is actually 220 lines long and I do have some cached variables at the top. That said, I am probably caching more than I need to at the moment, and less than I could. Once I've got some semblance of a functional mod, I'll go back through and see what can be streamlined. My LUA is passable at best, so I definitely appreciate these sorts of tips and I'll look to incorporate them as I continue.

Do you have any functions or tools to help divine what the arguments/returns are for any arbitrary function? The modding companion I'm working from seems to have a fairly complete list of objects/events/functions, but not all the parameters and return values are included, so it would be helpful to have a means of identifying what all this is. I found a function online that will print out the contents of a table, but this only helps for objects and event parms, not functions. A quick search suggests that accessing function arguments/return values isn't possible, but I'm hoping my search was a poor one.

Another question I think you can answer. Is there any way to directly modify or insert into the tables for units/buildings/districts etc? For example, lets consider the railroad builder. I want it to only be able to build 10 railroads. Currently, I'm doing this via a cached table.

Code:
local charges = buildTracker[playerID][unitID]

For this specific instance, that's fine, it works and I don't think I need to change it. But, just for the sake of understanding what's possible, could I instead insert a "RR Charges" entry into the table for the Railroad Builder, and then update the entry for it in the unit database? Maybe something like this:

Code:
unit.RR_Charges = 10;
Units[unitID] = unit;

I don't think this works for units because as far as I can tell the table is only returned as the result of a function, and thus is never actually exposed to the LUA engine, so it cannot be updated in this fashion. But might it work with the player table? Are there any other 'exposed' tables similar to Players[]?
 
Top Bottom