Quick Modding Questions Thread

As far as compiling the DLL goes the hardest part is getting your hands on an ancient copy of visual studio.
I know of modders that use Visual Studio 2017 (or maybe even newer versions).

The SDK, libs and compiler need to be the orignal ones. The IDE (i.e. Visual Studio) can be newer.
(If you have a project file for Visual Studio Express 2010 there may be adjustments necessary if you want to use a newer version of VS.)

------

Visual Studio Express 2010 of course still works perfectly fine, if you get it and also still find a license key.
Otherwise simply get a newer version e.g. Visual Studio 2017 and ask the other Civ4BTS modders for an according project file for VS 2017.

I cannot give you such a project file since I do not mod Civ4BTS and also still use VS Express 2010 myself.
(I basically never changed my development environment since I started modding in 2008 except small stuff like switching from SVN to GIT.)

------

You could however ask e.g. @Nightinggale or @devolution.
They both use newer versions of Visual Studio to my knowledge.
(Both also mod Civ4BTS and not just Civ4Col.)
 
Last edited:
I don't really see what could go wrong in CvPlayerAI::AI_unitUpdate itself.
I thought so too, but after finally taking the time to investigate that is where I ended up. In this part:
Code:
            while (tempGroupCycle.getLength() > 0)
            {
                pCurrUnitNode = tempGroupCycle.head();

                while (pCurrUnitNode != NULL)
                {
                    pLoopSelectionGroup = getSelectionGroup(pCurrUnitNode->m_data);
                    FAssertMsg(pLoopSelectionGroup != NULL, "selection group node with NULL selection group");

                    if (AI_movementPriority(pLoopSelectionGroup) <= iValue)
                    {
                        finalGroupCycle.insertAtEnd(pCurrUnitNode->m_data);
                        pCurrUnitNode = tempGroupCycle.deleteNode(pCurrUnitNode);

                        if (pLoopSelectionGroup->getID() == 1499136)
                        {
                            iNodeID = pCurrUnitNode->m_data;
                            log("inserted into list");
                        }
                    }
                    else
                    {
                        pCurrUnitNode = tempGroupCycle.next(pCurrUnitNode);
                    }
                }

                iValue++;
            }

            pCurrUnitNode = finalGroupCycle.head();

            while (pCurrUnitNode != NULL)
            {
                if (pCurrUnitNode->m_data == iNodeID)
                {
                    log("node ID found");
                }

                pLoopSelectionGroup = getSelectionGroup(pCurrUnitNode->m_data);

                if (pLoopSelectionGroup->getID() == 1499136)
                {
                    log("player update group AI");
                }

                if (NULL != pLoopSelectionGroup)  // group might have been killed by a previous group update
                {
                    if (pLoopSelectionGroup->AI_update())
                    {
                        break; // pointers could become invalid...
                    }
                }

                pCurrUnitNode = finalGroupCycle.next(pCurrUnitNode);
            }
1499136 is the ID of one of the selection groups that don't take their turn. I inserted breakpoints at each of the log statements. I only hit "inserted into list" in the first loop, but none of the breakpoints in the second loop. I also checked that the second loop gets iterated at all, and it does.

(For further context, this means that none of the AI code for that selection group and its units ever gets called, so there is actually no issue with those parts. I also eliminated all instances of Rhye's "loop fixes" so they were not the culprit here.)

I really can't explain how this could happen. Is there anything I am missing here?
 
Yesterday, I began having strange errors in onCityGrowth. I have found out exactly what causes it, but I have no idea why, or how to properly fix it. For years, I have had the Great Bath wonder in my game, which uses the following code in CvEventManager.py:

XML:
## The Great Bath Start ##
        pPlayer = gc.getPlayer(iPlayer)
        if pPlayer.getBuildingClassCount(gc.getInfoTypeForString("BUILDINGCLASS_GREAT_BATH")) == 1:
            obsoleteTech = gc.getBuildingInfo(gc.getInfoTypeForString("BUILDINGCLASS_GREAT_BATH")).getObsoleteTech()
            if gc.getTeam(pPlayer.getTeam()).isHasTech(obsoleteTech) == false or obsoleteTech == -1:
                pCity.changeFood(pCity.growthThreshold() /10)
## The Great Bath End ##

I.e., on city growth, it maintains 10% of a city's food. This has worked for years, but now, once a civilisation builds the Great Bath, and once a city of said civilisation grows, I get an error pointing at line 1224 in CvEventManager.py, which is the line of 'obsoleteTech = ...':
1665323007831.png

1665323035136.png

1665323057278.png

Fiddling around with <ObsoleteTech> in the XML file doesn't matter; all my wonders have <ObsoleteTech>NONE</ObsoleteTech>, but changing this to <ObsoleteTech/> or even <ObsoleteTech>TECH_RIFLING</ObsoleteTech> (a random choice) doesn't matter.
However, if I comment out the obsolete tech part, like below, it magically works fine again:

XML:
## The Great Bath Start ##
        pPlayer = gc.getPlayer(iPlayer)
        if pPlayer.getBuildingClassCount(gc.getInfoTypeForString("BUILDINGCLASS_GREAT_BATH")) == 1:
            #obsoleteTech = gc.getBuildingInfo(gc.getInfoTypeForString("BUILDINGCLASS_GREAT_BATH")).getObsoleteTech()
            #if gc.getTeam(pPlayer.getTeam()).isHasTech(obsoleteTech) == false or obsoleteTech == -1:
            pCity.changeFood(pCity.growthThreshold() /10)
## The Great Bath End ##

As far as I can see, the obsolete tech part should work. And it has worked, for years.
The only thing I changed recently, was adding seven new civilisations, none of which use anything but ordinary XML code.
The last thing I did involving Python and/or callbacks, was setting USE_CANNOT_FOUND_CITY_CALLBACK to 1 or 0 (you can find this in my post history in this very thread - again, this was months or a year ago or so).

Does anyone have any idea what is going on here?
 
May need to enable the respective Python callbacks in PythonCallbackDefines.xml. Vanished interface: Likely a Python exception, stack trace should be in PythonErr.log if logging is enabled in CivilizationIV.ini (can also get a popup through HidePythonExceptions=0). But imo implementing this rule change (partly) in Python only complicates matters, considering that you'll mod the DLL in any case.
Just for fun I tried that and I still get no effect. As in I don't get crashes but even if I change true to false (which should make all techs unresearchable) nothing happens.
 
Just for fun I tried that and I still get no effect. As in I don't get crashes but even if I change true to false (which should make all techs unresearchable) nothing happens.
I've just tried it with a BUG-based mod and also ran into some trouble. BUG overrides CvGameUtils: CvGameInterfaceFile.py#L11
BugGameUtils#L364 may still call the CvGameUtils module, but, it turns out that it won't do so when a default return value is set in BugGameUtils.py for the given callback function. If I comment out the line
self._setDefault("cannotResearch", False)
(and enable USE_CANNOT_RESEARCH_CALLBACK in XML), then returning True from the cannotResearch function in CvGameUtils does make all tech unresearchable. I don't know if that's how it's supposed to be done. There's documentation at the start of BugGameUtils.py, but I've never had the patience to study that. Because I don't think DLL modders should make use of Python callbacks anyway. Elegant as the BUG software design may be, I think it's a bit of a failure (as far as the handling of calls from the DLL is concerned) because it's too complex to understand for those modders that need to rely on it.

Edit: Case in point, the PIE modders ran into the same issue with BUG. (The request to "explain [...] the BUG python hook system" remained unanswered.)
 
Last edited:
Yesterday, I began having strange errors in onCityGrowth. I have found out exactly what causes it, but I have no idea why, or how to properly fix it. For years, I have had the Great Bath wonder in my game, which uses the following code in CvEventManager.py:

XML:
## The Great Bath Start ##
        pPlayer = gc.getPlayer(iPlayer)
        if pPlayer.getBuildingClassCount(gc.getInfoTypeForString("BUILDINGCLASS_GREAT_BATH")) == 1:
            obsoleteTech = gc.getBuildingInfo(gc.getInfoTypeForString("BUILDINGCLASS_GREAT_BATH")).getObsoleteTech()
            if gc.getTeam(pPlayer.getTeam()).isHasTech(obsoleteTech) == false or obsoleteTech == -1:
                pCity.changeFood(pCity.growthThreshold() /10)
## The Great Bath End ##

I.e., on city growth, it maintains 10% of a city's food. This has worked for years, but now, once a civilisation builds the Great Bath, and once a city of said civilisation grows, I get an error pointing at line 1224 in CvEventManager.py, which is the line of 'obsoleteTech = ...':
View attachment 641308
View attachment 641309
View attachment 641310
Fiddling around with <ObsoleteTech> in the XML file doesn't matter; all my wonders have <ObsoleteTech>NONE</ObsoleteTech>, but changing this to <ObsoleteTech/> or even <ObsoleteTech>TECH_RIFLING</ObsoleteTech> (a random choice) doesn't matter.
However, if I comment out the obsolete tech part, like below, it magically works fine again:

XML:
## The Great Bath Start ##
        pPlayer = gc.getPlayer(iPlayer)
        if pPlayer.getBuildingClassCount(gc.getInfoTypeForString("BUILDINGCLASS_GREAT_BATH")) == 1:
            #obsoleteTech = gc.getBuildingInfo(gc.getInfoTypeForString("BUILDINGCLASS_GREAT_BATH")).getObsoleteTech()
            #if gc.getTeam(pPlayer.getTeam()).isHasTech(obsoleteTech) == false or obsoleteTech == -1:
            pCity.changeFood(pCity.growthThreshold() /10)
## The Great Bath End ##

As far as I can see, the obsolete tech part should work. And it has worked, for years.
The only thing I changed recently, was adding seven new civilisations, none of which use anything but ordinary XML code.
The last thing I did involving Python and/or callbacks, was setting USE_CANNOT_FOUND_CITY_CALLBACK to 1 or 0 (you can find this in my post history in this very thread - again, this was months or a year ago or so).

Does anyone have any idea what is going on here?
Perhaps a typo somewhere? Like not obsoleteTech but ObsoleteTech? Just guessing.
 
I've just tried it with a BUG-based mod and also ran into some trouble. BUG overrides CvGameUtils: CvGameInterfaceFile.py#L11
BugGameUtils#L364 may still call the CvGameUtils module, but, it turns out that it won't do so when a default return value is set in BugGameUtils.py for the given callback function. If I comment out the line
self._setDefault("cannotResearch", False)
(and enable USE_CANNOT_RESEARCH_CALLBACK in XML), then returning True from the cannotResearch function in CvGameUtils does make all tech unresearchable. I don't know if that's how it's supposed to be done. There's documentation at the start of BugGameUtils.py, but I've never had the patience to study that. Because I don't think DLL modders should make use of Python callbacks anyway. Elegant as the BUG software design may be, I think it's a bit of a failure (as far as the handling of calls from the DLL is concerned) because it's too complex to understand for those modders that need to rely on it.

Edit: Case in point, the PIE modders ran into the same issue with BUG. (The request to "explain [...] the BUG python hook system" remained unanswered.)
Thanks. I just want a quick and dirty way to make a couple techs completely impossible to research.
 
Perhaps a typo somewhere? Like not obsoleteTech but ObsoleteTech? Just guessing.
But 'obsoleteTech' is a variable that is declared right there, used only there, and could as well have been 'heylookatmyprettyname' or anything you'd like. Right?
'getObsoleteTech' is an existing function.
 
I only hit "inserted into list" in the first loop, but none of the breakpoints in the second loop. I also checked that the second loop gets iterated at all, and it does.
Just based on looking at the code: The second loop terminates when CvSelectionGroupAI::AI_update returns true. That happens when the group "becomes busy," a comment says, but, actually, it also returns true when a group dies or splits during an attack. CvPlayerAI::AI_unitUpdate then gets another call from CvGame::update via CvGame::updateMoves – unless m_bAutoMoves has been set for the current player, which should happen when all units have spent their moves (or died) and none are waiting for combat or mission animations to finish:
Spoiler :
(This is slightly edited BtS code from CvGame::updateMoves; I haven't checked whether it's any different in DoC.)
Code:
if (!kPlayer.isAutoMoves())
{
	kPlayer.AI_unitUpdate();
	if (!kPlayer.isHuman() &&
		!kPlayer.hasBusyUnit() && !kPlayer.hasReadyUnit(true))
	{
		kPlayer.setAutoMoves(true);
	}
}
So I guess you're getting only one call to CvPlayerAI::AI_unitUpdate that inserts 1499136 into the group cycle. And then the cycle isn't fully processed (could verify that by inserting a breakpoint at the break statement once the inserted-into-list line has been hit), and, during the next CvPlayerAI::AI_unitUpdate call (is there one?) ... the group no longer exists, apparently. Might be worth checking if the ID comes up in CvPlayer::deleteSelectionGroup. But that should not cause an infinite loop – if some group absorbs 1499136 (not sure if that can happen), then, fine, group 1499136 no longer needs to be moved.

Looking at the code above, I wonder if the return true line in CvUnit::canMove keeps getting reached during the infinite loop. That function should get called by kPlayer.hasReadyUnit(true). Maybe you've already checked - and maybe that's how you've figured out that group ID. Well, in that case, the group clearly isn't getting killed. :undecide:
 
@need my speed: You're calling getBuildingInfo with a BuildingClass ID as the argument. You need a Building ID. So, I guess, simply using "BUILDING_GREAT_BATH" instead of "BUILDINGCLASS_GREAT_BATH" should fix the problem, assuming that the Great Bath building has that type string. I'm guessing that the BuildingClass ID of the Great Bath exceeds the range of Building IDs, and then the DLL will return NULL, which, I guess, becomes None in Python:
Code:
CvBuildingInfo* CyGlobalContext::getBuildingInfo(int i) const
{
	return (i>=0 && i<GC.getNumBuildingInfos()) ? &GC.getInfo((BuildingTypes)i) : NULL;
}
So, if you've added more building classes recently, that could explain why the mixup is no longer going unnoticed. Although it's strange that there should be fewer buildings than building classes. So maybe this explanation isn't quite right.
 
Just based on looking at the code: The second loop terminates when CvSelectionGroupAI::AI_update returns true. That happens when the group "becomes busy," a comment says, but, actually, it also returns true when a group dies or splits during an attack. CvPlayerAI::AI_unitUpdate then gets another call from CvGame::update via CvGame::updateMoves – unless m_bAutoMoves has been set for the current player, which should happen when all units have spent their moves (or died) and none are waiting for combat or mission animations to finish:
Spoiler :
(This is slightly edited BtS code from CvGame::updateMoves; I haven't checked whether it's any different in DoC.)
Code:
if (!kPlayer.isAutoMoves())
{
    kPlayer.AI_unitUpdate();
    if (!kPlayer.isHuman() &&
        !kPlayer.hasBusyUnit() && !kPlayer.hasReadyUnit(true))
    {
        kPlayer.setAutoMoves(true);
    }
}
So I guess you're getting only one call to CvPlayerAI::AI_unitUpdate that inserts 1499136 into the group cycle. And then the cycle isn't fully processed (could verify that by inserting a breakpoint at the break statement once the inserted-into-list line has been hit), and, during the next CvPlayerAI::AI_unitUpdate call (is there one?) ... the group no longer exists, apparently. Might be worth checking if the ID comes up in CvPlayer::deleteSelectionGroup. But that should not cause an infinite loop – if some group absorbs 1499136 (not sure if that can happen), then, fine, group 1499136 no longer needs to be moved.

Looking at the code above, I wonder if the return true line in CvUnit::canMove keeps getting reached during the infinite loop. That function should get called by kPlayer.hasReadyUnit(true). Maybe you've already checked - and maybe that's how you've figured out that group ID. Well, in that case, the group clearly isn't getting killed. :undecide:
Oh true, thanks. If AI_update() returns true for any selection group it breaks the loop - my mind somehow made the incorrect assumption that it's going to abort for the current selection group only (i.e. essentially assumed it's "continue"). That's definitely worth investigating. Maybe the issue is with another group that reports updating but not properly takes care of itself, like you mention, which then keeps repeating without giving the group I am looking at a chance to take its turn.

That might also explain why it's such a different selection of units affected. Identifying the group that causes this shouldn't be problematic, and I can start looking at it from there.

And yeah, hasReadyUnit was my starting point to identify one of the stuck groups, but it did not occur to me that only one unit/group in the game could exhibit the actual root cause and the others just keep getting stuck because they are never even allowed to take their turns.
 
@need my speed: You're calling getBuildingInfo with a BuildingClass ID as the argument. You need a Building ID. So, I guess, simply using "BUILDING_GREAT_BATH" instead of "BUILDINGCLASS_GREAT_BATH" should fix the problem, assuming that the Great Bath building has that type string. I'm guessing that the BuildingClass ID of the Great Bath exceeds the range of Building IDs, and then the DLL will return NULL, which, I guess, becomes None in Python:
Code:
CvBuildingInfo* CyGlobalContext::getBuildingInfo(int i) const
{
    return (i>=0 && i<GC.getNumBuildingInfos()) ? &GC.getInfo((BuildingTypes)i) : NULL;
}
So, if you've added more building classes recently, that could explain why the mixup is no longer going unnoticed. Although it's strange that there should be fewer buildings than building classes. So maybe this explanation isn't quite right.
Yep, simply changing the buildingclass to the building worked, thank you very much! :)
Still very weird how this only became an issue now, and not three years ago.
 
Can techs add bonus strength to units? As in tech X adds 5 points of damage to tanks or something. I am asking specifically about the unmodified game. I know I can mod it in but don't want to.
 
Oh! Thank you!
I was looking in the wrong file, then.
No wonder I was getting frustrated.

Here is the proof it worked!
View attachment 546389

I've attached the two files as zip-files. [Python\Screens\CvMainInterface] and [Python\BUG\Scoreboard].
All of my changes are marked with #kjotleik, so a search should be easy.

Thanks again. I would probably have spent way too long on this on my own.
I have not tested this in a full game, yet. So things may happen. But at turn zero, it looks good.

I know this is from years ago but, a BIG thank you. This helps a lot with maps that go off the edges. And especially for including your name everwhere you made a change to the code. It allowed me to use winmerge to compare the differences in our files, and putting your name allowed me to defrientiate what code changed from versions and which code are your changes. I was able to achieve the same thing as you without problems (just a bit of work changing the code line-by-line with winmerge). If you hadn't put your name with each commenting this would have been a real pain. And thanks for spending all those hours figuring it out for the rest of us.

Civ4ScreenShot0001.JPG

Edit: Looks like I spoke too soon. The specialist section on the city screen needs to be adjusted. However, after doing so the increase and decrease buttons do not work. I have run out of ideas since all y values that corrospond with those buttons have been changed going by both the edit in this thread, as well as the default settings.

Civ4ScreenShot0011.JPG


Python:
# Increase Specialists...
        i = 0
        for i in range( gc.getNumSpecialistInfos() ):
            if (gc.getSpecialistInfo(i).isVisible()):
                szName = "IncreaseSpecialist" + str(i)
                screen.setButtonGFC( szName, u"", "", xResolution - 46, (yResolution - 450 - (26 * iCount)), 20, 20, WidgetTypes.WIDGET_CHANGE_SPECIALIST, i, 1, ButtonStyles.BUTTON_STYLE_CITY_PLUS )
                screen.hide( szName )

                iCount = iCount + 1

        iCount = 0

        # Decrease specialists
        i = 0
        for i in range( gc.getNumSpecialistInfos() ):
            if (gc.getSpecialistInfo(i).isVisible()):
                szName = "DecreaseSpecialist" + str(i)
                screen.setButtonGFC( szName, u"", "", xResolution - 24, (yResolution - 450 - (26 * iCount)), 20, 20, WidgetTypes.WIDGET_CHANGE_SPECIALIST, i, -1, ButtonStyles.BUTTON_STYLE_CITY_MINUS )
                screen.hide( szName )

This corrospons to
Python:
# Citizen Buttons
        i = 0
        for i in range( gc.getNumSpecialistInfos() ):
       
            if (gc.getSpecialistInfo(i).isVisible()):
           
                szName = "CitizenDisabledButton" + str(i)
                screen.setImageButton( szName, gc.getSpecialistInfo(i).getTexture(), xResolution - 74, (yResolution - 452 - (26 * i)), 24, 24, WidgetTypes.WIDGET_DISABLED_CITIZEN, i, -1 )
                screen.enable( szName, False )
                screen.hide( szName )

                for j in range(MAX_CITIZEN_BUTTONS):
                    szName = "CitizenButton" + str((i * 100) + j)
                    screen.addCheckBoxGFC( szName, gc.getSpecialistInfo(i).getTexture(), "", xResolution - 74 - (26 * j), (yResolution - 452 - (26 * i)), 24, 24, WidgetTypes.WIDGET_CITIZEN, i, j, ButtonStyles.BUTTON_STYLE_LABEL )
                    screen.hide( szName )

and
Python:
for j in range( iCount ):
                bHandled = True
                szName = "CitizenButton" + str((i * 100) + j)
                screen.addCheckBoxGFC( szName, gc.getSpecialistInfo(i).getTexture(), "", xResolution - 74 - (26 * j), (yResolution - 452 - (26 * i)), 24, 24, WidgetTypes.WIDGET_CITIZEN, i, j, ButtonStyles.BUTTON_STYLE_LABEL )
                screen.show( szName )
                szName = "CitizenButtonHighlight" + str((i * 100) + j)
                screen.addDDSGFC( szName, ArtFileMgr.getInterfaceArtInfo("BUTTON_HILITE_SQUARE").getPath(), xResolution - 74 - (26 * j), (yResolution - 452 - (26 * i)), 24, 24, WidgetTypes.WIDGET_CITIZEN, i, j )
                if ( pHeadSelectedCity.getForceSpecialistCount(i) > j ):
                    screen.show( szName )
                else:
                    screen.hide( szName )

and

Python:
while (iCount > 0):
                bHandled = True

                xCoord = xResolution - 74 - (26 * j)
                yCoord = yResolution - 452 - (26 * i)

No idea why the increase and decrease buttons for the specialists are unclickable. The only way to have it work is to have all the specialist values for the city screen set back to BUG's default and accept the overlap with the minimap. If I tackle this issue again and find a solution I shall post it here. If someone with more knowledge of how the interface is coded could point me in the right direction I would also greatly appreciate that!
 
Last edited:
Has anyone ever figured out how to make GFC billboards (setting a negative value in CIV4ArtDefines_Misc.xml) work in a meaningful way? I can't seem to find a way to influence, first and foremost, the width of the name (as you can see it is cut off at the sides, and messing with the nif of the billboard itself does nothing) or the scale of the font (technically the fScale in CIV4ArtDefines_Misc.xml is supposed to be, you know, scale, but it seems to not have any effect outside switching between positive and negative).
 

Attachments

  • cityname.jpg
    cityname.jpg
    82.7 KB · Views: 28
Can techs add bonus strength to units? As in tech X adds 5 points of damage to tanks or something. I am asking specifically about the unmodified game. I know I can mod it in but don't want to.
Don’t think there’s a direct xml edit to achieve that.
It would be opaque to the AI but you could have python give a trait to civ when a tech is researched. That trait can then give an applicable promotion to units.
 
Edit: Looks like I spoke too soon. The specialist section on the city screen needs to be adjusted. However, after doing so the increase and decrease buttons do not work. I have run out of ideas since all y values that corrospond with those buttons have been changed going by both the edit in this thread, as well as the default settings. [...] No idea why the increase and decrease buttons for the specialists are unclickable. The only way to have it work is to have all the specialist values for the city screen set back to BUG's default and accept the overlap with the minimap. If I tackle this issue again and find a solution I shall post it here. If someone with more knowledge of how the interface is coded could point me in the right direction I would also greatly appreciate that!
Maybe the resource list (or some invisible or transparent container around it) is overlapping the buttons and intercepts clicks? But I guess, if the specialists can be moused over (displaying hover text), then that can't be the issue. There's a lot of code in CvMainInterface.py for dealing with specialists because BUG adds two alternative display methods – "Chevron" and "Stacker" – with a lot of redundant code between those two and the original display method. Looks like you use the original one and have changed only the non-BUG code – which should suffice.
 
Maybe the resource list (or some invisible or transparent container around it) is overlapping the buttons and intercepts clicks? But I guess, if the specialists can be moused over (displaying hover text), then that can't be the issue. There's a lot of code in CvMainInterface.py for dealing with specialists because BUG adds two alternative display methods – "Chevron" and "Stacker" – with a lot of redundant code between those two and the original display method. Looks like you use the original one and have changed only the non-BUG code – which should suffice.
Yes, hover effect works. I'll just have to try tackle the issue again at a later date, but I ran out of ideas this round. As you say, the hover effect means that its not 'under' some other element, but I haven't found anything related to a z-index, so I'll start with that approach next time I get to this and report back when I have found a solution.
 
And another one from me. I know the diplo screen is generally considered unmoddable, but I wonder if the strings used are not. In particular the leader/civ name text at the top of the screen - I'd love to be able to intercept it and reduce the font size if it's too long (as in this example).
 

Attachments

  • Cath.png
    Cath.png
    732.9 KB · Views: 22
Back
Top Bottom