Quick Modding Questions Thread

It is however a good starting point for him if he wants to delve into the DLL to do it.
 
How can I correctly add a player option to a mod?

I tried to add one by simply adding another entry in CIV4PlayerOptionInfos.xml. However that seems to introduce problems when switching between mods or to the base game: it seems that when the number of options is different, the stored preferences become incompatible, so that they reset to the defaults for all game related settings (e.g. low resolution etc.) when switching to a different mod.

Is there a way to add an option that avoids this?

I also saw that the base game reserved three _MODDER options that are currently unused for this purpose. I don't have a problem with using those, but they don't seem to show up in the game menu. Which change do I need to make so they can be toggled in the menu?
 
USE_MODDERS_PLAYEROPTION_1 in GlobalDefines seems to do the trick. Gets used here: CvOptionsScreen.py#L280
[...] it seems that when the number of options is different, the stored preferences become incompatible, so that they reset to the defaults for all game related settings (e.g. low resolution etc.) when switching to a different mod.
This sounds like a difficult problem to work around - if the unmodified game chokes on extra data in the user profile but the mod needs that data to stop the option from being reset each time that the mod is loaded.

BUG, with its mod-specific INI files, imo solves this problem neatly. Unless an option affects the opening menu, that is. E.g. the BUG option for Sevopedia has no effect when the Pedia is accessed from the opening menu.
 
Great, thanks! Following BUG's pattern seems a bit too much work for me to get into right now so I guess the reserved option will have to do.
 
I think you'd have to do that through the DLL, specifically CvTeam::setHasTech or, if you want to get rid of all change-civics popups CvDLLButtonPopup::launchChangeCivicsPopup.
So if I want to get rid of this:
1662452572972.png


I just need to do this:
C++:
    CivicOptionTypes eCivicOptionType = (CivicOptionTypes)info.getData1();
    CivicTypes eCivicType = (CivicTypes)info.getData2();
    bool bValid = false;

    if (eCivicType != NO_CIVIC)
    {
        for (int iI = 0; iI < GC.getNumCivicOptionInfos(); iI++)
        {
            if (iI == eCivicOptionType)
            {
                paeNewCivics[iI] = eCivicType;
            }
            else
            {
                paeNewCivics[iI] = GET_PLAYER(GC.getGameINLINE().getActivePlayer()).getCivics((CivicOptionTypes)iI);
            }
        }

        if (GET_PLAYER(GC.getGameINLINE().getActivePlayer()).canRevolution(paeNewCivics))
        {
            bValid = true;
        }
    }
    else
    {
        bValid = true;
    }
/*
    if (bValid)
    {
        CvWString szBuffer;
        if (eCivicType != NO_CIVIC)
        {
            szBuffer = gDLL->getText("TXT_KEY_POPUP_NEW_CIVIC", GC.getCivicInfo(eCivicType).getTextKeyWide());
            if (!CvWString(GC.getCivicInfo(eCivicType).getStrategy()).empty())
            {
                CvWString szTemp;
                szTemp.Format(L" (%s)", GC.getCivicInfo(eCivicType).getStrategy());
                szBuffer += szTemp;
            }
            szBuffer += gDLL->getText("TXT_KEY_POPUP_START_REVOLUTION");
            gDLL->getInterfaceIFace()->popupSetBodyString(pPopup, szBuffer);

            szBuffer = gDLL->getText("TXT_KEY_POPUP_YES_START_REVOLUTION");
            int iAnarchyLength = GET_PLAYER(GC.getGameINLINE().getActivePlayer()).getCivicAnarchyLength(paeNewCivics);
            if (iAnarchyLength > 0)
            {
                szBuffer += gDLL->getText("TXT_KEY_POPUP_TURNS_OF_ANARCHY", iAnarchyLength);
            }
            gDLL->getInterfaceIFace()->popupAddGenericButton(pPopup, szBuffer, NULL, 0, WIDGET_GENERAL);
        }
        else
        {
            gDLL->getInterfaceIFace()->popupSetBodyString(pPopup, gDLL->getText("TXT_KEY_POPUP_FIRST_REVOLUTION"));
        }

        gDLL->getInterfaceIFace()->popupAddGenericButton(pPopup, gDLL->getText("TXT_KEY_POPUP_OLD_WAYS_BEST").c_str(), NULL, 1, WIDGET_GENERAL);
        gDLL->getInterfaceIFace()->popupAddGenericButton(pPopup, gDLL->getText("TXT_KEY_POPUP_SEE_BIG_PICTURE").c_str(), NULL, 2, WIDGET_GENERAL);
        gDLL->getInterfaceIFace()->popupSetPopupType(pPopup, POPUPEVENT_CIVIC, ARTFILEMGR.getInterfaceArtInfo("INTERFACE_POPUPBUTTON_CIVICS")->getPath());
        gDLL->getInterfaceIFace()->popupLaunch(pPopup, false, POPUPSTATE_MINIMIZED);
    }
*/
    SAFE_DELETE_ARRAY(paeNewCivics);

    return (bValid);
}

Is that right or am I missing something?
 
Is that right or am I missing something?
Well, probably better not to return true when the popup wasn't actually launched. So I'd comment out the whole body and return false instead – which will hopefully cause the EXE to (immediately) free the memory allocated for the popup. :think:
Given this uncertainty about memory deallocation, I would actually feel more at ease commenting out the player loop in CvTeam::setHasTech (and leaving launchChangeCivicsPopup alone). Even if that means that the change-civics popup shown at the end of Advanced Start needs to be commented out as well. That seems to be the only place other than setHasTech where a change-civics popup gets triggered. (I see no other place in Rise of Mankind either.)
 
Honestly we'd have to look at just what consumes that return value. I mean, is that boolean a return for "popup has launched" or is it something else that is used somewhere else later on?
 
Well, probably better not to return true when the popup wasn't actually launched. So I'd comment out the whole body and return false instead – which will hopefully cause the EXE to (immediately) free the memory allocated for the popup. :think:
Given this uncertainty about memory deallocation, I would actually feel more at ease commenting out the player loop in CvTeam::setHasTech (and leaving launchChangeCivicsPopup alone). Even if that means that the change-civics popup shown at the end of Advanced Start needs to be commented out as well. That seems to be the only place other than setHasTech where a change-civics popup gets triggered. (I see no other place in Rise of Mankind either.)
Well, I tested it and seemed to work fine: No popup, no crash. Though I played only 1 turn to test it.
I can try what you suggest but to make clear: It's only the popup I want to get rid of. Players should be able to change civics on the civic screen.
 
Then, at worst, I expect that returning true despite not launching the popup results in a tiny memory leak. It's conceivable that popupLaunch (in the EXE) deletes pPopup and that the caller of launchButtonPopup (also in the EXE) will only delete pPopup if false was returned. In any case, a comment above launchButtonPopup states that the function "returns false if popup is not launched." The cleanest way to disable the popup would arguably be inside the addPopup function that both CvTeam::setHasTech and CvPlayer::doAdvancedStartAction call. That way, the change would be in a single place and the popup data would not be unnecessarily allocated. addPopup is implemented in the EXE, but Rise of Mankind has implemented its own queue for popups, so one could discard change-civics popups in CvGame::doQueuedUIActivity.
I can try what you suggest but to make clear: It's only the popup I want to get rid of. Players should be able to change civics on the civic screen.
Yes, I realize that, and killing the popup shouldn't be complicated to do. Regardless of where you remove code for handling BUTTONPOPUP_CHANGECIVIC, the observable result should be the same – no popup.
 
I think what you commented out is fine too so long as you ensure that false is returned. It's actually pretty normal not to launch at that point; will also happen if the new civic has requirements, e.g. a state religion, that aren't met. CvTeam::setHasTech doesn't make those checks, they only happen when the popup is about to launch. So the EXE clearly can handle that correctly. When returning true, i.e. telling the EXE that the popup has been launched, it's difficult to rule out anything. A few byte of memory getting leaked each time that a new civic becomes unlocked seems plausible. Which is irrelevant for performance but might someday catch and divert the attention of someone trying to debug a more serious memory leak.
 
Is there a way to include version information in save files in a way that is accessible without opening them in the game? I would like to include something like a commit hash, or a git tag, so that when someone sends a save file in a bug report I know which state of my code it is from and what it is compatible with.

How this information can be retrieved/provided is the next question, but before I think about that I would like to know if that is feasible at all.
 
There is some uncompressed data at the start of a savegame: The SAVE_VERSION from GlobalDefines, mod name, CRC data (when using Lock Modified Assets) plus some of the data stored by CvInitCore – but it's not obtained via CvInitCore::write, so one can't just write a version string in that function. One could put the version into the game name. In the attached screenshot (hex editor vs. text editor), the game name is ... "HAMZAH" (not a game I created). That'll also be visible under "Game Details" from the in-game ESC menu; nowhere else I think(?). Might be possible to return the version string only when the EXE is about to write the preamble, but that seems tricky to get right.
Code:
const CvWString& getGameName() const
{
	if (...?)
	{
		static CvWString szModVersion = "mod build 1.07.9";
		return szModVersion;
	}
	return m_szGameName;
}
Edit: Or could store the true game name, as received by CvInitCore::setGameName, in a separate serialized string member and let CvDLLButtonPopup::launchAdminPopup ("Game Details") show that true name. Would just have to be careful to add the new data member after the members that the EXE accesses directly. m_szGameName btw also seems to get accessed directly, but changing it via the return value of getGameName still seems to work because the EXE goes through CvInitCore::resetGame(CvInitCore*,bool,bool), which calls getGameName. CvInitCore is a nasty piece of work. Or perhaps appending version info to the actual game name would be fine, even in the game name that the player gets to see.
Not sure if both passwords (game and admin pw) are used in a single-player game; maybe one of those could be repurposed. Edit: Those get hashed, predictably.

Increasing the SAVE_VERSION value (in XML or through CvGlobals::setDefineINT) should work, but has some drawbacks: Will prevent players from loading saves written by a more recent version (i.e. breaks forward compatibility); will (iirc) show a version mismatch popup when trying to open the mod's saves in BtS or in a different mod - instead of the more helpful popup stating which mod is required; gets written as an integer, so not really readable in a text editor.

Obtaining a version string from the (zlib-)compressed part of the savegame is probably too big a task.

Would be nice if one could store the version string in Windows metadata. That would involve figuring out when a savegame has been written (perhaps the file is already present when the serialization functions in the DLL get called?), locating the file and using the Windows property system to write the metadata. And I'm not sure if Windows would actually set such data aside when an application opens the file; obviously BtS wouldn't know what to do with it.

And then there's the question of how to (automatically) get Git data into C++ code. Maybe you already have a toolchain for that.
 

Attachments

  • save_preamble.jpg
    save_preamble.jpg
    1.8 MB · Views: 20
Last edited:
Oh, using the game name would be a good solution, I think. I agree that appending to the player selected game name would not be too bad. If someone names their game something like "Leoreth's game" but it's shown as "Leoreth's game (version x.y.z)" that would not be that disruptive or unexpected. It actually may be a net benefit either way because it gives a fairly intuitive answer when a player asks where to look up their game version, rather than "open this file lying around in your mod folder".

I agree that using windows properties would be even nicer but I kind of expected that not to be feasible. I'm personally fine digging it out of the raw file, my main problem is that currently this information is completely absent.

As for how to inject the version identifier, my initial idea was to have a header file that contains a MOD_VERSION macro definition that gets retemplated whenever the version identifier is update via some shellscript. What I don't like about this approach is it necessitates recompiling the DLL whenever the version is supposed to be updated. Not only is that hard to script around in the windows c++ world, it would also take a lot of time and require to include the DLL binary in each commit, which should be avoided.

So I'd rather inject the version into the DLL some other way, preferably using existing mechanisms. The next best thing I can think of is XML defines. Is it possible to add an extra file containing XML defines? If possible I would like to render the entire file instead of regex manipulating one entry in the larger existing file. Or is there another useful mechanism of getting external definitions into the DLL?

As for how to obtain the version identifier. Like I mentioned in the original question, ideally it would be the hash of the commit that represents the current state of the code, updated with a commit hook - but that's not actually possible, because it's impossible to know the commit hash before the commit is created, and the file that is supposed to contain the commit hash needs to be included in the commit.

I think the next best thing is to have a shell script that just creates an autoincrementing version number (or just the current datetime), creates a corresponding tag containing the version number, and then templates it into the corresponding file. That wouldn't even need to happen on a commit hook, I think it's fine to get into the habit of running this script before pushing my code.

Anyway, I think that's already a rough outline of a plan to get this working, thank you.
 
Oh, using the game name would be a good solution, I think. I agree that appending to the player selected game name would not be too bad.
With this approach, come to think of it, you'll need to ensure that the version string gets appended only once. So, I guess, a string-ends-with check in CvInitCore::setGameName is needed – append only if that check fails. And rather than keep the original game name in a separate data member, one could just use string operations to discard the version part in CvDLLButtonPopup::launchAdminPopup – unless Game Details is really the best place for showing version information. (Most mods seem to show it in the hover text of the big flag button, i.e. CvDLLWidgetData::parseFlagHelp.)
What I don't like about this approach is it necessitates recompiling the DLL whenever the version is supposed to be updated.
Oh, right, not every change will involve the DLL. So my notion of a "build number" is also a little off-target.
Is it possible to add an extra file containing XML defines? If possible I would like to render the entire file instead of regex manipulating one entry in the larger existing file.
Should just be a matter of adding a couple of lines to CvXMLLoadUtility::SetGlobalDefines. Those files all use the GlobalDefines schema (Civ4GlobalDefinesSchema.xml).
Or is there another useful mechanism of getting external definitions into the DLL?
Only a Python call comes to mind. (I don't see an advantage over the XML approach.)
I think the next best thing is to have a shell script that just creates an autoincrementing version number (or just the current datetime), creates a corresponding tag containing the version number, and then templates it into the corresponding file. That wouldn't even need to happen on a commit hook, I think it's fine to get into the habit of running this script before pushing my code.
I see, the parent commit (current head) would be potentially ambiguous, and the version only needs to be updated before a push. But interesting to know, for me, that "githooks" would be the topic to look into for an automatic mechanism. (In fact, my local repos already have a sample file with a comment 'To enable this hook, rename this file to "pre-commit".' Nothing to be afraid of, it seems.)
 
Back
Top Bottom