Creating a semi-permanent effect

j_mie6

Deity
Joined
Dec 20, 2009
Messages
2,963
Location
Bristol (uni)/Swindon (home)
Hello!

For my mod I want to include storms. I have already set up the code to create storms and to manage their effects, but I need to know how to keep the visual effect of the storm going through the duration.

I am currently using the tsunami effect from gods of old (the clouds one not the coast one) and calling it with:

Code:
gDLL->getEngineIFace()->TriggerEffect((EffectTypes) GC.getInfoTypeForString("EFFECT_STORM"), pPlot->getPoint());

but this code only spawns the effect for as long as the effect lasts... I need it to last until the storm ends (which could be in several turns).

Any ideas?

Thanks,
Jamie
 
but that would override forests no? I did think about that. Plus it is an effect not improvement art (not sure if that matters)
 
Rhye's plague is done with dummy buildings, but I can't do it as it can spawn anywhere. if it's a feature it will remove forests & floodplains. if it's an improvement (like FFH) it will always remove an improvement (don't want that)...

I need an independent way of running an effect :hmm:
 
Use plot script data. After the effect, replace the feature or improvement whichever u like
 
I'm using C++ :p so if I did that I wouldn't use script data (problems with conflicting script data) I would just add in some weird new field.

problem with the improvement is:
1) can't be spawned on cities, the improvement needs to be omnipresent.
2) would reset the growth timer on towns etc
3) I have an effect, not an improvement art file. It just wouldn't work afaik

problem with the feature is:
1) might cancel worker actions on the tile if the feature disappears (especially important with catapult construction where swordsmen turn forests into catapults, and this costs money).
2) the feature disappears (would look weird to see a forest disappear and reappear later)
3) same art file problems.

a problem with both is the users ability to remove them via world builder, the problem then is how are the storms cleaned up?

The only way I can see (without ANY negatives) is to render the effect but semi-permanently... My current theory involves clouds, if I can figure out how they are rendered I might be able to utilise that code... but I don't know how. There has to be some way of making an effect loop round. just where.
 
I played with it a little, and I have a possible solution: Create a thread which re-triggers the effect for as long as you need it, at intervals you tell it.

I did some testing, and came up with this code:

General:
Code:
static bool s_bLoopEffect = false;
struct PermanentEffectData
{
	EffectTypes eEffect;
	CvPlot* pPlot;
	float intervalSecs;
};

DWORD WINAPI loopEffect(void* pData)
{
	PermanentEffectData* pEffectData = (PermanentEffectData*)pData;
	while (s_bLoopEffect)
	{
		gDLL->getEngineIFace()->TriggerEffect(pEffectData->eEffect, pEffectData->pPlot->getPoint());
		Sleep(long(pEffectData->intervalSecs * 1000));
	}
	return 0;
}

Start the effect:
Code:
	s_bLoopEffect = true;
	static PermanentEffectData permEffectData;
	permEffectData.eEffect = (EffectTypes)GC.getInfoTypeForString("EFFECT_GODS_TSUNAMI");
	permEffectData.pPlot = GC.getMapINLINE().plotSorenINLINE(18,11);
	permEffectData.intervalSecs = 30.0f;
	CreateThread(NULL, 0, loopEffect, &permEffectData, 0, NULL);

Stop the effect:
Code:
s_bLoopEffect = false;

Now, this is only a demo. Note the bad use of statics here.
You'll probably need to expand it to have a permanent thread with effects list, and to have the intervals as part of the xml etc.
This won't be as simple as here but this is the basic idea.

I tried it and it works, but note that the effect doesn't stop immediately when you stop it, it just won't re-trigger the next time.

Let me know if you need more help with this. I don't have a lot of time, but it's a pretty interesting mechanism.
 
I thought about something like this myself actually, just never occurred to me to use threads :) I will see about implementing it when I have time to focus later in the week :D thanks! For now, I don't think I need a bigger framework, as this is the only effect I ever deal with, but if I end up adding other natural events, like sandstorms or something, I will have to expand the thread. Question: what would the performance hit be for running this thread? I'm guessing due to it's thread nature none on the game itself.
 
aaaactually, there is a problem that I think is present with this, storms do not exist for the same number of turns, also there are some dying and being born every turn. a static Boolean will not cause much flexibility to ALL the storms, surely?

in other words, I could stop the effect at the beginning of each turn, storms die and are born, restart the effect, but then won't that mean the previous threads for now dead storms are still running? I would need to kill some threads... and now it gets complicated... I think I will need your help with this :p
 
Of course one boolean is not enough. You'll need a boolean for each instance of an effect, and not for each effect. If the storm appears in 3 different plots at the same time, you'll need 3 different booleans.

The thread in the code I posted will exit once you stop the effect.

You have two main design options:
1. Opening a thread for each permanent effect instance.
2. Creating a single thread to handle all the permanent effects.

In either case, you'll need a place to hold the running permanent effects.
In option 1 each plot can hold its own permanent effect instance, whereas in option 2 you'll need a central place for all instances.
 
well I couple of ideas, what do you think.

every plot has a m_bIsStorm Boolean already, this could be used to terminate the effect, by testing if the plot from the struct (although I have other plans for that :p) hasStorm().

there is one thread, and it is stored in CvGame (though not saved, no need, just reinitialise it on next game turn). every turn it is destroyed and it is recreated but with a vector of new LoopableEffect derived classes, so we have

StormEffect : LoopableEffect

then we can easily have more for other types

BlizzardEffect : LoopableEffect

std::vector <LoopableEffect *> is passed into the method, iterated through and run, happy days.

what do you think of that for a basis?

edit: actually instead of classes, we can use inherited structs with the same method, allowing all effects to be passed through and handled... I forgot structs can inherit :p
 
There's no need to destroy and create the thread every turn. You do have to consider saving and loading the game while the permanent effect is on (saved game should contain the data for the permanent effect, and you should play it when you load the game).

Using m_bIsStorm will limit you to displaying storms only.

I don't see a need here for inheritance. The behavior of the loopable effects is not different, just their data (which effect and trigger interval).

Structs and classes in C++ are identical besides their default field permission (public for structs, private for classes).

How about this design:

There is a single thread, which as you suggested is held in CvGame (You can hold the Thread ID there).
Each plot holds a struct with its current effect data (similar to the code I posted):

Code:
struct PermanentEffectData
{
	EffectTypes eEffect;
	float intervalSecs;
	bool isActive;
};

CvGame holds a vector of CvPlot* for plots with active effects. Access to it must be protected by a mutex (A mutex protects the data so only a single thread can access it at a time - see this example).

When I have the time (probably not this week) I can write some code for this. Possibly a new mod comp (haven't done these in ages... ;) )
 
when I meant m_bIsStorm I mean it is a CvPlot member, so iterating over every plot in the array will identify if the effect (and hence its place in the array) needs to be terminated. And as far as I can see saving isn't much of an issue because it will be reinitialised on the new game turn, surely it saves some trouble? oh and mutex? I'm guessing that's C++'s equivalent of Java's synchronized? I do love multithreading :)

Question, why store the Plots with effects in an array when we could create one every turn from the code that manages storms? see here:

Spoiler :
Code:
/*Jamie's Rome Mod 1.3/2.1 code - Start*/
void CvGame::doCustomCode()
{
	//Custom code constants
	int iChanceNewStormSeaTimes100 = GC.getDefineINT("STORM_CHANCE_SEA");
	int iChanceNewStormLandTimes100 = GC.getDefineINT("STORM_CHANCE_LAND");
	int iMaxDuration = GC.getDefineINT("STORM_MAX_DURATION");
	int iMinDuration = GC.getDefineINT("STORM_MIN_DURATION");
	int iMaxStorms = GC.getDefineINT("STORM_MAX_PRESENT");
	int iMaxChecks = GC.getDefineINT("STORM_MAX_CHECKS");

	/* Experimental Code - this code is designed to be more randomly representative within the population*/
	int i = 0;
	while (i <= iMaxChecks /*Only try this for iMaxChecks amount of times!!! */&& GC.getMapINLINE().getNumStorms() <= (iMaxStorms - getSorenRandNum(iMaxStorms, "STORM: Should we decrease the max number this turn?")))
	{
		CvPlot * pPlot = GC.getMapINLINE().plotByIndex(getSorenRandNum(GC.getMapINLINE().numPlots(), "STORM: Pick a plot"));
		if (pPlot->isWater() && getSorenRandNum(1000000, "STORM: Spawning Sea Storm?") < iChanceNewStormSeaTimes100 && pPlot->getArea() != GC.getMap().plot(87, 51)->getArea())
		{
			if (!pPlot->hasStorm())
			{ 
				GC.getMap().changeNumStorms(1);
				pPlot->setHasStorm(true);
				gDLL->getInterfaceIFace()->addMessage(pPlot->getOwner(), true, GC.getEVENT_MESSAGE_TIME(), gDLL->getText("TXT_KEY_STORM_STRUCK_US"),"AS2D_STORM_STUCK", MESSAGE_TYPE_MINOR_EVENT, ARTFILEMGR.getInterfaceArtInfo("STORM_BUTTON")->getPath(), (ColorTypes) GC.getInfoTypeForString("COLOR_STORM_TEXT"), pPlot->getX_INLINE(), pPlot->getY_INLINE(), true, true);
			}
			pPlot->setStormTime(std::max(iMinDuration, getSorenRandNum(iMaxDuration + 1, "STORM: How long?")));
		}
		else
		{
			if (getSorenRandNum(1000000, "STORM: Spawning Land Storm?") < iChanceNewStormLandTimes100)
			{
				if (!pPlot->hasStorm())
				{ 
					GC.getMap().changeNumStorms(1);
					pPlot->setHasStorm(true);
					gDLL->getInterfaceIFace()->addMessage(pPlot->getOwner(), true, GC.getEVENT_MESSAGE_TIME(), gDLL->getText("TXT_KEY_STORM_STRUCK_US"),"AS2D_STORM_STUCK", MESSAGE_TYPE_MINOR_EVENT, ARTFILEMGR.getInterfaceArtInfo("STORM_BUTTON")->getPath(), (ColorTypes) GC.getInfoTypeForString("COLOR_STORM_TEXT"), pPlot->getX_INLINE(), pPlot->getY_INLINE(), true, true);
				}
				pPlot->setStormTime(std::max(iMinDuration, getSorenRandNum(iMaxDuration + 1, "STORM: How long?")));
			}
		}
		i++;
	}
	/* Experimental Code */

	
	for (int i = 0; i<GC.getMapINLINE().numPlots(); i++)
	{
		CvPlot * pPlot = GC.getMapINLINE().plotByIndex(i);
		
		/*Jamie's Rome Mod 1.3/2.1 Storms code - Start*/
		/*
		if (GC.getMapINLINE().getNumStorms() < GC.getDefineINT("STORM_MAX_PRESENT"))
		{
			if (pPlot->isWater() && getSorenRandNum(1000000, "STORM: Spawning Sea Storm?") < iChanceNewStormSeaTimes100 && pPlot->getArea() != GC.getMap().plot(87, 51)->getArea())
			{
				if (!pPlot->hasStorm())
				{ 
					GC.getMap().changeNumStorms(1);
					pPlot->setHasStorm(true);
					gDLL->getInterfaceIFace()->addMessage(pPlot->getOwner(), true, GC.getEVENT_MESSAGE_TIME(), gDLL->getText("TXT_KEY_STORM_STRUCK_US"),"AS2D_STORM_STUCK", MESSAGE_TYPE_MINOR_EVENT, ARTFILEMGR.getInterfaceArtInfo("STORM_BUTTON")->getPath(), (ColorTypes) GC.getInfoTypeForString("COLOR_STORM_TEXT"), pPlot->getX_INLINE(), pPlot->getY_INLINE(), true, true);
				}
				pPlot->setStormTime(std::max(iMinDuration, getSorenRandNum(iMaxDuration + 1, "STORM: How long?")));
			}
			else
			{
				if (getSorenRandNum(1000000, "STORM: Spawning Land Storm?") < iChanceNewStormLandTimes100)
				{
					if (!pPlot->hasStorm())
					{ 
						GC.getMap().changeNumStorms(1);
						pPlot->setHasStorm(true);
						gDLL->getInterfaceIFace()->addMessage(pPlot->getOwner(), true, GC.getEVENT_MESSAGE_TIME(), gDLL->getText("TXT_KEY_STORM_STRUCK_US"),"AS2D_STORM_STUCK", MESSAGE_TYPE_MINOR_EVENT, ARTFILEMGR.getInterfaceArtInfo("STORM_BUTTON")->getPath(), (ColorTypes) GC.getInfoTypeForString("COLOR_STORM_TEXT"), pPlot->getX_INLINE(), pPlot->getY_INLINE(), true, true);
					}
					pPlot->setStormTime(std::max(iMinDuration, getSorenRandNum(iMaxDuration + 1, "STORM: How long?")));
				}
			}
		}
		*/
		doStormManage(pPlot);
		/*Jamie's Rome Mod 1.3/2.1 Storms code - End*/

		/*Jamie's Rome Mod 1.3/2.1 Watchtower code - Start*/
		//doWatchtowerManage(pPlot);
		/*Jamie's Rome Mod 1.3/2.1 Watchtower code - Start*/
	}
}
/*Jamie's Rome Mod 1.3/2.1 code - End*/

/*Jamie's Rome Mod 1.3/2.1 Storms code - Start*/
void CvGame::doStormManage(CvPlot * pPlot)
{
	if (pPlot->hasStorm())
	{
		if (pPlot->getStormTime() == 0)
		{
			//deinit storm
			pPlot->setHasStorm(false);
			GC.getMap().changeNumStorms(-1);
			return;
		}
		int iPercentPopulationLoss = GC.getDefineINT("STORM_POPULATION_LOSS_PERCENT");
		int iChanceDestroyImprovement = GC.getDefineINT("STORM_IMPROVEMENT_DESTROY_CHANCE");

		doStormEffect(pPlot);
		pPlot->changeStormTime(-1);
		//do some damage!
		ImprovementTypes eImprovement = pPlot->getImprovementType();
		if (pPlot->isCity())
		{
			CvCity * pCity = pPlot->getPlotCity();
			int iPopulation = (pCity->getPopulation()*iPercentPopulationLoss/100.0)+0.5;
			pCity->changePopulation(-iPopulation);
		}
		else if (eImprovement != NO_IMPROVEMENT && getSorenRandNum(100, "STORM: Destroying Improvement?") < iChanceDestroyImprovement)
		{
			if (pPlot->getImprovementType() == (ImprovementTypes) GC.getInfoTypeForString("IMPROVEMENT_HAMLET"))
			{
				pPlot->setImprovementType((ImprovementTypes) GC.getInfoTypeForString("IMPROVEMENT_COTTAGE"));
			}
			else if (pPlot->getImprovementType() == (ImprovementTypes) GC.getInfoTypeForString("IMPROVEMENT_VILLAGE"))
			{
				pPlot->setImprovementType((ImprovementTypes) GC.getInfoTypeForString("IMPROVEMENT_HAMLET"));
			}
			else if (pPlot->getImprovementType() == (ImprovementTypes) GC.getInfoTypeForString("IMPROVEMENT_TOWN"))
			{
				pPlot->setImprovementType((ImprovementTypes) GC.getInfoTypeForString("IMPROVEMENT_VILLAGE"));
			}
			else
			{
				pPlot->setImprovementType(NO_IMPROVEMENT);
			}
			gDLL->getInterfaceIFace()->addMessage(pPlot->getOwner(), true, GC.getEVENT_MESSAGE_TIME(), gDLL->getText("TXT_KEY_STORM_STRUCK_OUR_IMPROVEMENT", GC.getImprovementInfo(eImprovement).getDescription()),"AS2D_STORM_STUCK", MESSAGE_TYPE_MINOR_EVENT, ARTFILEMGR.getInterfaceArtInfo("STORM_BUTTON")->getPath(), (ColorTypes) GC.getInfoTypeForString("COLOR_STORM_TEXT"), pPlot->getX_INLINE(), pPlot->getY_INLINE(), true, true);
		}
		for (int i = 0; i < pPlot->getNumUnits(); i++)
		{
			int iDamage = getSorenRandNum(100, "STORM: How much damage to unit?");
			CvUnit * pUnit = pPlot->getUnitByIndex(i);
			pUnit->setDamage(std::max(iDamage, std::min(pUnit->getDamage() + iDamage, 100)));
			gDLL->getInterfaceIFace()->addMessage(pUnit->getOwner(), true, GC.getEVENT_MESSAGE_TIME(), gDLL->getText("TXT_KEY_STORM_STRUCK_OUR_UNIT", pUnit->getNameKey()),"AS2D_STORM_STUCK", MESSAGE_TYPE_MINOR_EVENT, ARTFILEMGR.getInterfaceArtInfo("STORM_BUTTON")->getPath(), (ColorTypes) GC.getInfoTypeForString("COLOR_STORM_TEXT"), pPlot->getX_INLINE(), pPlot->getY_INLINE(), true, true);
		}
	}
	return;
}

void CvGame::doStormEffect(CvPlot * pPlot)
{
	gDLL->getEngineIFace()->TriggerEffect((EffectTypes) GC.getInfoTypeForString("EFFECT_STORM"), pPlot->getPoint());
}
/*Jamie's Rome Mod 1.3/2.1 Storms code - End*/

make the manageStorm return the Plot and add it to an array (or rather vectors, I dislike the arrays "stiffness" after years of pythons lists)... etc etc.

edit: oh wait ignore me, the array would mean the thread always has access to fresh plots, allowing it to be left alone. Elegant, I like it :D sounds good :) and yeah, it's about time a new asaf produce is released ;)

also quick question: In the above code, is there anyway to turn the if statements in the StormManage (the damage part) into a switch statement? It appears that the DLL doesn't like switch statements with the ImprovementTypes...
 
Actually Mutex is the general name for it (MUTual EXclusive), and Java synchronized is probably implemented using a mutex.

I did suggest a vector and not an array - because this is a list of plots which will be changed - so a vector is more appropriate.

You can't use switch for ImprovementTypes because the values for this enum are loaded from the xml file at runtime, and are not known at compile time.

I also see you hard-coded the transitions between town,village,etc. instead of using the XML values. I suggest changing that - what would happen if you edited the hamlet to skip to town? Or added another layer in between?
 
I doubt I would, but just in case others want to rip this code, how would I go about the implementation of dynamic reduction?
 
dynamic reducing of improvements, so basically non hard-coded downgrading
 
Some mods have an item in the improvement XML that lists the lower level improvement you get if it is pillaged.

Instead of adding a new tag like that you could loop over all the improvements and see if there is one that grows into the one being pillaged. If so, change it to that one instead of just removing it.
 
Back
Top Bottom