Quick Modding Questions Thread

@The Snug: Never done that, but here's my best bet:
The FeatureGenerator class in (Warlords - not modified by BtS) Assets\Python\CvMapGeneratorUtil.py should take care of most if not all of the standard map scripts. The complex community-created scripts (e.g. PerfectWorld) have their own feature placement code. If you only want to change one particular map script, then I think the script just needs to define a addFeatures function (no call parameters, no return value); some already have one, e.g. Donut. That function should call addFeatures on an instance of CvMapGeneratorUtil.FeatureGenerator and then place any novel features.

Edit: Features with an appearance probability get placed automatically by CvMapGeneratorUtil:
Spoiler :
Code:
for iI in range(self.gc.getNumFeatureInfos()):
    if pPlot.canHaveFeature(iI):
        if self.mapRand.get(10000, "Add Feature PYTHON") < self.gc.getFeatureInfo(iI).getAppearanceProbability():
            pPlot.setFeatureType(iI, -1)

This all looks like Greek to me. Is the 'PYTHON' supposed to be the name of the feature?
 
This all looks like Greek to me. Is the 'PYTHON' supposed to be the name of the feature?
Programing language to write the code in :lol:
Civ4 has some parts written in python (these are easier to access to modders) and some in C+ (or C#? Whatever). Of course XML is the easiest to mod also the most limited.
 
This all looks like Greek to me. Is the 'PYTHON' supposed to be the name of the feature?
The feature gets referenced through its ID (variable iI). The loop runs through all features. To obtain the ID of one particular feature, e.g.
self.gc.getInfoTypeForString("FEATURE_JUNGLE")
is used. (self.gc having been set to CyGlobalContext() in FeatureGenerator.__init__)

The random number generator takes as its second argument a debug string that gets written to Logs\MPLog.txt (if RandLog is enabled in My Games\Beyond the Sword\CivilizationIV.ini). Those log entries are, among other things, useful for debugging synchronization problems in multiplayer games, I guess that's why they get written to the "MP" log. In this case, I think the message is supposed to indicate that a random number was used for (possibly) adding a feature, and that this happened in a Python script.

My takeaway from this code fragment is that all features with a positive appearance probability (at per-myriad precision) set in Civ4FeatureInfos.xml get placed by all map scripts that use the standard FeatureGenerator – which may well be all you need. However, if your feature has placement requirements beyond those configurable in XML, e.g. appearing only at certain latitudes, then iAppearance is no help; will have to write some Python code. Oasis has all its restrictions set in XML, and it gets placed through iAppearance=500, i.e. on an expected 5.00% of the flat noncoastal nonriver desert tiles that aren't already adjacent to an oasis. Jungle can only appear on grassland, the rest is handled explicitly by the FeatureGenerator in Python.

By the way, just noticed a small thing that I'll want to improve: The code I've posted gets executed in this order:
Code:
for iX in range(self.iGridW):
    for iY in range(self.iGridH):
        self.addFeaturesAtPlot(iX, iY)
Since oases can't appear adjacent to each other, this order of execution biases oasis placement toward tiles with low x coordinates. If iAppearance were much higher, oases would appear noticeably more often one tile away from a western coast than from an eastern coast. I'm going to randomize the order of traversal. (The map generation code has these small arbitrary biases in a lot of places; trying to weed that out.) Edit: Had gotten some directions confused in this paragraph.
 
Last edited:
So, I've added a new terrain type, and got the mapscript to place it, the problem is, it's only plotting it in tundra areas. How do I control the latitude ranges for new terrains?
 
@<Nexus>: Someone recently suggested removing the <CitySoundscapes> tags from Civ4EraInfos.xml,
but this causes the era music to restart upon opening a city screen. I'm not aware of any way of letting it continue uninterrupted.
What really interesting is, that one can "bring back" the music by entering the city screen than open and close the pedia while still in the city.
 
Weird indeed. Foreign Advisor (F4) also has that effect. Maybe something that the EXE does upon closing a screen, perhaps tied to WidgetTypes.WIDGET_CLOSE_SCREEN (though closing the screen with Esc also works). Might be possible to create a hack based on that, but, actually, having just tried @Buffalo Solider's approach (the thread I had linked to), it seems that the music does not start over when no city soundscapes exist. Don't know how I got that idea. So this seems to work fine, apart from the change in volume caused by zooming in.
 
Hello team, can you help with something I thought would be quite straightforward and turns out surprisingly difficult to find where to change :

I would like to be able to add a line when you have several units of cargos.
On top of "Cargo Space: 3/3"
I would like to add "Total Cargo Space Selection: 5/15"

upload_2022-2-1_21-18-16.png


I would have assumed I can change it in this function : void CvGameTextMgr::setUnitHelp
Since it's the only place in the whole C++ calling : "TXT_KEY_UNIT_HELP_CARGO_SPACE"

However I noticed that none of the changes I do (there or elsewhere) in ::setUnitHelp are producing any changes.
An ultra obvious test would be to double up the line and it doesn't do anything
Code:
....
    {
        //2.25f end
        szTempBuffer = NEWLINE + gDLL->getText("TXT_KEY_UNIT_HELP_CARGO_SPACE", 5, pUnit->cargoSpace());//just checking twice
        szTempBuffer = NEWLINE + gDLL->getText("TXT_KEY_UNIT_HELP_CARGO_SPACE", pUnit->getCargo(), pUnit->cargoSpace());
    }

I would be very surprised but is this not the location in the code ? Not in the C++ ? I've looked at the widgets and I don't see anything related either
 
It might work if you change the 2nd line from "=" to "+=".
A debugger would generally help with such conundrums, would show whether the assignment is reached and how it affects the szTempBuffer variable. (There are instructions -in bold- at the end of Nightinggale's makefile thread.)

By the way, regarding city sounds, through the DLL, it's easy enough to create a BUG option for that. Just need to return -1 from CvCity::getSoundscapeScriptId when the sounds are disabled.
 
Can't believe it, so silly... excellent spot, it does work of course. Will make the loop for the count properly now!
I will also read the link you sent ! Might help me a ton.

I'll ask another for the road then... :

MIN_WATER_SIZE_FOR_OCEAN

I thought this would be a global define (which is set at 10) where if the "lake" is less than 10 tiles, you cannot build Boats in the lake,
But if the lake is more than 10 you can.

Well it's not I don't see what it changes to change this. Any idea how to change that you can make boats in EVERY city that has access to X amount of water tiles ? (am thinking 2 water tiles).
 
Can't believe it, so silly... excellent spot, it does work of course. Will make the loop for the count properly now!
I will also read the link you sent ! Might help me a ton.

I'll ask another for the road then... :

MIN_WATER_SIZE_FOR_OCEAN

I thought this would be a global define (which is set at 10) where if the "lake" is less than 10 tiles, you cannot build Boats in the lake,
But if the lake is more than 10 you can.

Well it's not I don't see what it changes to change this. Any idea how to change that you can make boats in EVERY city that has access to X amount of water tiles ? (am thinking 2 water tiles).

That's controlled in the xml for unitinfos. Each water unit defines how many water tiles it needs.with the tag: MinAreaSize. If you make this low, though, however, then the AI will build entire navies on lakes.
 
Scenario files (aka CivBeyondSwordWBSave) have this GameInfo block:
Code:
BeginGame
   Calendar=CALENDAR_DEFAULT
   Option=GAMEOPTION_AGGRESSIVE_AI
   Option=GAMEOPTION_NO_TECH_BROKERING
   GameTurn=181
   StartYear=-3000
   Description=TXT_KEY_RHYES_WB_DESC
   ModPath=Mods\RFC Dawn of Civilization
EndGame
I am particularly interested in the "GameTurn" entry. My understanding is that the file is parsed by the pyWB/CvWBDesc.py file. However, it doesn't actually seem to do anything with the parsed GameTurn value:
Code:
class CvGameDesc:
   "class for serializing game data"
   def __init__(self):
       self.eraType = "NONE"
       self.speedType = "NONE"
       self.calendarType = "CALENDAR_DEFAULT"
       self.options = ()
       self.mpOptions = ()
       self.forceControls = ()
       self.victories = ()
       self.gameTurn = 0
       self.maxTurns = 0
       self.maxCityElimination = 0
       self.numAdvancedStartPoints = 0
       self.targetScore = 0
       self.iStartYear = -4000
       self.szDescription = ""
       self.szModPath = ""
       self.iRandom = 0
       
   def apply(self):
       "after reading, apply the game data"
       gc.getGame().setStartYear(self.iStartYear)
       
   def write(self, f):
       "write out game data"
       f.write("BeginGame\n")
       f.write("\tEra=%s\n" %(gc.getEraInfo(gc.getGame().getStartEra()).getType(),))
       f.write("\tSpeed=%s\n" %(gc.getGameSpeedInfo(gc.getGame().getGameSpeedType()).getType(),))
       f.write("\tCalendar=%s\n" %(gc.getCalendarInfo(gc.getGame().getCalendar()).getType(),))
       
       # write options
       for i in range(gc.getNumGameOptionInfos()):
           if (gc.getGame().isOption(i)):
               f.write("\tOption=%s\n" %(gc.getGameOptionInfo(i).getType()))
               
       # write mp options
       for i in range(gc.getNumMPOptionInfos()):
           if (gc.getGame().isMPOption(i)):
               f.write("\tMPOption=%s\n" %(gc.getMPOptionInfo(i).getType()))
               
       # write force controls
       for i in range(gc.getNumForceControlInfos()):
           if (gc.getGame().isForcedControl(i)):
               f.write("\tForceControl=%s\n" %(gc.getForceControlInfo(i).getType()))
               
       # write victories
       for i in range(gc.getNumVictoryInfos()):
           if (gc.getGame().isVictoryValid(i)):
               if (not gc.getVictoryInfo(i).isPermanent()):
                   f.write("\tVictory=%s\n" %(gc.getVictoryInfo(i).getType()))
               
       f.write("\tGameTurn=%d\n" %(gc.getGame().getGameTurn(),))
       f.write("\tMaxTurns=%d\n" %(gc.getGame().getMaxTurns(),))
       f.write("\tMaxCityElimination=%d\n" %(gc.getGame().getMaxCityElimination(),))
       f.write("\tNumAdvancedStartPoints=%d\n" %(gc.getGame().getNumAdvancedStartPoints(),))
       f.write("\tTargetScore=%d\n" %(gc.getGame().getTargetScore(),))
       
       f.write("\tStartYear=%d\n" %(gc.getGame().getStartYear(),))
       f.write("\tDescription=%s\n" % (self.szDescription,))
       f.write("\tModPath=%s\n" % (self.szModPath,))
       f.write("EndGame\n")
       
   def read(self, f):
       "read in game data"
       self.__init__()
       
       parser = CvWBParser()
       if (parser.findNextTokenValue(f, "BeginGame")!=-1):
           while (true):
               nextLine = parser.getNextLine(f)
               toks = parser.getTokens(nextLine)
               if (len(toks)==0):
                   break
                   
               v = parser.findTokenValue(toks, "Era")
               if v!=-1:
                   self.eraType = v
                   continue
                   
               v = parser.findTokenValue(toks, "Speed")
               if v!=-1:
                   self.speedType = v
                   continue

               v = parser.findTokenValue(toks, "Calendar")
               if v!=-1:
                   self.calendarType = v
                   continue

               v = parser.findTokenValue(toks, "Option")
               if v!=-1:
                   self.options = self.options + (v,)
                   continue
                   
               v = parser.findTokenValue(toks, "MPOption")
               if v!=-1:
                   self.mpOptions = self.mpOptions + (v,)
                   continue
                   
               v = parser.findTokenValue(toks, "ForceControl")
               if v!=-1:
                   self.forceControls = self.forceControls + (v,)
                   continue
                   
               v = parser.findTokenValue(toks, "Victory")
               if v!=-1:
                   self.victories = self.victories + (v,)
                   continue
                   
               v = parser.findTokenValue(toks, "GameTurn")
               if v!=-1:
                   self.gameTurn = int(v)
                   continue

               v = parser.findTokenValue(toks, "MaxTurns")
               if v!=-1:
                   self.maxTurns = int(v)
                   continue
                   
               v = parser.findTokenValue(toks, "MaxCityElimination")
               if v!=-1:
                   self.maxCityElimination = int(v)
                   continue

               v = parser.findTokenValue(toks, "NumAdvancedStartPoints")
               if v!=-1:
                   self.numAdvancedStartPoints = int(v)
                   continue

               v = parser.findTokenValue(toks, "TargetScore")
               if v!=-1:
                   self.targetScore = int(v)
                   continue

               v = parser.findTokenValue(toks, "StartYear")
               if v!=-1:
                   self.iStartYear = int(v)
                   continue
                   
               v = parser.findTokenValue(toks, "Description")
               if v!=-1:
                   self.szDescription = v
                   continue
                   
               v = parser.findTokenValue(toks, "ModPath")
               if v!=-1:
                   self.szModPath = v
                   continue

               v = parser.findTokenValue(toks, "Random")
               if v!=-1:
                   self.iRandom = int(v)
                   continue

               if parser.findTokenValue(toks, "EndGame") != -1:
                   break
As you can see, the apply method actually doesn't set most of the values parsed here. But clearly the GameTurn entry works (and I am sure so do the others). When I set it to 181, the game will actually start with CyGlobalContext().getGame().getGameTurn() == 181.

How does that work? Is there another part of the game that also parses the file? Where? Or does it happen in the exe? I am fairly sure it must be because even breakpoints at CvInitCore::setGameTurn do not get triggered with 181. Just checking that I am not missing anything I am unaware of.
 
I think what happens is this:
exe will call getGameData in CvWBInterface.py which will call CvWBDesc.py to parse the scenario file. The gameturn with all other scenario data is passed to the exe via the read function.
The gamedata is then later passed from the exe to the gamecore dll.
 
[...] even breakpoints at CvInitCore::setGameTurn do not get triggered with 181.
The gamedata is then later passed from the exe to the gamecore dll.
So I suppose this last part happens through some form of raw memory access to a blank instance of CvInitCore. Which might explain why changes to the memory layout of the "CORE PLAYER INIT DATA" of CvInitCore result in memory corruptions – despite all constructors being DLL-internal. (Exported constructors are usually the reason for memory corruptions when changing the size of a class in the DLL.)

In the debugger, I see that iGameTurn is still 0 when I select the radio button of the Earth1000AD scenario (breakpoint in setMapScriptName). When I confirm that selection and the civ selection screen comes up (setCiv call assigning me the default civ, China), iGameTurn is 160. Some further rather pointless observations:
Spoiler :
Those setter calls modify CvGlobals::m_initCore, but there are two other global CvInitCore instances: m_loadedInitCore and m_iniInitCore. The latter seems to get modified (presumably with data from CivilizationIV.ini) only when BtS starts up. The "loaded" init core also gets a setCiv call upon loading the civ selection menu, but never seems to learn the map script name. So two instances are involved with the scenario setup screens, but the loaded init core seems to have a lesser role. The reset functions that take a CvInitCore pointer "pSource" seem to get used only for resetting both the regular and the loaded init core to the INI values when returning to the opening menu (i.e. pSource is always the ini init core). Edit: Upon loading a savegame, the regular init core gets reset with the loaded init core as the source pointer. So it looks like the loaded init core is mainly concerned with loading savegames.
 
Last edited:
I think so as well. Since python just passes a python tuple to the exe instead of using getters for all variables, the memory layout for it is hardcoded. They probably did something similar for the gamecore dll.
 
Interesting, thanks for your answers. I was not aware of CvWBInterface.py and its related method, that makes sense. Since in my use case I am using a normal map script which is backed by a CivBeyondSwordWBSave, I guess that is never called. Not that any of the steps skipped are super complex, so I can just replicate them in Python. Just wanted to make sure I am not totally losing the plot here and needlessly writing duplicate code.
 
Hello! I was wondering if anyone knows where I could edit the popup-window size for random events, the thing is I have a mod with event images implemented, it is possible to just size up the event images,
since they are orginally made in 256x256, however for modern screens mostly in 1920x1080 they are too tiny for my taste, but the popup window dimensions need to change in order fit it properly, as you can see
below I can scroll down so it adaps heightswise but not in width:

eventpopupwindow.png


Can I edit the size in some python-file perhaps?
 
The file Assets\python\pyHelper\Popup.py has a function setsize. I suggest to try to modify that.
 
Back
Top Bottom