Attention all C++ and Python Programmers: Need Help Rebuilding Map Graphics

LyTning94

Dragonborn
Joined
Nov 10, 2010
Messages
397
Location
Skyrim
I'm trying to add a feature to the game allowing you to switch between two or more CvMap objects in-game. This would enable mods to have multiple maps, each with different cities and units, which you could switch between periodically and manage both maps in the same game. If I can get this to work, it could be useful in many different kinds of mods.

I've already switched the map object succesfully, but I'm running into problems with the graphics. When I switch to a map of the same size, the graphics engine loads the new map without a problem, but when I try a different sized map it appears as a haphazard combination of the two maps.

Does anyone have any idea what I might need to call (from the SDK or python) which would rebuild the whole map's graphics from scratch, allowing a different sized map? I'm currently using only the SDK to do this, but since WB maps are loaded in python, I'm wondering if there's something there that could help me.

Any knowledge, or guesses, on how I might go about this would be greatly appreciated!
 
The first thing loading a WB save actually applies is the staring year. Probably not vital.
After that...

When a WB save loads, the map is initially created via
Code:
		worldSizeType = CvUtil.findInfoTypeNum(gc.getWorldInfo, gc.getNumWorldInfos(), self.mapDesc.worldSize)
		climateType = CvUtil.findInfoTypeNum(gc.getClimateInfo, gc.getNumClimateInfos(), self.mapDesc.climate)
		seaLevelType = CvUtil.findInfoTypeNum(gc.getSeaLevelInfo, gc.getNumSeaLevelInfos(), self.mapDesc.seaLevel)
		CyMap().rebuild(self.mapDesc.iGridW, self.mapDesc.iGridH, self.mapDesc.iTopLatitude, self.mapDesc.iBottomLatitude, self.mapDesc.bWrapX, self.mapDesc.bWrapY, WorldSizeTypes(worldSizeType), ClimateTypes(climateType), SeaLevelTypes(seaLevelType), 0, None)
So it passes both the grid size in the X and Y directions and the world size type value, as well as a bunch of other stuff.

The function CyMap::rebuild is defined in CyMap.cpp and is a pretty direct reproduction of that code:
Code:
void CyMap::rebuild(int iGridW, int iGridH, int iTopLatitude, int iBottomLatitude, bool bWrapX, bool bWrapY, WorldSizeTypes eWorldSize, ClimateTypes eClimate, SeaLevelTypes eSeaLevel, int iNumCustomMapOptions, CustomMapOptionTypes * aeCustomMapOptions)
{
	if (m_pMap)
	{
		m_pMap->rebuild(iGridW, iGridH, iTopLatitude, iBottomLatitude, bWrapX, bWrapY, eWorldSize, eClimate, eSeaLevel, iNumCustomMapOptions, aeCustomMapOptions);
	}
}

Where m_pMap is set to be "m_pMap = &GC.getMapINLINE();".

It then sets the plot type and terrain type for each plot.
Then calls "CyMap().recalculateAreas()".
Applies the rest of the plot data, plot by plot.
and so on

This is done in code in Python\pyWB\CvWBDesc.py in the applyMap function of the CvWBDesc class (starting on line 1559 in the unmoded BtS file).
 
I've already done all this in the SDK. It rebuilds the plots fine if the map is the same size, but a different sized map messes things up because the graphics engine only rebuilds the plots, not the entire map. I'm looking for something that will essentially reload the entire map's graphics.
 
In addition to the CvMap::rebuild function there is the CvMap::reset function (which is called by init which is called by rebuild). According to the code CvMap::reset only resizes the map if the argument is present with the relevant size data, otherwise it just uses the existing size based on what GC.getInitCore().getWorldSize() says it is (or what the map script's override of the size says it is).

Note that the size passed in the CvMapInitData for these various functions is in plots, not in the 4x4 "terrain cell units".
 
In addition to the CvMap::rebuild function there is the CvMap::reset function (which is called by init which is called by rebuild). According to the code CvMap::reset only resizes the map if the argument is present with the relevant size data, otherwise it just uses the existing size based on what GC.getInitCore().getWorldSize() says it is (or what the map script's override of the size says it is).

Note that the size passed in the CvMapInitData for these various functions is in plots, not in the 4x4 "terrain cell units".
The problem is that the graphics engine is not informed that you used that function and it does not do any API calls to inform it.
 
If the graphics engine is not told about it, how does it display the first map to begin with? This is the same stuff that is called to create the map in the first place. Somewhere in there (or done by something after that stage in the startup) some data, including the map size, must make its way back to the .exe or no map would ever get shown.

On the other hand, it would not surprise me too much if it were not designed to change the map size after it is set up once. It may not be designed to reallocate/reinitialize the required memory for all the various data elements, for example. It may only be able to throw it all out as done when exiting back to the main menu (probably essentially shutting down the game engine part of it entirely, wiping out the stored data in preparation to either exit or restart). I would find that a bit odd since it is based on a somewhat generic gaming engine that is useable for games that have multiple levels with different maps of different sizes - but that functionality may not be exposed to the DLL (or even used at all by Civ4).

I guess that might be something to check. What does it do when you exit back to the main menu? Is it visible to the DLL, and if so it is just one call or multiple things that could be used to let you just clear the map state in preparation to reinitialize the map?
 
If the graphics engine is not told about it, how does it display the first map to begin with? This is the same stuff that is called to create the map in the first place. Somewhere in there (or done by something after that stage in the startup) some data, including the map size, must make its way back to the .exe or no map would ever get shown.

Right. Somewhere the graphics engine has to build itself using the map width, height, wrapping, etc. This isn't done in CvMap::rebuild(), though (or any function called by it). Since the data is saved in the CvMap object, it would be easy to access from any function in the .exe, and it's this function that I'm trying to find.

On the other hand, it would not surprise me too much if it were not designed to change the map size after it is set up once. It may not be designed to reallocate/reinitialize the required memory for all the various data elements, for example. It may only be able to throw it all out as done when exiting back to the main menu (probably essentially shutting down the game engine part of it entirely, wiping out the stored data in preparation to either exit or restart). I would find that a bit odd since it is based on a somewhat generic gaming engine that is useable for games that have multiple levels with different maps of different sizes - but that functionality may not be exposed to the DLL (or even used at all by Civ4).

I guess that might be something to check. What does it do when you exit back to the main menu? Is it visible to the DLL, and if so it is just one call or multiple things that could be used to let you just clear the map state in preparation to reinitialize the map?

Exiting to the main menu and loading a new game are done with gDLL->getInterfaceIFace()->exitingToMainMenu() and gDLL->loadGame(). Both functions are in the .exe, so we can't really tell what they do to the graphics engine when they're called.

It seems to me that whenever a new WB map is loaded, via CvWBInterface, there would have to be a function call that would accomplish what I'm looking for. I don't have Civ4 installed on my new computer yet so I don't have access to the python files at the moment, but I'm hoping it's done somewhere there versus in the .exe, so I can find the necessary function.
 
It seems to me that whenever a new WB map is loaded, via CvWBInterface, there would have to be a function call that would accomplish what I'm looking for. I don't have Civ4 installed on my new computer yet so I don't have access to the python files at the moment, but I'm hoping it's done somewhere there versus in the .exe, so I can find the necessary function.
The Python code only places the button widget, but unfortunately it seems that the click on it is handled by the exe. So the exe has control over it and knows that the map size might have changed now and therefore will call the necessary functions of the graphics engine.
 
The Python code only places the button widget, but unfortunately it seems that the click on it is handled by the exe. So the exe has control over it and knows that the map size might have changed now and therefore will call the necessary functions of the graphics engine.

Other mods (this one for example) load new WB saves directly from python, so it must be possible somehow. I still think there might be a python function that needs called after switching the maps. As soon as I get Civ4 installed I'll be able to look into it further.
 
Other mods (this one for example) load new WB saves directly from python, so it must be possible somehow. I still think there might be a python function that needs called after switching the maps. As soon as I get Civ4 installed I'll be able to look into it further.
Do you know if they load WB saves that have a different size that way into the game?
Because as far as I can see the only important function called in there is the one God-Emperor already pointed out: CvMap rebuild.
 
Do you know if they load WB saves that have a different size that way into the game?
Because as far as I can see the only important function called in there is the one God-Emperor already pointed out: CvMap rebuild.

No, I guess I don't know for sure. I asked over in the thread I linked earlier, so hopefully someone will confirm either way. And I know there aren't any other important calls in applyMap() or applyInitialItems(); I thought they might have called another function, after these two, which would accomplish what I need.
 
I am only aware of two other mods which switch maps in-game using any method (the Dungeon Aventure FfH modmod and Civ4 Panzer General). I've asked both mod designers, and neither was able to get a different sized map working. So, at this point, I'm ready to give it up as impossible. I'll still be able to switch to maps of the same size, but different sizes will have to be implemented by impassible terrain or some other hack. If anyone has a better idea, let me know.
 
I am only aware of two other mods which switch maps in-game using any method (the Dungeon Aventure FfH modmod and Civ4 Panzer General). I've asked both mod designers, and neither was able to get a different sized map working. So, at this point, I'm ready to give it up as impossible. I'll still be able to switch to maps of the same size, but different sizes will have to be implemented by impassible terrain or some other hack. If anyone has a better idea, let me know.
Well, one hack I can think of would be fake loading. Use a very small savegame and order Civ to load it. Before that switch your map and set a global bool to true.
Add an if to all DLL methods that change the gamestate and are called during game loading and just return/do nothing if the global bool is true.
Annoying to implement but it might work.

Still, it is a hack so I think it is better to do the switching as it is now and add impassable terrain around the battlefield.
 
Still, it is a hack so I think it is better to do the switching as it is now and add impassable terrain around the battlefield.

I agree.
 
Ummm, there's CvDLLEngineIFaceBase::InitGraphics().
I don't see it used anywhere in the DLL, but maybe it does what you want?

Code:
gDLL->getEngineIFace()->InitGraphics();
 
Ummm, there's CvDLLEngineIFaceBase::InitGraphics().
I don't see it used anywhere in the DLL, but maybe it does what you want?

Code:
gDLL->getEngineIFace()->InitGraphics();

Yeah, I already tried that a while back. It caused a crash somewhere down the line in the exe, probably because it was rebuilding the graphical engine which already existed.
 
Oh well. worth a shot.
Maybe there's a 'shutdown' function which can be called before the init?

And can you post your existing code? it might allow some experimentation.
 
No, unfortunately there doesn't seem to be a function that destroys the engine. Here's my final code:

Code:
void CvGlobals::swapMaps()
{
	int i, iLoop;
	CvUnit* pLoopUnit;
	CvCity* pLoopCity;
	
	for (i = 0; i < MAX_TEAMS; i++)
	{
		GC.getMapINLINE().setRevealedPlots(((TeamTypes)i), false);
	}
	
	for (i = 0; i < MAX_PLAYERS; i++)			// destroying all the unit/city entities currently on the map
	{
		if (GET_PLAYER((PlayerTypes)i).isAlive())
		{
			for (pLoopCity = GET_PLAYER((PlayerTypes)i).firstCity(&iLoop); pLoopCity != NULL; pLoopCity = GET_PLAYER((PlayerTypes)i).nextCity(&iLoop))
			{
				pLoopCity->removeEntity();
				pLoopCity->destroyEntity();
			}
			
			for (pLoopUnit = GET_PLAYER((PlayerTypes)i).firstUnit(&iLoop); pLoopUnit != NULL; pLoopUnit = GET_PLAYER((PlayerTypes)i).nextUnit(&iLoop))
			{
				if (gDLL->getEntityIFace()->IsSelected(pLoopUnit->getEntity()))
				{
					gDLL->getInterfaceIFace()->selectUnit(pLoopUnit, true, true);
				}
				
				if (GC.IsGraphicsInitialized())
				{
					gDLL->getEntityIFace()->RemoveUnitFromBattle(pLoopUnit);
					pLoopUnit->removeEntity();
				}
				pLoopUnit->destroyEntity();
			}
			
			GET_PLAYER((PlayerTypes)i).swapInfo();			// function to swap the player's units' and cities' list with another list for the alternate map
		}
	}

	gDLL->getInterfaceIFace()->setInterfaceMode(INTERFACEMODE_GLOBELAYER_INPUT);

	for (i = 0; i < GC.getMap().numPlots(); i++)
	{
		gDLL->callUpdater();
		GC.getMap().plotByIndex(i)->destroyGraphics();			// this function included below.
		gDLL->getEngineIFace()->MarkPlotTextureAsDirty(GC.getMap().plotByIndex(i)->getX(), GC.getMap().plotByIndex(i)->getY());
	}
	
	for (i = 0; i < NUM_AREA_BORDER_LAYERS; i++)
	{
		gDLL->getEngineIFace()->clearAreaBorderPlots((AreaBorderLayers)i);
	}
	gDLL->getEngineIFace()->clearColoredPlots(PLOT_LANDSCAPE_LAYER_ALL);
	gDLL->getEngineIFace()->clearSigns();

	CvMap* tempMap = m_map;			// switching the map object
	m_map = m_altMap;
	m_altMap = tempMap;

	GC.getMapINLINE().rebuild(15, 15, 45, -45, false, false, (WorldSizeTypes)0, (ClimateTypes)0, (SeaLevelTypes)0, 0, NULL);
	for (i = 0; i < GC.getMapINLINE().numPlots(); i++)
	{
		gDLL->callUpdater();
		GC.getMapINLINE().plotByIndex(i)->setTerrainType((TerrainTypes)GC.getInfoTypeForString("TERRAIN_GRASS"), false, false);
		gDLL->getEngineIFace()->MarkPlotTextureAsDirty(GC.getMap().plotByIndex(i)->getX(), GC.getMap().plotByIndex(i)->getY());
		gDLL->getEngineIFace()->RebuildTileArt(GC.getMap().plotByIndex(i)->getX(), GC.getMap().plotByIndex(i)->getY());
	}
	gDLL->getEngineIFace()->RebuildAllPlots();
	GC.getMapINLINE().setupGraphical();

	gDLL->getEngineIFace()->SetDirty(GlobeTexture_DIRTY_BIT, true);
	gDLL->getEngineIFace()->SetDirty(CultureBorders_DIRTY_BIT, true);
	gDLL->getEngineIFace()->SetDirty(GlobePartialTexture_DIRTY_BIT, true);
	gDLL->getEngineIFace()->SetDirty(MinimapTexture_DIRTY_BIT, true);
	for (i = 0; i < NUM_INTERFACE_DIRTY_BITS; i++)
	{
		gDLL->getInterfaceIFace()->setDirty((InterfaceDirtyBits)i, true);
	}
	gDLL->getInterfaceIFace()->makeInterfaceDirty();

	for (i = 0; i < MAX_PLAYERS; i++)				// creating new entities for the units/cities on this map
	{
		if (GET_PLAYER((PlayerTypes)i).isAlive())
		{
			for (pLoopCity = GET_PLAYER((PlayerTypes)i).firstCity(&iLoop); pLoopCity != NULL; pLoopCity = GET_PLAYER((PlayerTypes)i).nextCity(&iLoop))
			{
				gDLL->getEntityIFace()->createCityEntity(pLoopCity);
				pLoopCity->setupGraphical();
			}
			
			for (pLoopUnit = GET_PLAYER((PlayerTypes)i).firstUnit(&iLoop); pLoopUnit != NULL; pLoopUnit = GET_PLAYER((PlayerTypes)i).nextUnit(&iLoop))
			{
				gDLL->getEntityIFace()->createUnitEntity(pLoopUnit);
				pLoopUnit->setupGraphical();
			}
		}
	}
}

void CvPlot::destroyGraphics()				// everything copied from the destructor
{
	gDLL->getFeatureIFace()->destroy(m_pFeatureSymbol);
	if(m_pPlotBuilder)
	{
		gDLL->getPlotBuilderIFace()->destroy(m_pPlotBuilder);
	}
	gDLL->getRouteIFace()->destroy(m_pRouteSymbol);
	gDLL->getRiverIFace()->destroy(m_pRiverSymbol);
	gDLL->getFlagEntityIFace()->destroy(m_pFlagSymbol);
	gDLL->getFlagEntityIFace()->destroy(m_pFlagSymbolOffset);
	m_pCenterUnit = NULL;

	deleteAllSymbols();
}

I really have no idea what out of all of this is necessary, but I'm guessing most of it is not. Toward the end I was just experimenting by adding function after function and hoping it did something that worked. :lol:

I do know that I got a same-sized map to load properly without near this amount of code, but even then I don't know what was necessary. As soon as I get some other problems fixed, I'm going to experiment and figure out what all needs to be called to switch to a same-sized map, and hopefully from there you can figure something out.
 
Just had an idea: I never tried calling gDLL->uninitGlobals(). Perhaps this destroys the engine object, which could then be recreated with gDLL->initGlobals()?
 
Back
Top Bottom