Adding xml tags that work through Lua (eg, policy-dependent units & buildings)

Pazyryk

Deity
Joined
Jun 13, 2008
Messages
3,584
If you want to do this through DLL rather than Lua, see Adding new XML Tags/Tables to work through the DLL.

Let's say you want a simple policy prerequisite to build a building (there isn't one in the base game).

You can add columns to existing core tables via SQL. Here's the code you need in an SQL file:

Code:
ALTER TABLE Buildings ADD COLUMN 'PrereqPolicy' TEXT DEFAULT NULL;

SQL is added exactly as you add XML files (as an OnModActivated). Just make sure that the above file runs before your XML where you are using the new "PrereqPolicy" tag.

Now you need to add some Lua code to tell the dll what to do with this new table column:
Code:
GameEvents.PlayerCanConstruct.Add(
function(iPlayer, buildingTypeID)
	local prereqPolicy = GameInfo.Buildings[buildingTypeID].PrereqPolicy
	if prereqPolicy then
		local player = Players[iPlayer]
		local policyID = GameInfo.Policies[prereqPolicy].ID
		if not player:HasPolicy(policyID) then
			return false
		end
	end
	return true
end)

That's it! You can now use this new tag to make any building construction dependent on having any particular policy, just like you use any other xml tag.

Following the example above, you should be able to add PolicyPrereq tags to Units and Techs in a similar way using these GameEvents:

GameEvents.PlayerCanTrain.Add(function(iPlayer, unitTypeID) ... end)
GameEvents.PlayerCanResearch.Add(function(iPlayer, techTypeID) ... end)

One suggestion: I actually add the above tag as "PrereqPolicy_paz". This is so that there are no problems down the road if the developers add a tag with this exact name (or perhaps a dll modder, once that is possible).

Also note that the above prereq will not be added to the pedia. You'll have to do that yourself. Otherwise, there will be no indication in the game why the building cannot be built.

**************************************************************
Added 12/29


OK, let's say you want to run a conditional test of your own using a tag "LuaReq". Some of you may remember many such tags in FFH (and mods thereof). They are extremely powerful because you can write short Lua functions to create whatever condition you want.

As above, you add another column to Buildings like this:

Code:
ALTER TABLE Buildings ADD COLUMN 'LuaReq' TEXT DEFAULT NULL;

Now let's make a Monument conditional on something, say turn > 5 (you can do this in XML of course, but I do everything in SQL so it's much easier for me this way):

Code:
UPDATE Buildings SET LuaReq='Game.GetGameTurn() > 5' WHERE Type='BUILDING_MONUMENT';

That works, but it's pretty limited. What you really want to do is this:

Code:
UPDATE Buildings SET LuaReq='RunUserTestFunction()' WHERE Type='BUILDING_MONUMENT';

If RunUserTestFunction() returns true, then Monument can be built. If it reuturns false, then Monument cannot be built. (Example code below returns true when turn is > 5 and iPlayer == 0.) The code to make this all work for Buildings is below (adds both LuaReq and PrereqPolicy). The only thing you need to do is supply your own Lua condition test functions and call the compile function during game init (the very last line below). If you want to add LuaReq to other tables, hopefully you can use this as skeleton code to do it (be sure to add any Lua callback tags to the two tables on lines 2 and 3).

Code:
--add any tags and their tables to the two tables below in matching pairs (only for tags that "call" a Lua function)
local luaCallbackTable =	{ "Buildings"}
local luaCallbackTag =		{ "LuaReq"}
---------------------------------------------------------------
-- Compile Lua callback text (run once at game init)
---------------------------------------------------------------
local bLuaCallbacksCompiled = false
local LuaCallback = {}	--will hold all lua functions (as function values) keyed by function text exactly as it appears in table
args = {}	--loadstring always compiles in global environment, so use this global to hold arguments for function call

function CompileTableLuaCallbacks()
	-- this must run after functions defined!
	print("Compiling table Lua callbacks...")
	for i, callbackTable in ipairs(luaCallbackTable) do
		local callbackTag = luaCallbackTag[i]
		for item in GameInfo[callbackTable]() do
			local functText = item[callbackTag]
			if functText and not LuaCallback[functText] then
				print("Table "..callbackTable..", tag "..callbackTag..", function "..functText)
				local funct = assert(loadstring("return "..functText))
				LuaCallback[functText] = funct
			end
		end
	end
	bLuaCallbacksCompiled = true
end

---------------------------------------------------------------
-- Functions for new tags added to tables (including Lua callbacks)
---------------------------------------------------------------
-- Building requirements
GameEvents.PlayerCanConstruct.Add(
function(iPlayer, buildingTypeID)
	local player = Players[iPlayer]
	local building = GameInfo.Buildings[buildingTypeID]
	local prereqPolicy = building.PrereqPolicy
	if prereqPolicy then
		local policyID = GameInfo.Policies[prereqPolicy].ID
		if not player:HasPolicy(policyID) then
			return false
		end
	end
	local functText = building.LuaReq
	if functText and bLuaCallbacksCompiled then
		local funct = LuaCallback[functText]
		args.iPlayer = iPlayer
		args.buildingTypeID = buildingTypeID
		if not funct() then	--!!! that was the function call, so make sure args holds whatever you need !!!
			return false
		end
	end
	--add any additional restrictions to buildings here
	return true
end)


---------------------------------------------------------------
-- An example user defined function (can be in another file but must be same state)
-- Note that any arguments must be passed through a global (I use args here)
---------------------------------------------------------------

function RunUserTestFunction()
	print("called RunUserTestFunction")
	iPlayer = args.iPlayer
	buildingTypeID = args.buildingTypeID
	print(iPlayer)
	print(buildingTypeID)
	if Game.GetGameTurn() > 5 and iPlayer == 0 then
		return true
	end
	return false
end


---------------------------------------------------------------
-- !!! Important !!! Put this somewhere in your program where it runs
-- at game init (after all functions above have been defined)
---------------------------------------------------------------
CompileTableLuaCallbacks()

Edit: One correction, you should (in theory) be able to add an argument to a LuaReq function call. Just be aware that it will be compiled in a global environment. So ReqTurn(5) should work and ReqTurn(g) should work if g is a global, but ReqTurn(x) won't work if x is local. Of course, if the value you want is in a global, you can get it from within the function anyway (that's how I use args above) so I'm not sure if that will be useful. But the ability to supply a hard value would certainly be useful. For example, you could have many buildings call the same ReqTurn function, but each has a different turn requirement. Or you could require different gold levels in treasury. Or different total culture. Or... The possibilities are really endless. (I haven't actually tested this yet.)

**************************************************************
There were a bunch of "Can" game events added in patch 1.0.1.332. Here's the whole list:
Code:
(Lua) Added GameEvents.CityCanBuyAnyPlot(ownerID, cityID) (TestAll)
(Lua) Added GameEvents.CityCanBuyPlot(ownerID, cityID, plotX, plotY) (TestAll)
(Lua) Added GameEvents.CityCanCreate(ownerID, cityID, projectTypeID); (TestAll)
(Lua) Added GameEvents.CityCanMaintain(ownerID, cityID, processTypeID); (TestAll)
(Lua) Added GameEvents.CityCanPrepare(ownerID, cityID, specialistTypeID); (TestAll)
(Lua) Added GameEvents.CityCanTrain(ownerID, cityID, unitTypeID); (TestAll)
(Lua) Added GameEvents.PlayerAdoptPolicy(playerID, policyTypeID); (Hook)
(Lua) Added GameEvents.PlayerAdoptPolicyBranch(playerID, policyBranchTypeID); (Hook)
(Lua) Added GameEvents.PlayerCanAdoptPolicy(playerID, policyTypeID); (TestAll)
(Lua) Added GameEvents.PlayerCanAdoptPolicyBranch(playerID, policyBranchTypeID); (TestAll)
(Lua) Added GameEvents.PlayerCanConstruct(playerID, buildingTypeID); (TestAll)
(Lua) Added GameEvents.PlayerCanCreate(playerID, projectTypeID); (TestAll)
(Lua) Added GameEvents.PlayerCanEverReseearch(playerID, techtypeID); (TestAll)
(Lua) Added GameEvents.PlayerCanMaintain(playerID, processTypeID); (TestAll)
(Lua) Added GameEvents.PlayerCanPrepare(playerID, specialistTypeID); (TestAll)
(Lua) Added GameEvents.PlayerCanResearch(playerID, techTypeID); (TestAll)
(Lua) Added GameEvents.PlayerCanTrain(playerID, unitTypeID); (TestAll)
(Lua) Added GameEvents.TeamSetHasTech(teamID, techID); (Hook)

**************************************************************
Edit 11/2012: Thanks to DonQuiche, we now have a complete wiki listing all of the GameEvents. (A few errors here and there, but on the whole it is an awesome resource...)
 
This is certainly a helpful little snippet. But seriously, give us some sneak peaks into what you are working on. I am practically jumping out of my seat after seeing all your code.
 
Here's a teaser from the 1st line of my mod's description: "You begin knowing only that you are a small tribe, belonging to one of three races, without name, leader or history." I still have about 10 items on the checklist before alpha release, and each is a full day of work. Perhaps 4 - 8 weeks away.

Back on thread topic: I've also figured out a simple generic way to add "LuaReq" as a table column. So you add something like "ReqLuaExample()" as LuaReq for a building. The building can only be built if your function ReqLuaExample() returns a true value. I'll post this in the next couple days.
 
Added code for a new LuaReq tag to the original post. This allows you to specify any Lua function (that you supply) that must return true for a building to be buildable.
 
This example code sure helped me a lot, thanks! :)

I added building and unit restrictions based on the current state of world diplomacy, to create an alternative path to diplomatic victories (rather than buying everyone out). If interested, check here.
 
Awesome work right here, free to use I suppose?

Yes, of course.

Also, you wouldn't happen to know where I can find a tutorial on adding new tables, would you?

No. I'll probably write a general SQL tutorial at some point that includes table creation (I don't do any of this in xml so I've forgotten how). I'll edit this post later with an example when I have access.
 
@Pouakai: with some examples available, adding new tables in XML is pretty straightforward. You can look for instance at the one I posted in the "culture per population" topic, then look at CIV5Buildings.xml. You will see it's the same thing really.
 
Thank you Paz. Very good example! I have a further question here: how to make this policy requirement shown in tooltips (such as the one in techtree when our pointer hang over the buiding)?

Some of these you can do by modifying "Help" fields in the existing xml files. But others are generated "on the fly" within the UI Lua files. I haven't done a lot of this yet so you're going to have to figure it out on a case-by-case basis.
 
There is a player:GetCurrentEra() at the wiki. I would assume that it returns current era ID as an integer, but I haven't used it.

You're right, it returns 0-6.

What content type do you publish this lua as?


EDIT:
If it's not obvious what I'm trying to do... Your post above got me thinking I could replicate, in LUA, the variation by era of units (like workers and great people). But it could also be useful to, e.g., simply obsolete a unit/building by era rather than a specific tech. I imagine you could do some interesting things with tech subtrees that don't have prerequisites as such.

But... I'm no good with LUA modding, and this isn't working:

SQL:
Spoiler :
Code:
ALTER TABLE Units ADD COLUMN 'UnitArtInfoEraVariationStartEra_RED' TEXT DEFAULT NULL;
ALTER TABLE Units ADD COLUMN 'UnitArtInfoEraVariationStopEra_RED' TEXT DEFAULT NULL;

-- Spanish knight
INSERT INTO "Units" ('Type', 'Description', 'Civilopedia', 'Strategy', 'Help', 'Requirements', 'Combat', 'RangedCombat', 'Cost', 'Moves', 'Immobile', 'Range', 'BaseSightRange', 'Class', 'Special', 'Capture', 'CombatClass', 'Domain', 'CivilianAttackPriority', 'DefaultUnitAI', 'Food', 'NoBadGoodies', 'RivalTerritory', 'MilitarySupport', 'MilitaryProduction', 'Pillage', 'Found', 'FoundAbroad', 'CultureBombRadius', 'GoldenAgeTurns', 'IgnoreBuildingDefense', 'PrereqResources', 'Mechanized', 'Suicide', 'CaptureWhileEmbarked', 'PrereqTech', 'ObsoleteTech', 'GoodyHutUpgradeUnitClass', 'HurryCostModifier', 'AdvancedStartCost', 'MinAreaSize', 'AirUnitCap', 'NukeDamageLevel', 'WorkRate', 'NumFreeTechs', 'RushBuilding', 'BaseHurry', 'HurryMultiplier', 'BaseGold', 'NumGoldPerEra', 'SpreadReligion', 'IsReligious', 'CombatLimit', 'RangeAttackOnlyInDomain', 'RangeAttackIgnoreLOS', 'RangedCombatLimit', 'XPValueAttack', 'XPValueDefense', 'SpecialCargo', 'DomainCargo', 'Conscription', 'ExtraMaintenanceCost', 'NoMaintenance', 'Unhappiness', 'UnitArtInfo', 'UnitArtInfoCulturalVariation', 'UnitArtInfoEraVariation', 'ProjectPrereq', 'SpaceshipProject', 'LeaderPromotion', 'LeaderExperience', 'DontShowYields', 'ShowInPedia', 'MoveRate', 'UnitFlagIconOffset', 'PortraitIndex', 'IconAtlas', 'UnitFlagAtlas', 'UnitArtInfoEraVariationStartEra_RED', 'UnitArtInfoEraVariationStopEra_RED')
	SELECT	("UNIT_SPANISH_KNIGHT"), ("TXT_KEY_UNIT_SPANISH_KNIGHT"), "Civilopedia", "Strategy", "Help", "Requirements", "Combat", "RangedCombat", "Cost", "Moves", "Immobile", "Range", "BaseSightRange", "Class", "Special", "Capture", "CombatClass", "Domain", "CivilianAttackPriority", "DefaultUnitAI", "Food", "NoBadGoodies", "RivalTerritory", "MilitarySupport", "MilitaryProduction", "Pillage", "Found", "FoundAbroad", "CultureBombRadius", "GoldenAgeTurns", "IgnoreBuildingDefense", "PrereqResources", "Mechanized", "Suicide", "CaptureWhileEmbarked", "PrereqTech", "ObsoleteTech", "GoodyHutUpgradeUnitClass", "HurryCostModifier", "AdvancedStartCost", "MinAreaSize", "AirUnitCap", "NukeDamageLevel", "WorkRate", "NumFreeTechs", "RushBuilding", "BaseHurry", "HurryMultiplier", "BaseGold", "NumGoldPerEra", "SpreadReligion", "IsReligious", "CombatLimit", "RangeAttackOnlyInDomain", "RangeAttackIgnoreLOS", "RangedCombatLimit", "XPValueAttack", "XPValueDefense", "SpecialCargo", "DomainCargo", "Conscription", "ExtraMaintenanceCost", "NoMaintenance", "Unhappiness",
			("ART_DEF_UNIT_U_SPANISH_KNIGHT"), "UnitArtInfoCulturalVariation", "UnitArtInfoEraVariation", "ProjectPrereq", "SpaceshipProject", "LeaderPromotion", "LeaderExperience", "DontShowYields", "ShowInPedia", "MoveRate", "UnitFlagIconOffset", "PortraitIndex", "IconAtlas", "UnitFlagAtlas", NULL, 5
	FROM "Units" WHERE (Type = "UNIT_KNIGHT");
INSERT INTO "Language_en_US" ( 'Tag', 'Text' )
	VALUES ( 'TXT_KEY_UNIT_SPANISH_KNIGHT', 'Knight Spain' );
INSERT INTO "Unit_AITypes" ('UnitType', 'UnitAIType')
	SELECT ("UNIT_SPANISH_KNIGHT"), "UnitAIType"
	FROM "Unit_AITypes" WHERE (UnitType = "UNIT_KNIGHT");
INSERT INTO "Unit_ClassUpgrades" ('UnitType', 'UnitClassType')
	SELECT ("UNIT_SPANISH_KNIGHT"), "UnitClassType"
	FROM "Unit_ClassUpgrades" WHERE (UnitType = "UNIT_KNIGHT");
INSERT INTO "Civilization_UnitClassOverrides" ( 'CivilizationType', 'UnitClassType', 'UnitType' )
	VALUES ( 'CIVILIZATION_SPAIN', 'UNITCLASS_KNIGHT', 'UNIT_SPANISH_KNIGHT' );
INSERT INTO "Unit_Flavors" ('UnitType', 'FlavorType', 'Flavor')
	SELECT ("UNIT_SPANISH_KNIGHT"), "FlavorType", "Flavor"
	FROM "Unit_Flavors" WHERE (UnitType = "UNIT_KNIGHT");
INSERT INTO "Unit_FreePromotions" ('UnitType', 'PromotionType')
	SELECT ("UNIT_SPANISH_KNIGHT"), "PromotionType"
	FROM "Unit_FreePromotions" WHERE (UnitType = "UNIT_KNIGHT");
INSERT INTO "Unit_ResourceQuantityRequirements" ('UnitType', 'ResourceType', 'Cost')
	SELECT ("UNIT_SPANISH_KNIGHT"), "ResourceType", "Cost"
	FROM "Unit_ResourceQuantityRequirements" WHERE (UnitType = "UNIT_KNIGHT");

LUA:
Spoiler :
Code:
GameEvents.PlayerCanTrain.Add(
function(iPlayer, unitTypeID)
	local player = Players[iPlayer]
	local eraID = player:GetCurrentEra()
	local eraStop = GameInfo.Units[unitTypeID].UnitArtInfoEraVariationStopEra_RED
	local eraStart = GameInfo.Units[unitTypeID].UnitArtInfoEraVariationStartEra_RED
	if eraStop then
		if eraID >= eraStop then return false
		end
	end
	if eraStart then
		if eraID < eraStart then return false
		end
	end
	return true
end)
 
For SQL, some aesthetic issues with quotes (not fatal but ugly and confusing):

Don't use double quotes. That's advice from the SQLite documentation itself.

Change this: INSERT INTO "Unit_AITypes" ('UnitType', 'UnitAIType')
To this: INSERT INTO Unit_AITypes (UnitType, UnitAIType)
--you don't need any quotes for Tables and Columns that already have been defined.

To be honest, I don't really understand what you are doing with this:

INSERT INTO "Unit_AITypes" ('UnitType', 'UnitAIType')
SELECT ("UNIT_SPANISH_KNIGHT"), "UnitAIType"
FROM "Unit_AITypes" WHERE (UnitType = "UNIT_KNIGHT");

But then, I never use very complicated SQL statements like this, so I might not understand something very basic. I've been able to add all of my game elements with a simple repetitive syntax that allows many (100s) of rows per INSERT statement. I'm actually fairly ignorant when it comes to more advanced SQL logic (I haven't needed it), which is I guess what you are doing here.


I don't see any errors in the Lua. In fact, I'm pretty sure it has no errors. When I run into this situation, the first question I always ask is: Did my changes/additions get into the DB correctly? You should be looking at the DB everytime you alter it (with SQlite Manager), or else you will be wasting time troubleshooting something that is perfectly correct.

What content type do you publish this lua as?

I don't understand this part of your question. Do you mean: how do I add a Lua file?
 
For SQL, some aesthetic issues with quotes (not fatal but ugly and confusing):
The quotes are from Gedemon's R.E.D. modpack. It works, so I have not played with them.

To be honest, I don't really understand what you are doing with this:

INSERT INTO "Unit_AITypes" ('UnitType', 'UnitAIType')
SELECT ("UNIT_SPANISH_KNIGHT"), "UnitAIType"
FROM "Unit_AITypes" WHERE (UnitType = "UNIT_KNIGHT");
Again, from Gedemon. It's the same thing for most of the entries. I'm making a copy of the rows where UNIT_KNIGHT is the UnitType, and changing it to UNIT_SPANISH_KNIGHT instead. So in this case, UNIT_SPANISH_KNIGHT gets 2 rows for 2 UnitAITypes, just like UNIT_KNIGHT does, one for UNITAI_DEFENSE and one for UNITAI_FAST_ATTACK.

The unit is my own with some art I've been playing with, but the SQL entries are a copy-paste job. The unit itself works fine. The only thing I added to the SQL that was at all different from my usual setup was the first 2 lines, and the references to them in the add to the Units table. The columns are added successfully, and the entries for those columns are added successfully.

I don't see any errors in the Lua. In fact, I'm pretty sure it has no errors.
Thanks for taking a look... it's certainly simple enough, and I can't see why it wouldn't be working. To the extent I can bend FireTuner to my will, it looks like everything is working. But I can still train a unit before the 'UnitArtInfoEraVariationStartEra_RED' and after the 'UnitArtInfoEraVariationStopEra_RED'.

I don't understand this part of your question. Do you mean: how do I add a Lua file?
The question was basically whether this is using content type "InGameUIAddin."

I know the state of LUA modding has changed since Kael's guide came out. My understanding is no one makes changes to InGame.lua or InGame.xml anymore. Is this incorrect? I've hardly used LUA, and never in CiV, so I'll go take a look at some tutorials and/or recent prior art.

Thanks again, Pazyryk!
 
Again, from Gedemon. It's the same thing for most of the entries. I'm making a copy of the rows where UNIT_KNIGHT is the UnitType, and changing it to UNIT_SPANISH_KNIGHT instead. So in this case, UNIT_SPANISH_KNIGHT gets 2 rows for 2 UnitAITypes, just like UNIT_KNIGHT does, one for UNITAI_DEFENSE and one for UNITAI_FAST_ATTACK.
I see. I tend to delete everything and re-add. It's just easier for me to keep it straight in my head that way.

Thanks for taking a look... it's certainly simple enough, and I can't see why it wouldn't be working. To the extent I can bend FireTuner to my will, it looks like everything is working. But I can still train a unit before the 'UnitArtInfoEraVariationStartEra_RED' and after the 'UnitArtInfoEraVariationStopEra_RED'.
Put a print statement in there so you know exactly what values Lua sees when it is called, or if it is being called at all.

The question was basically whether this is using content type "InGameUIAddin."
EaMain.lua is added as "InGameUIAddin", yes, with VFS=false. Then I have EaInit.lua, EaFunctions.lua, EaAI.lua, etc., all of which are included by "include" statements in EaMain.lua and have VFS=true.

No need anymore to worry about InGame.lua/InGame.xml.
 
I don't know if you can add a column in XML or not. Maybe someone else has an answer.

(I gave up XML modding a while ago. Too hard for me.;))
 
I see. I tend to delete everything and re-add. It's just easier for me to keep it straight in my head that way.
Sure, but if Firaxis patches something in that table, that way the mod may just upgrade gracefully.

Put a print statement in there so you know exactly what values Lua sees when it is called, or if it is being called at all.
Well, yeah, if you want to get technical about it. :hammer2: I have no idea why, but the LUA wasn't being executed. I set up a brand new mod, with almost the exact same setup, and suddenly it works. Very odd. :wallbash: Regardless...

It works! :woohoo: The only issue I had was that the numbers in the new columns were being stored as strings (though I had no quotes surrounding them), and comparison operators like greater/less than don't coerce strings to equivalent numbers. So, tonumber() function to the rescue, and the darn thing works.

Now. On to world domination! :mwaha:

:nono: (smily overload!)



:thanx:
 
Back
Top Bottom