Project: Changing Culture Spread

If you create a Visual Studio project and add all the source files to it, it will index everything so you can right-click a function and say "Go to Definition" or "... Declaration" as well as "Find All References". It's extremely handy for browsing the code.

Because each plot is affected by the surrounding plots, you need to make sure that you calculate the culture to add during the turn before changing the actual culture of the plots. This means probably adding a m_iCultureToAddThisTurn field to CvPlot that is used only during the doCulture() run (it doesn't need to be written to the saved game.

CvPlayer::doCulture() probably loops over all the player's cities to call CvCity::doCulture(). You may want to leave this in but have that function do nothing and then write your new code in the CvPlayer function.
 
Good idea regarding adding my own code. Here's a rough draft of the code I'd like to write.

plotGroupPrepared
plotGroupFrontier (can I make one of these per player?)

doExpansion()
Clear the list for plotGroupPrepared
For each plot a player controls
doConsolidate(currentPlayer,currentPlot) their control​
For each plot in plotGroupFrontier
For each cardinal direction adjacent to that centralPlot
prepareAddCulture(plotGroupFrontier,currentPlot,TotalCulture,getPerimeter) to the centralPlot
if the plot is worked
if the plot is improved
prepareAddCulture(plotGroupFrontier,currentPlot,XMLimprovement_culture,1) to the centalPlot​
prepareAddCulture(plotGroupFrontier,currentPlot,XMLworking_culture,1) to the centralPlot​
addCulture()

doConsolidate(currentPlayer,currentPlot)
If currentPlot's culturePerPlot for currentPlayer is less than 10 greater than any other player's culturePerPlot for currentPlot
add to currentPlayer's culturePerPlot for currentPlot
for each other player
lessen that player's culturePerPlot for currentPlot​

prepareAddCulture(plotgroup plotGroupAssignment,plot currentPlot,float CultureAmount, int Divisor) Can I do that with the currentPlot and plotGroup thing?
assign to currentPlot in plotGroupAssignment additional culture equal to (CultureAmount/Divisor)

addCulture()
for each plot in plotGroupPrepared
add the assigned culture (can I do that?)
if the currentPlayer's culturePerPlot for currentPlot minus the highest culturePerPlot for currentPlot among other players exceeds xml_value_of_some_name
changeCulture
for each plot adjacent to currentPlot along the cardinal directions
If that plot is surrounded by friendly plots and in plotGroupFrontier (it should be, but never hurts)
remove that plot from plotGroupFrontier​

I'll also need to modify found city to add plots to the plotGroupFrontier, but I don't even know if I can do that since I haven't investigated plotGroups yet. Does what I've written make any sense?
 
I'm not sure about how to do it; seems a big project and I didn't even find yet where it is set that you can't own ocean tiles (maybe I will get my answer now :p).

But I'm dropping by to say that I'm really interested in this because I'm feeling too that the culture spread isn't realistic enough, or even isn't likable enough.

My own idea was to greatly limit its expansion over peaks. I always found it silly when I owned a plot behind a big chain of mountains. Hopefully with your mod, we will be able to NOT see this kind of things.

(And I'll try to help you, if none of the other SDK masters can ;))
 
That seems like a reasonable algorithm for a first cut. You've got some research on CvPlotGroup to do. I suggest using Visual Studio to make browsing the code easier (it indexes the use of all classes and functions) and find all references to CvPlotGroups constructor: CvPlotGroup::CvPlotGroup(). This will show you all the places a plot group is being created and hopefully stored.
 
I can't get the visual studio version to compile, something about linking errors. I followed the 2003 tutorial, except with the express 2008 version, which doesn't have a SDK platform to point to in the makefile (at least, I can't find one). Is the problem in the makefile, as I imagine?

Regarding the code, there doesn't appear to be a CvPlayer::doCulture(). I am examining the PlotGroup class to figure out how that works, and need to find the section of code that involves running through a turn, so I can trace the culture generation from there.
 
You can use VS 2008 to edit/view the source code, but you cannot compile the code directly with it. Instead, create a makefile project type and use Refar's guide to set it up to kick off the nmake commands to do compilation.

There is CvPlayer::doTurn() I believe. Probably should start there.
 
I've been hunting through the code, and found each player's turn, thanks EF

Code:
void CvPlayer::doTurn()
{
	PROFILE_FUNC();

	CvCity* pLoopCity;
	int iLoop;

	FAssertMsg(isAlive(), "isAlive is expected to be true");
	FAssertMsg(!hasBusyUnit() || GC.getGameINLINE().isMPOption(MPOPTION_SIMULTANEOUS_TURNS)  || GC.getGameINLINE().isSimultaneousTeamTurns(), "End of turn with busy units in a sequential-turn game");

	CvEventReporter::getInstance().beginPlayerTurn( GC.getGameINLINE().getGameTurn(),  getID());

	doUpdateCacheOnTurn();

	GC.getGameINLINE().verifyDeals();

	AI_doTurnPre();

	if (getRevolutionTimer() > 0)
	{
		changeRevolutionTimer(-1);
	}

	if (getConversionTimer() > 0)
	{
		changeConversionTimer(-1);
	}

	setConscriptCount(0);

	AI_assignWorkingPlots();

	if (0 == GET_TEAM(getTeam()).getHasMetCivCount(true) || GC.getGameINLINE().isOption(GAMEOPTION_NO_ESPIONAGE))
	{
		setCommercePercent(COMMERCE_ESPIONAGE, 0);
	}

	verifyGoldCommercePercent();

	doGold();

	doResearch();

	doEspionagePoints();

[B]	for (pLoopCity = firstCity(&iLoop); pLoopCity != NULL; pLoopCity = nextCity(&iLoop))
	{
		pLoopCity->doTurn();
	}[/B]

	if (getGoldenAgeTurns() > 0)
	{
		changeGoldenAgeTurns(-1);
	}

	if (getAnarchyTurns() > 0)
	{
		changeAnarchyTurns(-1);
	}

	verifyCivics();

	updateTradeRoutes();

	updateWarWearinessPercentAnger();

	doEvents();

	updateEconomyHistory(GC.getGameINLINE().getGameTurn(), calculateTotalCommerce());
	updateIndustryHistory(GC.getGameINLINE().getGameTurn(), calculateTotalYield(YIELD_PRODUCTION));
	updateAgricultureHistory(GC.getGameINLINE().getGameTurn(), calculateTotalYield(YIELD_FOOD));
	updatePowerHistory(GC.getGameINLINE().getGameTurn(), getPower());
	updateCultureHistory(GC.getGameINLINE().getGameTurn(), countTotalCulture());
	updateEspionageHistory(GC.getGameINLINE().getGameTurn(), GET_TEAM(getTeam()).getEspionagePointsEver());
	expireMessages();  // turn log

	gDLL->getInterfaceIFace()->setDirty(CityInfo_DIRTY_BIT, true);

	AI_doTurnPost();

	CvEventReporter::getInstance().endPlayerTurn( GC.getGameINLINE().getGameTurn(),  getID());
}

It pretty much just calls a bunch of functions that move the turn along. The bit in bold goes through each city and moves the turn along for each of them.

In CvCity, doTurn looks like

Code:
void CvCity::doTurn()
{
	PROFILE("CvCity::doTurn()");

	CvPlot* pLoopPlot;
	int iI;

	if (!isBombarded())
	{
		changeDefenseDamage(-(GC.getDefineINT("CITY_DEFENSE_DAMAGE_HEAL_RATE")));
	}

	setLastDefenseDamage(getDefenseDamage());
	setBombarded(false);
	setPlundered(false);
	setDrafted(false);
	setAirliftTargeted(false);
	setCurrAirlift(0);

	AI_doTurn();

	bool bAllowNoProduction = !doCheckProduction();

	doGrowth();

[B]	doCulture();[/B]

	doPlotCulture(false, getOwnerINLINE(), getCommerceRate(COMMERCE_CULTURE));

	doProduction(bAllowNoProduction);

	doDecay();

	doReligion();

	doGreatPeople();

	doMeltdown();

	updateEspionageVisibility(true);

	if (!isDisorder())
	{
		for (iI = 0; iI < NUM_CITY_PLOTS; iI++)
		{
			pLoopPlot = getCityIndexPlot(iI);

			if (pLoopPlot != NULL)
			{
				if (pLoopPlot->getWorkingCity() == this)
				{
					if (pLoopPlot->isBeingWorked())
					{
						pLoopPlot->doImprovement();
					}
				}
			}
		}
	}

	if (getCultureUpdateTimer() > 0)
	{
		changeCultureUpdateTimer(-1);
	}

	if (getOccupationTimer() > 0)
	{
		changeOccupationTimer(-1);
	}

	if (getHurryAngerTimer() > 0)
	{
		changeHurryAngerTimer(-1);
	}

	if (getConscriptAngerTimer() > 0)
	{
		changeConscriptAngerTimer(-1);
	}

	if (getDefyResolutionAngerTimer() > 0)
	{
		changeDefyResolutionAngerTimer(-1);
	}

	if (getHappinessTimer() > 0)
	{
		changeHappinessTimer(-1);
	}

	if (getEspionageHealthCounter() > 0)
	{
		changeEspionageHealthCounter(-1);
	}

	if (getEspionageHappinessCounter() > 0)
	{
		changeEspionageHappinessCounter(-1);
	}

	if (isOccupation() || (angryPopulation() > 0) || (healthRate() < 0))
	{
		setWeLoveTheKingDay(false);
	}
	else if ((getPopulation() >= GC.getDefineINT("WE_LOVE_THE_KING_POPULATION_MIN_POPULATION")) && (GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("WE_LOVE_THE_KING_RAND"), "Do We Love The King?") < getPopulation()))
	{
		setWeLoveTheKingDay(true);
	}
	else
	{
		setWeLoveTheKingDay(false);
	}

	// ONEVENT - Do turn
	CvEventReporter::getInstance().cityDoTurn(this, getOwnerINLINE());

	// XXX
#ifdef _DEBUG
	{
		CvPlot* pPlot;
		int iCount;
		int iI, iJ;

		for (iI = 0; iI < NUM_YIELD_TYPES; iI++)
		{
			FAssert(getBaseYieldRate((YieldTypes)iI) >= 0);
			FAssert(getYieldRate((YieldTypes)iI) >= 0);

			iCount = 0;

			for (iJ = 0; iJ < NUM_CITY_PLOTS; iJ++)
			{
				if (isWorkingPlot(iJ))
				{
					pPlot = getCityIndexPlot(iJ);

					if (pPlot != NULL)
					{
						iCount += pPlot->getYield((YieldTypes)iI);
					}
				}
			}

			for (iJ = 0; iJ < GC.getNumSpecialistInfos(); iJ++)
			{
				iCount += (GET_PLAYER(getOwnerINLINE()).specialistYield(((SpecialistTypes)iJ), ((YieldTypes)iI)) * (getSpecialistCount((SpecialistTypes)iJ) + getFreeSpecialistCount((SpecialistTypes)iJ)));
			}

			for (iJ = 0; iJ < GC.getNumBuildingInfos(); iJ++)
			{
				iCount += getNumActiveBuilding((BuildingTypes)iJ) * (GC.getBuildingInfo((BuildingTypes) iJ).getYieldChange(iI) + getBuildingYieldChange((BuildingClassTypes)GC.getBuildingInfo((BuildingTypes) iJ).getBuildingClassType(), (YieldTypes)iI));
			}

			iCount += getTradeYield((YieldTypes)iI);
			iCount += getCorporationYield((YieldTypes)iI);

			FAssert(iCount == getBaseYieldRate((YieldTypes)iI));
		}

		for (iI = 0; iI < NUM_COMMERCE_TYPES; iI++)
		{
			FAssert(getBuildingCommerce((CommerceTypes)iI) >= 0);
			FAssert(getSpecialistCommerce((CommerceTypes)iI) >= 0);
			FAssert(getReligionCommerce((CommerceTypes)iI) >= 0);
			FAssert(getCorporationCommerce((CommerceTypes)iI) >= 0);
			FAssert(GET_PLAYER(getOwnerINLINE()).getFreeCityCommerce((CommerceTypes)iI) >= 0);
		}

		for (iI = 0; iI < GC.getNumBonusInfos(); iI++)
		{
			FAssert(isNoBonus((BonusTypes)iI) || getNumBonuses((BonusTypes)iI) >= ((isConnectedToCapital()) ? (GET_PLAYER(getOwnerINLINE()).getBonusImport((BonusTypes)iI) - GET_PLAYER(getOwnerINLINE()).getBonusExport((BonusTypes)iI)) : 0));
		}
	}
#endif
	// XXX
}

I'm pretty sure the bit in bold is the one I care about.


So, here's the plan at the moment:

1. I'm deleting the call to doCulture in CvCity
2. I'm creating a Header file called CvExpansion.h where I'll declare all of my new stuff.
3. I'm creating a CPP file called CvExpansion.cpp where I'll define all of my new stuff.
4. I'm inserting a call to doExpansion()[to be refined in CvExpansion.cpp] in the CvPlayer::doTurn fxn. If figure if I put it in before the call to doTurn for the cities loop, the net result for the cities should be the same. hopefully.
 
Alright, I've just about finished triangulating on where I need to change the code. Does anyone know where the code for when a player founds a new city is? My thought is that wherever that occurs, the plotgroup for that player will be updated. I can mimic that to generate the player's frontier plotgroup, and update that plotgroup in doExpansion in the CvPlayer::doTurn fxn.
 
I highly recommend against creating a new .cpp file unless it really, really makes sense. It requires modifying the makefile which makes it more tedious for other modders to merge your change with their mod. And from a logical point of view it makes sense to belong to the CvPlayer class, and you'll want access to the CvPlayer functions and member variables.

As for founding cities, I think it's CvPlayer::found() or foundCity(). Just do a search through files for "found" though you'll get stuff for religions and corporations. Or use the Python API to find the entry point in CyPlayer.
 
This is actually remarkably similar to a proposal I had back when Civ 3 was still in its development stages, and I had hoped to see it implemented right on through Civ4. In fact, I had incorporated it into the early design plans for my Civ4 mod a few years ago.

I scrapped it though for primarily two reasons... one, I lacked the coding knowledge (I don't anymore.. hmm! ). And two, running through these checks every turn on every plot would really, really slow the game down I think. Having the game take into account geography when considering cultural borders controlled from the city code would still be quite possible though, and I would definitely like to include that if not something closer to what I had originally envisioned (which is essentially what you're talking about now).

If you wanted to do this through the cities like they currently do now, it might be best to have some kind of "culture resistance" value. When a city's border pops, the cultural value has to overcome this natural resistance to have the player control the tile. This would create really nice, realistic borders moving quickly along rivers but not over them, and generally staying along hills and mountains but not including them immediately.

Of course, this would have big gameplay effects since it would make it much harder for certain tiles to be included in your cultural borders.
 
Ok... I've whipped this up. The resistance values can be easily adjusted in the XML, and it uses the existing city culture code rather than doing it by tile..



As you can see, the borders are resisting moving into the jungle and hill tiles... the borders around Washington avoided that Desert tile until the city's next border pop when the culture level was sufficient to overcome the tile's culture spread resistance.
 

Attachments

  • new borders.jpg
    new borders.jpg
    342 KB · Views: 305
Oh, very cool! That's pretty much what I was going for, but a lot simpler! Care to explain? or upload?
 
Yeah, I'm interested too. Would most likely do some tweaks but I'd add it without a doubt :)
 
Yeah, It's actually very few changes, so I'll extract the good stuff and upload it for you guys. All of the new values can be tweaked in the XML to your pleasure, so it probably won't require any further SDK or python work... The only thing you'd have to change is if you wanted these values to apply to the original eight squares around the city. Currently I've given those as freebies regardless of the terrain.

Ok, but before I upload it....

It currently factors in:

Is it on the other side of a river from the city?
Is it on a hill?
Is it a peak?
Does it have a route?
What's the terrain?
What kind of feature does it have (forests, jungles, ice, etc.)?

Some things I will toss in when I get home:
Is it along a river (will only check this if it's not across a river)?
Is it on the coast?
Does it have an improvement on it?
Is it adjacent to a certain improvement? You can use this if you want Forts and Cottages to reduce the culture spread resistance of surrounding tiles making it easier for borders to spread into more hostile terrains.

So since you guys would like to use this, do I need to add values for anything else??
 
I'm curious to what this will do for a larger cities radius mod I have in my SDK, that some players use. By the time a city hits influential culture, will it's culture spread be large enough for the city to work all 37 tiles?
 
How does the 'culture resistance' work? How is it measured and compared to the city's cultural output?

Could you add a check that's like 'is it behind a peak'? You know, getting culture over one isolated peak might be okay... but not over too many peaks. I will throw in a check to see if the civilization can work peaks, in which case it won't matter, but I will take care of this myself.

Hmm... Could you make it so if the culture should expand in the direction the river is flowing, it's easier for it to expand?

I may ask for too much :lol:
 
I'm curious to what this will do for a larger cities radius mod I have in my SDK, that some players use. By the time a city hits influential culture, will it's culture spread be large enough for the city to work all 37 tiles?
Probably not... and certainly, a city that has a bad starting location will control fewer tiles as well. At least until it gets towards the higher culture levels. So certainly this isn't just cosmetic. It will have gameplay ramifications. But give it a shot.

Some ways to counter this would be to make culture spread 1 more range than they currently do. So instead of the first border popping resulting in a fat cross, it would go to the next largest size... but some of the squares would be shaved off because of the terrain resulting in a much more ragged (and interesting) border area.

Incorporating Jeckel's tile control mod could be useful to give players more control over where those borders will form so that you don't lose any workable tiles.

I also think that incorporating my improvement upgrades modcomp would be useful too since that affects the upgrade rates of improvements based on the surrounding terrain. These two things together I think would give a much more natural and organic flow to the game with borders filling favorable land and improvements built on that land growing faster than on less desirable tiles. It essentially simulates a rural population without having to actually code such a game mechanic into the game.
 
Since cultural spread is the topic at hand, why not consider adding the culture spread from trade routes the lopez did in cultural influences?
 
Top Bottom