Thunderbrd
C2C War Dog
This post is to provide modders with an overview of the current support for multi-maps and viewports.
Overview
Firstly a brief recap of the traditional, vanilla Civ IV structure:
Each game state (either from a save, or generated when starting a new game) includes a single map, represented by the CvMap class (CyMap in Python). This class has properties of x and y size, and how the coordinates wrap (so plane, vs torus vs sphere topologically). It also allows you to find the details of any particular plot given its coordinates (returns a CvPlot or CyPlot)
From the perspective of the game engine, over which we have no control, the above model must be what the game engine sees. Hence, to change what gets displayed all we can do is vary the CONTENTS of the single map that is presented to the game engine (i.e. - what's in the plots). To achieve this we introduce a proxy class which presents the same external interface as CvMap, but pulls its data from different places (maybe different underlying maps, maybe different regions of a single map). This proxy is what is known as a viewport (CvViewport) [actually the class hierarchy is more complex than this, but I'll go into fuller detail later]. To switch maps we just change the CvMap the viewport pulls its data from and tell the game engine that all plots are dirty, so it requeries everything. Python does not see the viewport directly, since currently it always operates on the complete map (unlike the game engine it has to process game state, not just passively display things, so it has to see everything). For that reason Python access to the map bypasses the viewport and always accesses the underlying map. There are some exposed functions Python can use however, to determine whether a particular plot is in the current viewport that the game engine is seeing.
This system is most developed to support user-visible viewports as windows into a single larger map, but it's fundamentally exactly the same as viewports onto multiple maps. In current C2C it is always active, even if you have (user level) viewports turned off. In that default case the viewport simply has the same dimensions as the entire map, so the fact that the game engine is interfacing via the viewport has no functional effect.
C2C also supports (to some extent) multiple actual maps. The globals (CvInternalGlobals) holds an array of maps (instead of the single one that vanilla BTS holds). At any time any one of these can be the 'current' map (and for now map index 0 is always current until we add UI and Python to switch). The current map can be switched using cvInternalGlobals::switchMap() to any in the allocated array. Currently, when processing end turn, only the current map will be processed (so in effect the turn only takes place on the current map) - this is one of the things that will have to change (it will need to iterate through all the maps playing the [AI] turn in each).
At game start map 0 is automatically generated. At any other time new maps (with indexes > 0) may be generated by Python calls. Such map generation just creates a new CvMap instance - it does not actually generate its plots at the time. The first time a map switch is made to a new map its plots will be generated. Currently this just uses the default map generator, and there is no control over map script used (this needs to change - see below)
We also inherited (from Lytning's parallel maps prototype) some map-definition XML, though this is essentially unused by C2C. It is accessible via Python. It consists of two files:
CvMapInfo.xml, which defines a set of maps; and
CvMapSwitchInfo.xml which defines mechanisms for switching between maps
Both of these are essentially unused collections of properties. Lytning's intent was to access these from Python, and based on what they contain, to call the other Python APIs that actually manipulate and switch between multimaps, to generate (and use) the necessary resulting maps.
My recommendation is to do away entirely with CvMapSwitchInfo (map switching is better handled as implicit mechanisms in the DLL and UI elements via the Python IMO). I would also repurpose (and enhance) CvMapInfo so that it defines the full set of maps available (including scripts etc.). In effect that means that it predefines a set of types (MapTypes, which is an enum we already have) which are valid indexes to use when calling functions like SetCurrentMap(). The DLL needs to be enhanced to read the appropriate map script (and other properties like size and wrapping) when it needs to generate the map on demand the first time it is switched to (that way we don't take the memory hit until the map is needed). The code that does this (and will need to be enhanced to read CvMapInfo and act accordingly) is in CvMap::afterSwitch().
Detailed reference material
In detail the actual class hierarchy used for maps is as follows:
CvMapInterfaceBase - abstract class that defines the external interface a map needs to provide
CvMap - underlying concrete map - a subclass of CvMapInterfaceBase
CvViewport - a window onto an underlying map - a subclass of CvMapInterafceBase. This has a reference to a CvMap instance which is the udnerlying map - the viewport size may be any size less than or equal to the underlying map size
CvMapExternal - class the game engine links to. This is just a shell that forwards all requests to an associated CvMpInterfaceBase instance, which will be the current viewport on the current map
Python methods are currently exposed on CyMap, and are as follows:
Map type (so this will correspond with the type id in CvMapInfo.xml if we follow my suggestion above - currently it's just an array index in the global map array which is pretty meaningless to Python). If we do this then the setType() method should be removed from the python, since it will only be legitimate to set the type at map creation type.
Code:int getType(); void setType(int iNewType);
Viewports:
Code:bool viewportsEnabled(); int getViewportWidth(); int getViewportHeight(); int getMapXFromViewportX(int iX); int getMapYFromViewportY(int iY); int getViewportXFromMapX(int iX); int getViewportYFromMapY(int iY); bool isInViewport(int X, int Y);
Viewport manipulation (don't mess with closeAdvsor unless you know what you're doing - it's used to switch back to a normal view after being in the military advisor):
Code:void closeAdvisor(int advisorWidth, int iMinimapLeft, int iMinimapRight, int iMinimapTop, int iMinimapBottom); void bringIntoView(int iX, int iY, bool bDisplayCityScreen);
Map switching:
Code:void CyGlobalContext::switchMap(int iMap); int CyGlobalContext::getNumMapInfos() const; CvMapInfo* CyGlobalContext::getMapInfo(int iMap) const; int CyGlobalContext::getNumMapSwitchInfos() const; CvMapSwitchInfo* CyGlobalContext::getMapSwitchInfo(int iMapSwitch) const; CyMap* CyGlobalContext::getMapByIndex(int iIndex); void CyGlobalContext::initializeMap(int iMap);
These are only partially hooked up currently, and if we follow my proposal of getting rid of CvMapSwitchInfo.xml and using CvMapInfo.xml to define all the legitimate maps (MAP_EARTH, MAP_MOON, ...) then getMapByIndex() and getMapSwitchInfo() will disappear. InitializeMap() is also not needed in the Python, since we'll just have the DLL initialize all maps at startup (note - initializing a map is not the same as generating it, which happens on demand when it is first switched to)
Koshling, this is so amazingly helpful its unreal! I have only skimmed at this point due to some other more pressing matters to attend to at the moment but I wanted to start a thread based on your post so that we don't fill up the Modder's Documentation thread because I can already see I'm going to have some more questions and we have some work to do as a group to plan out where to go from here. But all in all I believe with this one post you may have just enabled me to, with patience, carry this out to a point of further completion. I have a lot of very pressing goals for this version but at the point in time I'm ready to move forward here what you've done to set us up is just... awesome!
Yes, like yourself, some help in designing the map generation routines themselves may be the biggest gap I'd want some assistance with. These would have to be considered one map TYPE at a time. Anyhow, I'll comment more as soon as I can devour this explanation in much greater depth but again, I think I speak for the whole team when I say, thank you!