Quick Modding Questions Thread

@Walter Hawkwood: Yes. Firaxis just inexplicably put that one annoying line in there while leaving all other UI settings as the previously active player had them. Well, I guess the clearEventMessages above is the other troublesome bit. Maybe just not clearing them would already improve matters in that respect.

@<Nexus>: print writes to PythonDbg.log – provided that LoggingEnabled is set in CivilizationIV.ini. Not so convenient, but pretty robust. If you find your message in PythonDbg.log, you could test the conditionals in the loop in the same manner. Otherwise, I guess the key press could be the issue. Actually, I'm not sure if checking for that in CvEventManager.py will work in a BUG-based mod like RoM. I suppose it should. I see the Shift+F1 (with cheats enabled) shortcut in there for accessing the Replay screen (without needing to retire); does that one work in your mod?
 
It seems that the BUG event manager doesn't call the original event manager's onKbdEvent handler. I guess most of the handlers there, or all, are bypassed by the BUG event manager, which is why Platy's mods generally aren't easily portable to BUG-based modpacks – so this really shouldn't surprise me. The clean approach, then, should be to proceed as described in this thread. For a quicker solution, you could co-opt RoMEventManager as a catch-all module for such situations. It doesn't have a handler for key events yet. Code for that could taken from e.g. UnitNameEventmanager, which registers its handler in its constructor through:
eventManager.addEventHandler("kbdEvent", self.onKbdEvent)
Then onKbdEvent checks for the relevant key combination (and calls some internal handler function):
Spoiler :
Python:
	def onKbdEvent(self, argsList):
		eventType,key,mx,my,px,py = argsList
		if ( eventType == self.eventMgr.EventKeyDown ):
			if (int(key) == int(InputTypes.KB_N)
			and self.eventMgr.bCtrl
			and self.eventMgr.bAlt):

				if UnitNamingOpt.isEnabled():
					self.eventMgr.beginEvent(RENAME_EVENT_ID)

		return 0
If Platy's NaturalWonders.py were turned into a BUG-style module, such code would be added there. Alternatively, you could copy-paste the addEventHandler line to the __init__ function of RoMEventManager,py and add to the end of the file, based on UnitNameEventManager:
Spoiler :
Python:
	def onKbdEvent(self, argsList):
		eventType,key,mx,my,px,py = argsList
		if ( eventType == self.eventMgr.EventKeyDown ):
			if (int(key) == int(InputTypes.KB_F)
			and self.eventMgr.bCtrl):
				NaturalWonders.NaturalWonders().showNaturalWonders()
				return 1
		return 0
Plus an import NaturalWonders near the top and possibly temporary print statements to see whether calls occur.
 
As luck would have it, I'm currently also trying to integrate one of Platypings modcomps into my BUG based mod: Requirement Script.
Core question:

Does BUG mess with the PythonCallbackDefines.xml that Requirement Script wants to use?

Requirement Script wants to use the CallbackDefines file, because it tries to disable the construction and training of units and buildings. Now, thanks to f1rpo's thread and comments, I've managed to get the mod to the point where it's actually a module, and everything works... right up until the point where I'd expect the building (Barracks in this case) to be disabled. Thus far, I've confirmed the CustomXML data is loaded (as it appears in the Sevopedia, and the trigger onGameStart printed a line in the logs.), and I've also managed to put handlers into the BugGameUtils (many thanks to this post) - which also show up in the PythonDbg. Everything seems to be set, but I still have no functionality... so now I worry that PythonCallbackDefines.xml, from where the cannotConstruct and cannotTrain functionality is enabled, is the reason. Even brute forcing the cannotTrain function to always return True doesn't have an impact, which leads me to believe my issue doesn't lie in my efforts transforming the mod into a BUG module.

If this is not the case, and PythonCallbackDefines.xml should work fine with BUG out of the box, we clearly have another issue here, and I'll move my struggle into f1rpo's thread 😅
 
I think it should work the way you've described it. In a quick test,
Python:
import BugGameUtils
class AIAutoPlay :
	def __init__(self, eventManager) :
		# ...
		BugGameUtils.addHandler(cannotTrain)
	# ...
def cannotTrain(argsList):
	return True
and USE_CANNOT_TRAIN_CALLBACK set to 1 in PythonCallbackDefines.xml works for me, i.e. I can't train any units after loading a savegame. I've put my code into the AIAutoPlay module of my fork of BULL and the XML change into the original BtS file to minimize my effort. Since your cannotTrain is apparently getting called (log output gets written as I understand you; multiple lines I would expect as the DLL will check canTrain for several if not all unit types), it's difficult to see what is going wrong. If nothing else helps, perhaps you could create a minimal (non-)working example, maybe just hacked into some BUG module, e.g. EventSigns.py. (I should've tried it that way myself, come to think of it, rather than use a module that doesn't come with BUG.)
 
Hello, I've got a question regarding adding new eras, for some reason when I add new eras, the tech tree becomes borked when I start or load a game.
More details here in this thread.
 
Since this answer seems rather general, I guess I'll reply here for the moment:
Python can reference XML-defined types too. Since the mod seems to include BUG (says the FF+ Welcome thread), the tech era coloring (also used by the NJAGC clock) could well be the cause. The Python calls to ClockOpt.getEraColor (CvMainInterface, CvTechChooser) do check for a negative return value, but I don't think this will make the code tolerate unrecognized eras, i.e. eras not covered by Not Just Another Game Clock.xml in Assets\Config. So I suppose no actual Python code will need to be changed. CvRandomEventInterface.py does refer to specific eras but should have no problem with new eras. Random events tend to break when removing XML types. The screenshot looks like a Python crash has occurred; PythonErr.log should have details if LoggingEnabled is set in My Games\Beyond the Sword\CivilizationIV.ini (and/or HidePythonExceptions = 0 for a slew of error popups). But, if the era colors turn out to be the issue, there's perhaps no need to go there.
 
Since this answer seems rather general, I guess I'll reply here for the moment:
Python can reference XML-defined types too. Since the mod seems to include BUG (says the FF+ Welcome thread), the tech era coloring (also used by the NJAGC clock) could well be the cause. The Python calls to ClockOpt.getEraColor (CvMainInterface, CvTechChooser) do check for a negative return value, but I don't think this will make the code tolerate unrecognized eras, i.e. eras not covered by Not Just Another Game Clock.xml in Assets\Config. So I suppose no actual Python code will need to be changed. CvRandomEventInterface.py does refer to specific eras but should have no problem with new eras. Random events tend to break when removing XML types. The screenshot looks like a Python crash has occurred; PythonErr.log should have details if LoggingEnabled is set in My Games\Beyond the Sword\CivilizationIV.ini (and/or HidePythonExceptions = 0 for a slew of error popups). But, if the era colors turn out to be the issue, there's perhaps no need to go there.
That did it. It was in fact the era colors, thanks for the help, had no idea that that could have been the cause.
Guess modders should also add a notice to edit \config\Not Just Another Game Clock.xml and \Python\BUG\Tabs\BugNJAGCOptionsTab.py as well if they make use of BUG from now on.
 
I think it should work the way you've described it. In a quick test,
Python:
import BugGameUtils
class AIAutoPlay :
    def __init__(self, eventManager) :
        # ...
        BugGameUtils.addHandler(cannotTrain)
    # ...
def cannotTrain(argsList):
    return True
and USE_CANNOT_TRAIN_CALLBACK set to 1 in PythonCallbackDefines.xml works for me, i.e. I can't train any units after loading a savegame. I've put my code into the AIAutoPlay module of my fork of BULL and the XML change into the original BtS file to minimize my effort. Since your cannotTrain is apparently getting called (log output gets written as I understand you; multiple lines I would expect as the DLL will check canTrain for several if not all unit types), it's difficult to see what is going wrong. If nothing else helps, perhaps you could create a minimal (non-)working example, maybe just hacked into some BUG module, e.g. EventSigns.py. (I should've tried it that way myself, come to think of it, rather than use a module that doesn't come with BUG.)
Knowing that it should work gave me the motivation to try again, correct way first this time, and so I have it working now. I must've had a mistake slip in at one point.

Thanks for your efforts!
 
It seems that the BUG event manager doesn't call the original event manager's onKbdEvent handler. I guess most of the handlers there, or all, are bypassed by the BUG event manager, which is why Platy's mods generally aren't easily portable to BUG-based modpacks – so this really shouldn't surprise me. The clean approach, then, should be to proceed as described in this thread. For a quicker solution, you could co-opt RoMEventManager as a catch-all module for such situations. It doesn't have a handler for key events yet. Code for that could taken from e.g. UnitNameEventmanager, which registers its handler in its constructor through:
eventManager.addEventHandler("kbdEvent", self.onKbdEvent)
Then onKbdEvent checks for the relevant key combination (and calls some internal handler function):
Spoiler :
Python:
    def onKbdEvent(self, argsList):
        eventType,key,mx,my,px,py = argsList
        if ( eventType == self.eventMgr.EventKeyDown ):
            if (int(key) == int(InputTypes.KB_N)
            and self.eventMgr.bCtrl
            and self.eventMgr.bAlt):

                if UnitNamingOpt.isEnabled():
                    self.eventMgr.beginEvent(RENAME_EVENT_ID)

        return 0
If Platy's NaturalWonders.py were turned into a BUG-style module, such code would be added there. Alternatively, you could copy-paste the addEventHandler line to the __init__ function of RoMEventManager,py and add to the end of the file, based on UnitNameEventManager:
Spoiler :
Python:
    def onKbdEvent(self, argsList):
        eventType,key,mx,my,px,py = argsList
        if ( eventType == self.eventMgr.EventKeyDown ):
            if (int(key) == int(InputTypes.KB_F)
            and self.eventMgr.bCtrl):
                NaturalWonders.NaturalWonders().showNaturalWonders()
                return 1
        return 0
Plus an import NaturalWonders near the top and possibly temporary print statements to see whether calls occur.

Okay, I tried to do the above:
1745167790054.png


1745167755790.png

But than I get these when pressing ANY key.

1745167660473.png


I must have misunderstood something...
 
I guess the variable is named self.eventManager, not self.eventMgr. Copy-paste error on my part then. In three places. If that doesn't do it, please see PythonErr.log; there's probably a useful error message somewhere that gets buried on screen. At least it seems that the key press gets reported to the proper function.
 
I guess the variable is named self.eventManager, not self.eventMgr. Copy-paste error on my part then. In three places. If that doesn't do it, please see PythonErr.log; there's probably a useful error message somewhere that gets buried on screen. At least it seems that the key press gets reported to the proper function.
:clap:
That did it!
1745184299544.png

Thank you!
:bowdown:
 
Is it possible to remove the diplomatic option allowing civs to voluntarily agree to vassalage, while still allowing capitulation from war vassalage? If so, how would I do this?
 
Returning false from CvPlayer::canTradeItem for TRADE_VASSAL should do it, but that'll require the GameCore DLL to be recompiled.

For an XML solution – looking at the AI logic in C++, VassalRefuseAttitudeThreshold (Civ4LeaderHeadInfos.xml) should affect only vassal agreements at peacetime, not capitulation. However, the check for that can get relaxed based on military power, and the threshold in XML can be set to ATTITUDE_FRIENDLY at most (for each of the 52 leaders), which, even without the military component, causes the AI to make vassal agreements at peacetime when Friendly toward the master. It might be possible to add some more attitude levels through Civ4AttitudeInfos.xml just for this purpose. Not sure if the game will handle that correctly and without side-effects. And this won't stop humans from becoming each other's vassals in multiplayer.

Through Python, it might be possible to use CyTeam::changeVassalTradingCount to disable all vassal trading whenever a team is not fighting any war. But that would still allow a team at war to peacefully become the vassal of a third party. I don't think the DLL gives Python a chance to block vassal agreements just as they're being proposed or signed.
 
Another fun question, though this time I'm not very hopeful. I recently discovered through some experimentation* that Civ 4 is extremely downtuned when it comes to texture MIP maps (for those who don't know what they are - dds textures are usually stored with several smaller versions of themselves, which graphical engines usually swap to when we zoom out, both to save the resources and for a smoother picture, as otherwise they'd have to deal with aliasing of small details on the original texture). This varies a bit with resolution (getting even worse at smaller ones) but overall, even at 2560x1440, at maximum (!) zoom a unit will mostly display its 1st MIP** rather than original texture, and for instance, a flag in the main interface is mostly*** 2nd MIP, permanently so, as obviously its "distance" from the screen doesn't change. In practice, it means we never see many (most? all?) textures in their original resolution in Civ 4, but only downscaled at least 2x (and in many cases, such as the interface flag, effectively 4x). I feel it's a pretty big deal.

Saving a dds without MIPs gets the original texture shown, which is great in theory (and can result in some nice-looking crisp screenshots), but since Civ 4 engine doesn't generate MIPs on the fly, MIPless textures cause an awful shimmering on anything animated and simply when moving the map, so it's not really a solution for most things that are displayed on the in-game map (it is the preferable solution to the UI bits that get "overlaid" over the screen, such as all the buttons), as the amount of visual noise when playing increases manyfold.

Depending on the hardware one has, one may be able to force negative LOD bias**** (as I did with my NVIDIA drivers), and it actually works quite well and results in a much crisper picture (basically, you get at least 2x texture resolution in-game with zero additional memory or performance costs!), but this is a highly individual solution, and for instance people playing on integrated Intel chipsets can't do it at all.

That was the long preface; the question itself is as follows: try as I might, I haven't found anything that could control either the overall LOD bias in game, or adjust it for specific elements (either "legitimately" or maybe by tricking the enging into thinking it's closer to the camera than it really is)*****. I wonder if anyone else had more luck in that regard? I'd love to have a hardware-independent solution that I could implement on my side for the users of RI.

------
* As simple as creating a custom texture where MIP levels were instead of their usual auto-generated selves, simply different solid colours. When zooming in and out, I could then clearly tell which MIP level I was seeing on a, say, unit, as it turned solid blue or red.
** MIP levels are each 2x smaller than the previous one; in this case, 1st MIP is 2x smaller than the original texture (or "0 MIP").
*** MIP levels don't "pop" when zooming out, but rather blend into each other in Civ 4 engine, so at any given time you see 2-3 MIP levels blended into each other, usually with one prevalent.
**** LOD bias is basically telling the graphics engine to use a higher or lower level of MIP than it would naturally use in any given situation. A LOD bias of -1, for instance, would force it to use MIP 2 when it would normally use MIP 3.
***** One trick I found is simply doubling the resolution of the existing texture. Obviously, the end "result" should, in theory, look no better than the original, as we didn't improve the texture anyhow by making it bigger. But due to the engine trying to show us MIP 1, the engine is now "tricked" into showing us the original texture as MIP 1, as the original texture is 2x smaller than the new one and is essentially the MIP 1 of the new texture. This has the obvious (and rather harsh) drawback of quadrupling the VRAM usage, as the new texture takes 4x VRAM due to its size. This is basically equal to forcing LOD bias of -1, but unfortunately at a cost.
 
Last edited:
In my mod there is a Movement Limits mechanic, where bExtendMovementLimits tech expands the limit if a certain tech is researched.
My problem is it being a boolean expands the limit only once and not every time researched.
This seems to be the responsible part of the code in cvUnit.cpp

Code:
// Movement Limits by 45deg - START  // f1rpo: rewritten
bool CvUnit::isInsideMovementLimits (const CvPlot* pPlot) const
{
    if (pPlot->isOwned() || pPlot->isRoute() || isRivalTerritory())
        return true;

    CvPlayer const& kOwner = GET_PLAYER(getOwner());
    CvTeam const& kTeam = GET_TEAM(kOwner.getTeam());
    if (kTeam.isRemoveMovementLimits())
        return true;
    if (kOwner.getNumCities() <= 0 || kOwner.isBarbarian())
        return true;

    static int const iDefaultLimit = GC.getDefineINT("BASE_EXPLORATION_RADIUS")
            + GC.getMap().getWorldSize();
    int iMovementLimit = iDefaultLimit;
    if (kTeam.isExtendMovementLimits())
        iMovementLimit *= 2;
    int iIter;
    for (CvCity const* pCity = kOwner.firstCity(&iIter); pCity != NULL; pCity = kOwner.nextCity(&iIter))
    {
        if (plotDistance(pPlot->getX(), pPlot->getY(), pCity->getX(), pCity->getY()) <= iMovementLimit)
            return true;
    }
    // temporary hack for pitboss
    if (gDLL->IsPitbossHost() || (pPlot->isOwned() && pPlot->getTeam() >= 100))
        return true;

    return false;
}
What is the easiest approach?
 
What's the best way to edit the icon grid in the GameFont.tga file?
I want to add a bunch of icons to my fonts. I'm using Asaf's font editor, but that only goes so far... if I add too many new bonuses for example, I'll run out of space. I've tried copying and pasting existing rows with GIMP, but I can't get it to actually take the transparent pixels with it upon copy-pasting 😅

Bonus question: where is the logic that reads the icons from these files hiding? I've done a cursory glance at the SDK files, but I'm not familiar enough to find it myself. Seeing where this is (and perhaps making some changes) might also help me understand how to make the most of the current grid layout - or even adapt another mod's layout and fill it with my own icons.
 
I can't get it to actually take the transparent pixels with it upon copy-pasting 😅
Did you check alpha layer before doing that? They are supposed to exist there.
I want to add a bunch of icons to my fonts.
Maybe try using expanded fonts from other mods? I uploaded some.
 

Attachments

In my mod there is a Movement Limits mechanic, where bExtendMovementLimits tech expands the limit if a certain tech is researched.
My problem is it being a boolean expands the limit only once and not every time researched.
This seems to be the responsible part of the code in cvUnit.cpp

Code:
// Movement Limits by 45deg - START  // f1rpo: rewritten
bool CvUnit::isInsideMovementLimits (const CvPlot* pPlot) const
{
    if (pPlot->isOwned() || pPlot->isRoute() || isRivalTerritory())
        return true;

    CvPlayer const& kOwner = GET_PLAYER(getOwner());
    CvTeam const& kTeam = GET_TEAM(kOwner.getTeam());
    if (kTeam.isRemoveMovementLimits())
        return true;
    if (kOwner.getNumCities() <= 0 || kOwner.isBarbarian())
        return true;

    static int const iDefaultLimit = GC.getDefineINT("BASE_EXPLORATION_RADIUS")
            + GC.getMap().getWorldSize();
    int iMovementLimit = iDefaultLimit;
    if (kTeam.isExtendMovementLimits())
        iMovementLimit *= 2;
    int iIter;
    for (CvCity const* pCity = kOwner.firstCity(&iIter); pCity != NULL; pCity = kOwner.nextCity(&iIter))
    {
        if (plotDistance(pPlot->getX(), pPlot->getY(), pCity->getX(), pCity->getY()) <= iMovementLimit)
            return true;
    }
    // temporary hack for pitboss
    if (gDLL->IsPitbossHost() || (pPlot->isOwned() && pPlot->getTeam() >= 100))
        return true;

    return false;
}
What is the easiest approach?
IMHO, the easiest way to generalize the movement limit would probably be to have kTeam.isExtendMovementLimits() return an int rather than a bool which you could then use a as a modifier rather than an on/off toggle.
 
IMHO, the easiest way to generalize the movement limit would probably be to have kTeam.isExtendMovementLimits() return an int rather than a bool which you could then use a as a modifier rather than an on/off toggle.
Yeah, I guess it's "that simple" :lol:
I'm a little anxious about seeing ExtendMovementLimits in some form all over the dll:
1747319395422.png


1747319428526.png


1747319479795.png


1747319516249.png


1747319532229.png


1747319548780.png


1747319565264.png


1747319583604.png


1747319598278.png


1747319615264.png


1747319644638.png


But maybe it isn't as difficult as I imagine? Looks like 45 already had that in mind, I guess?
 
Back
Top Bottom