Sid Meier's Civilization V

Sid Meier's Civilization V

C15's Advanced Religious Preferences
Chrisy15  [developer] Nov 19, 2023 @ 9:32am
So how does it all work, anyway?
The explanation for what the code does is actually a lot simpler than the code itself, so I may as well summarise it here lest anyone venture into the hellish foray of the mod's Lua file...

When an AI considers their choice of Religion, a GameEvent gets called which provides us with the following parameters:

(playerID, iDefaultReligion, bCivDefaultReligionFounded)
Additionally, this GameEvent allows us to return a value into the Game Core - the ID of the Religion which Firaxis will put through their validity-testing process. Thus, if we return the same iDefaultReligion value provided by the parameter then normal function will ensure; however if the bCivDefaultReligionFounded boolean tells us that iDefaultReligion is not a valid Religion to found, then we can instead return an ID that we conclude to be valid ourselves so as to avoid using Firaxis' determinate decision process.

Side note 1: I didn't consider until now what happens if multiple functions are subscribed to this GameEvent, GetReligionToFound; I think I accidentally did such a thing while testing and as a result the results of both functions seemed to have just been ignored, but as I didn't think to actually test the matter I wouldn't take this conclusion as meaning anything more than "Mods which use this GameEvent are probably incompatible but they shouldn't break the game outright by being so."

The function's first job is to handle the potential of the Player in question having multiple Preferred Religions. The Preferred Religions for each Civilization, alongside any Priority value which has been assigned to them, are queried from the database and cached like so:

local tCivilizationReligions = {} for row in DB.Query("SELECT DISTINCT a.ID CivID, b.ID ReligionID, (CASE WHEN EXISTS (SELECT 1 FROM C15_CivilizationReligionPriorities WHERE CivilizationType = a.Type AND ReligionType = b.Type) THEN (SELECT Priority FROM C15_CivilizationReligionPriorities WHERE CivilizationType = a.Type AND ReligionType = b.Type) ELSE 0 END) Priority FROM Civilizations a, Religions b, Civilization_Religions c WHERE a.Type = c.CivilizationType AND b.Type = c.ReligionType ORDER BY Priority") do if tCivilizationReligions[row.CivID] then table.insert(tCivilizationReligions[row.CivID], row.ReligionID) else tCivilizationReligions[row.CivID] = {row.ReligionID} end end
As demonstrated here, an entry into C15_CivilizationReligionPriorities is meaningless unless a corresponding entry exists in Civilization_Religions; this was done to avoid this new table from unexpectedly interfering with the proper Preferred Religion infrastructure and causing unforeseen issues for users or modders unaware of this mod's existence.

Firaxis provides no support for a Civilization having multiple entries into Civilization_Religions, and so if the function detects that the Player's Civilization has multiple entries then the iDefaultReligion parameter is abandoned entirely and the cached preference data takes precedence. These Preferred Religions are ordered primarily by the Priority value provided by C15_CivilizationReligionPriorities, with the rowid sequence in Civilization_Religions acting as a tiebreaker; as entries in Civilization_Religions which lack a corresponding entry in C15_CivilizationReligionPriorities are given a Priority of 0, this means that the rowid will be the sole arbiter in situations where no entry to C15_CivilizationReligionPriorities is provided. This system runs contrary to how Firaxis "handles" multiple entries, where they instead settle for the first Religion in the Religions table to be preferred by the CivilizationType in question; shifting the agency to the order of the rows in Civilization_Religions thus provides a much greater degree of control than Firaxis' "implementation", and the C15_CivilizationReligionPriorities table serves to provide an actual data-driven solution to the matter.

local iCiv = pPlayer:GetCivilizationType() local tPreference = tCivilizationReligions[iCiv] if tPreference and #tPreference > 1 then for index = 1, #tPreference do local iReligion = tPreference[index] print("Preferred: ", iReligion) if IsReligionAvailable(iReligion, false) then return iReligion else tReligionsToConsider[#tReligionsToConsider+1] = {Religion = iReligion, Founded = not IsReligionAvailable(iReligion, false)} end end else tReligionsToConsider[1] = {Religion = iDefaultReligion, Founded = bCivDefaultReligionFounded} end
As these are defined "Preferred Religions" they are treated as comparable to the iDefaultReligion value provided by the GameEvent, and thus have the potential to return back to the Game Core at this early point if the IsReligionAvailable function deems that they haven't already been adopted; if they have been adopted, then they get filed into a table so that they can be considered during the Religious Group phase.

local tIgnoreList = {} for index = 1, #tReligionsToConsider do if tReligionsToConsider[index].Founded then local sType = tReligionsIndexable[tReligionsToConsider[index].Religion] local sGroup = tReligiousGroups[sType] local iReligion, tIgnoreList = C15_GroupHandler(sGroup, tIgnoreList) print("Test 1: ", iReligion) if iReligion > 0 then return iReligion end else return tReligionsToConsider[index].Religion end end
This block handles the Religious Group logic, where Religions "related" to the Preferred Religion are accumulated, selected at random, and tested for eligibility. This process utilises a set of recursive functions which explore the tree of Religious Groups constructed from the database tables C15_ReligiousGroups and C15_ReligiousGroupGroups. The tIgnoreList table is used to keep track of what Groups have already been explored; since all Religions in a Group get tested whenever a Group is considered, there should be no need to test a Group more than once and so the tIgnoreList table persists throughout this whole algorithm to avoid redundant processing of Religions which have already been concluded on.

function C15_BranchNavigator(sGroup, tRandomList, tIgnoreList) print("Branch Navigator: ", sGroup) tIgnoreList[sGroup] = true if tReligiousGroups[sGroup] then for k, v in pairs(tReligiousGroups[sGroup]) do if v.Type == "Group" then if not tIgnoreList[v.ID] then tRandomList, tIgnoreList = C15_BranchNavigator(v.ID, tRandomList, tIgnoreList) end elseif v.Type == "Religion" then tRandomList[#tRandomList+1] = v.ID end end end return tRandomList, tIgnoreList end function C15_RandomHandler(tRandom, bConsiderPreferences) print("Random Handler") local tRandomCopy = {} while #tRandom > 0 do local iRandom = JFD_GetRandom(1, #tRandom) local iReligion = tRandom[iRandom] print("Testing: ", iReligion, GameInfo.Religions[iReligion].Type) tRandomCopy[iRandom] = iReligion table.remove(tRandom, iRandom) if IsReligionAvailable(iReligion, bConsiderPreferences) then return iReligion, tRandomCopy end end return -1, tRandomCopy end function C15_GroupHandler(sGroup, tIgnoreList) print("Group Handler: ", sGroup) if sGroup then local tRandom, tIgnoreList = C15_BranchNavigator(sGroup, {}, tIgnoreList) local iReligion, tRandom = C15_RandomHandler(tRandom, false) if iReligion > 0 then return iReligion, tIgnoreList end return C15_GroupHandler(tReligiousGroups.ChildRegister[sGroup], tIgnoreList) end return -1, tIgnoreList end
The RandomHandler tests the eligibility of every Religion in a provided table in a random order. The bConsiderPreferences variable dictates whether the "eligibility" of a Religion is affected by the presence of a Civilization in the game who possesses a Preference for the Religion in question; Firaxis applies this condition in their first sweep of their Religions iteration, and that practice is followed in this algorithm as well. If an eligible Religion is found, a return can occur immediately as the algorithm's job has been completed; otherwise, the Religions are moved into a "tested" pile and are returned back out to the function which called the RandomHandler for future reference if a valid Religion is not found.

The BranchNavigator iterates through the child nodes of a passed Group - after determining that the Group hasn't been explored previously - and accumulates the Religions belonging to said Group and to Groups belonging to said Group. The ReligionType, ReligiousGroup, ParentReligiousGroup, and ChildReligiousGroup values are queried from the database and cached as strings, with the two types of data being distinguished during the caching process by their assigned "Type" value. The BranchNavigator then tests the Type of a given node of the passed Group to determine whether it's a Religion to be added to the table or a Group which must be explored via a recursive call of the BranchNavigator.

The GroupHandler synthesises these functions together to accumulate all Religions from Groups within the Group, and select a random Religion from that list. If an eligible Religion is found, then the GroupHandler can return out successfully; if not, then the GroupHandler is called recursively to repeat this process with the passed Group's ParentReligiousGroup. If no such ParentReligiousGroup exists, the GroupHandler doesn't try to explore the branches of a nil value and instead just returns an invalid Religion ID of -1 and a list of the Groups that have been explored thus far.

If the GroupHandler fails to find a valid Religion from any Group related to any Preferred Religion assigned to the Civilization in question, then the algorithm proceeds to the final state of totally random selection:

local tReligions = {} for k, v in pairs(tReligionsIndexable) do local sGroup = tReligiousGroups[v] if not (sGroup and tIgnoreList[sGroup]) then print("Iterating Indexable: ", k, v, sGroup) tReligions[#tReligions+1] = k end end local iNumReligions = #tReligions iReligion, tReligions = C15_RandomHandler(tReligions, true) print("Test 2: ", iReligion) if iReligion <= 0 then iReligion, tReligions = C15_RandomHandler(tReligions, false) print("Test 3: ", iReligion) end if iReligion > 0 then return iReligion end return iDefaultReligion
The contents of the database's Religions table have been cached at file load, and here they are iterated and accumulated in a local table for the RandomHandler to process. if a Religion belongs to a Group which features in the Ignore List, then it is ignored as that means that its validity would have already been tested by the GroupHandler.

The remaining Religions are then tested by the RandomHandler: firstly with respect for the Preferred Religions of other Civilizations in the game, and if this proves fruitless a second test is performed without such respect. This mirrors' Firaxis' implementation of the code, but as illustrated above the order in which Religions are tested is randomised and not predetermined.

Finally, if a valid Religion has been found, then it gets returned to the Game Core; if somehow no valid Religion is found, out of all the Religions in the database, then the original iDefaultReligion parameter is returned as though this function's processes never happened and the issue is left to Firaxis to resolve. Thus, through this system, the Preferred Religion consideration is drastically expanded to allow more "appropriate" - as defined by database values - results to be prioritised and the fallback process if no "appropriate" value is available has been improved to remove determinism and create "true" randomness as opposed to Firaxis' illusion of such.

TL;DR:
If Civ cannot adopt one of their Preferred Religions, a substitute Religion related to those Preferred Religions is searched for; these definitions of "Preferred" and "related" are made in the database, and can be changed as anyone wishes. In the case where a related substitute cannot be found, a substitute is instead picked at random from all the Religions in the game, contrary to how Firaxis picks a substitute by working through the list of Religoins in a predetermined and consistent order. Thus, even if no "related" Religions exist, this mod still serves to better implement Firaxis' fallback system.
Last edited by Chrisy15; Nov 19, 2023 @ 9:37am