configuring mod Options with a dedicated Mods tab

beezany

Trixie
Joined
Jan 4, 2009
Messages
731
Location
San Jose, CA
this is more of a worked example than a tutorial, but i hope that it will illustrate the option configuration process. the end result is a set of game Options on their own Mods tab (along with any other mod that uses the same technique).

more-mod-options.jpg


for illustration i'll use my Resource Re-sorts mod, which has two checkbox options and just a little code.

option configuration scripts​

first you'll need to add two scripts to your mod:
the first script is a variant of the ModSettings script that @leonardify created and several modders have been using to save config options. it stores the data in the game's persistent localStorage which requires cooperation between modders. this version adds a bit of code to create the new Mods tab on the Options screen. this script requires no modification, just drop it into your project.

if you are already using @leonardify's technique, all you need to do is add this snippet to your code and set your mod category to CategoryType.Mods:
JavaScript:
import { CategoryData, CategoryType } from '/core/ui/options/options-helpers.js';

CategoryType["Mods"] = "mods";
CategoryData[CategoryType.Mods] = {
    title: "LOC_UI_CONTENT_MGR_SUBTITLE",
    description: "LOC_UI_CONTENT_MGR_SUBTITLE_DESCRIPTION",
};

otherwise, you'll need to edit the other script to configure your options. i've linked to the version in Resource Re-sorts, which is of course tailored to that mod. you'll need to change everything with a bz or BZ prefix, including the filename, because those things are specific to my mods. here's some of the specific steps:
  • change MOD_ID = "bz-re-sorts" to your own mod-id
  • rename bzReSortsOptions to suit your mod
  • model your config options in the data object and update the various accessors & event callbacks to match them
  • change group: 'bz_mods' to something based on your name or your mod-id. use underscores here, not hyphens.
  • check for anything still containing bz or BZ because those are all specific to my mod
the group: name requires some explanation. the Option processor translates that into a locale tag "LOC_OPTIONS_GROUP_" + uppercase(group) used as a section title for your options. easiest thing to do here is set group to your mod-id with hyphens changed to underscores:
JavaScript:
group: MOD_ID.replace(/-/g, '_');
which turns my-mod into LOC_OPTIONS_GROUP_MY_MOD. my example mod uses a different value because i group all of my mods' options into a single section.

other option types​

the game's Options API supports several kinds of options:
  • Checkbox and Switch for boolean options
  • Slider and Stepper for numeric ranges
  • Dropdown for menu options
  • Editor for text
i've only used OptionType.Checkbox so i can't provide specific guidance on the others yet.

modinfo​

you'll need to add these to your modinfo in two places. for in-game options, add them to the same action group as the rest of your scripts. for the main menu, you'll also need to create a shell action group:
Code:
<ActionGroup id="MOD-ID-menu" scope="shell" criteria="always">
    <Properties>
        <LoadOrder>100</LoadOrder>
    </Properties>
    <Actions>
        <UIScripts>
            <Item>ui/options/MOD-ID-options.js</Item>
            <Item>ui/options/mod-options-decorator.js</Item>
        </UIScripts>
        <UpdateText>
            <Item>text/en_us/InGameText.xml</Item>
        </UpdateText>
    </Actions>
</ActionGroup>

once you set up the two scripts and update your modinfo, you should be able to see your options on both the main menu and in-game Options screens. you can refresh the in-game options using the debug console, but you have to restart the game to refresh the main menu.

using the options​

after that, all that's left is to hook up your options in your main scripts. import your settings object and use the properties you declared in the configuration script above. for example:
JavaScript:
import bzReSortsOptions from '/bz-re-sorts/ui/options/bz-re-sorts-options.js';

const settlementSort = (a, b) => {
    if (bzReSortsOptions.sortCitiesByType) {
        // first sort capital, city, town
        // ...
    }
    if (bzReSortsOptions.sortCitiesBySlots) {
        // then sort by total resource slots
        // ...
    }
    // then sort by name ...
};

ui/options/mod-options-decorator.js​

for reference, here is the entire mod-options-decorator.js script (no editing required):
JavaScript:
import { CategoryData, CategoryType } from '/core/ui/options/options-helpers.js';

CategoryType["Mods"] = "mods";
CategoryData[CategoryType.Mods] = {
    title: "LOC_UI_CONTENT_MGR_SUBTITLE",
    description: "LOC_UI_CONTENT_MGR_SUBTITLE_DESCRIPTION",
};

// fix Options tab spacing
const MOD_OPTIONS_STYLE = document.createElement('style');
MOD_OPTIONS_STYLE.textContent = `
.option-frame .tab-bar__items .flex-auto {
    flex: 1 0 auto;
    min-width: 0rem;
    margin-left: 0.4444444444rem;
    margin-right: 0.4444444444rem;
}`;
document.head.appendChild(MOD_OPTIONS_STYLE);

// Please, always use ModSettings to save and load settings in your mod.
// Right now if you try to use **multiple** keys in localStorage, it
// will break reading from localStorage for **every mod**. This is
// a workaround to avoid this issue, while keeping a namespace to give
// each mod its own settings.
export class ModSettingsSingleton {
    save(modID, data) {
        if (localStorage.length > 1) {
            console.warn(`ModSettings: erasing storage (${localStorage.length} items)`);
            localStorage.clear();
        }
        const modSettings = JSON.parse(localStorage.getItem("modSettings") || '{}');
        modSettings[modID] = data;
        localStorage.setItem("modSettings", JSON.stringify(modSettings));
        console.warn(`SAVE ${modID}=${JSON.stringify(data)}`);
    }
    load(modID) {
        try {
            const modSettings = localStorage.getItem("modSettings");
            if (!modSettings) return null;
            const data = modSettings && (JSON.parse(modSettings)[modID] ?? null);
            console.warn(`LOAD ${modID}=${JSON.stringify(data)}`);
            return data;
        }
        catch (e) {
            console.error(`ModSettings: error loading settings`);
            console.error(`${modID}: ${e}`);
        }
        return null;
    }
}
const ModSettings = new ModSettingsSingleton();
export { ModSettings as default };
 
Last edited:
this would probably be better with an empty template mod instead of using a real mod as an example! i probably won't get to that for a while though, so if anybody else wants to tackle it, please feel free.

(be especially careful about editing all the bz references if you use my code as a template, especially in the import statements. i am releasing a hotfix today because i goofed that in one of my own mods. i didn't notice because the import still works when you have both mods enabled.)
 
Last edited:
That Trixie's Mod caused me a bit of confusion. I see from your signature where Trixie's Mods came from. I searched for a while trying to figure out where that came from when it showed up in game until I finally noticed a tooltip on "one click repairs" referencing City Hall. It seems, at least, you should put out a mod pack by that name since searching downloads didn't find it either. I suggested a super category of basically global and mod specific options, but I guess mod packs would make for a third. It seems transitioning would be easy enough by adding optional parms that default to global and when specified moves it to a new category. I don't know if any of yours apply across mods, but it's conceivable a mod pack might configure the mods it includes. Overall, it would be modder consensus but there seems a lot of potential to baffle and confuse the user.
 
That Trixie's Mod caused me a bit of confusion. I see from your signature where Trixie's Mods came from. I searched for a while trying to figure out where that came from when it showed up in game until I finally noticed a tooltip on "one click repairs" referencing City Hall. It seems, at least, you should put out a mod pack by that name since searching downloads didn't find it either. I suggested a super category of basically global and mod specific options, but I guess mod packs would make for a third. It seems transitioning would be easy enough by adding optional parms that default to global and when specified moves it to a new category. I don't know if any of yours apply across mods, but it's conceivable a mod pack might configure the mods it includes. Overall, it would be modder consensus but there seems a lot of potential to baffle and confuse the user.
thanks for your feedback about recognition and searchability – i'll need to adjust my option text so that the connection to my mods is more obvious.

i'm not sure what you're talking about here with "transitioning" and "moving to a new category." it sounds over-complicated. each Option specifies its category, group heading, text, and tooltip. the category determines which tab the option goes on, in this case CategoryType.Mods. the group heading is literally just a text heading that the game displays above related options, automatically grouping all options with the same heading. i chose trixie's mods for all of my options across all mods, because i didn't want to scatter a handful of options across three headings, like in the screenshot below. btw hi, i'm trixie, and i make UI mods.

if you have more questions or concerns about my mods, please use their discussion threads for feedback. this thread is a tutorial for implementing persistent mod options, aimed primarily at modders who want to add options to their mods, or who want to move their existing Options to a dedicated "Mods" tab.

mod-config.jpg
 
Last edited:
you might notice that the Options bar gets a bit crowded with the extra Mods tab, and it's worse with some languages, as this user illustrates with the Russian interface. i'm pretty sure that the base game uses a flex box to organize the space here, and it doesn't distribute the space evenly between the columns. if so, that's a pretty easy thing to fix with some extra CSS styling. there are potential complications in getting that to work with multiple independent mods, but once i figure that out i'll add it to the snippet that creates the new tab.

1742089196268.png
 
Can categories contain categories? Like having a "Trixie's Mods" category then subcategories for the individual mods?
 
Can categories contain categories? Like having a "Trixie's Mods" category then subcategories for the individual mods?
there are only two levels: category and group. each category has its own tab, and each group has its own heading. Mods is a group with CategoryType.Mods and internal id "mods". trixie's mods is a group with internal ID "bz_mods" and heading text defined by the LOC_OPTIONS_GROUP_BZ_MODS locale tag. that's all the organization that the standard Options menu provides. also, this organization is strictly cosmetic and has no effect on how the options behave or which mods they affect. it's solely a way for the game to present configuration options to the player.
 
After I posted here I saw a reply by Mattifus saying you can have subcategories but have to use the combobox option. Perhaps I jumped to a invalid assumption that this is about how to use his mod. Perhaps I'm totally wrong about what his mod does for that matter. I assume his mod is what is taking this data, creating the mod tab on options and displaying the options so people can set them. Is that correct?
 
After I posted here I saw a reply by Mattifus saying you can have subcategories but have to use the combobox option. Perhaps I jumped to a invalid assumption that this is about how to use his mod. Perhaps I'm totally wrong about what his mod does for that matter. I assume his mod is what is taking this data, creating the mod tab on options and displaying the options so people can set them. Is that correct?
this has nothing to do with his mod. this is a technique for configuring mod options that i developed independently. it's a simple bit of code that anybody can drop into their own mods. i shared it here so that other modders can also use it, without any dependency on me or my mods or anything else.

i started developing this idea before Mattifus published his mod, and it's completely independent from his work. despite that, he accused me of plagiarism. i already told you this and said that i would not discuss him or his work further, so why you would assume that i'm talking about it?

this thread is primarily for developers who want to use my technique. i don't mind more general questions about usage or future directions, but you are derailing the thread, and i'm extremely upset that you are conflating my work with somebody else who tried to take credit for it.
 
Last edited:
the tabs on the Options screen are oddly spaced, and it looked especially bad with the new Mods tab. the root cause was a min-width rule that was making the smaller tabs take up too much room proportionally, exacerbated by suboptimal flex settings. i removed the minimum width and replaced it with modest margins that do a better job of keeping the tabs separate. i also adjusted the flex layout to distribute the remaining space more evenly.

JavaScript:
// fix Options tab spacing
const MOD_OPTIONS_STYLE = document.createElement('style');
MOD_OPTIONS_STYLE.textContent = `
.option-frame .tab-bar__items .flex {
    justify-content: space-evenly;
    align-items: center;
}
.option-frame .tab-bar__items .flex-auto {
    flex: 1 0 auto;
    min-width: 0rem;
    margin-left: 0.4444444444rem;
    margin-right: 0.4444444444rem;
}`;
document.head.appendChild(MOD_OPTIONS_STYLE);

multiple mods can use this snippet without conflict, and it works great alongside the Category definition from earlier.

mod-options-fixed.jpg
 
My apologies, just tunnel vision. I just got locked on the idea this was tutorial to use that mod. The color syntax highlighting on this site is making the code unreadable. Once I copied the code out to Notepad++ it became apparent this is stand alone. I should think more, post less.
 
Thanks for this tutorial! I just went through and looked at how to utilize dropdown boxes. Here's what I came up with:

JavaScript:
const onInitBuildingInfo = (info) => {
    if (!info.dropdownItems)
        return;
    for (let i = 0; i < info.dropdownItems.length; i++) {
        const item = info.dropdownItems[i];
        if (item.value == wondersScreenOptions.BuildingInformationType) {
            info.selectedItemIndex = i;
            break;
        }
    }
}
const onUpdateBuildingInfo = (info, selectedIndex) => {
    const selectedItem = info.dropdownItems[selectedIndex];
    wondersScreenOptions.BuildingInformationType = selectedItem.value;
}

const buildingInfoOptions = [
    { label: 'LOC_OPTIONS_KAYLEER_WONDERS_BUILDING_INFO_ALL', value: WondersModsBuildingInformationType.ALL },
    { label: 'LOC_OPTIONS_KAYLEER_WONDERS_BUILDING_INFO_EXCLUDE_UNMET', value: WondersModsBuildingInformationType.EXCLUDE_UNMET },
    { label: 'LOC_OPTIONS_KAYLEER_WONDERS_BUILDING_INFO_EXCLUDE_NON_VISIBLE', value: WondersModsBuildingInformationType.EXCLUDE_NON_VISIBLE },
]

Options.addInitCallback(() => {
    Options.addOption({
        category: CategoryType.Mods,
        // @ts-ignore
        group: 'kayleeR_wonder_mods',
        type: OptionType.Dropdown,
        id: "kayleeR-wonder-mods-building-information-type",
        initListener: onInitBuildingInfo,
        updateListener: onUpdateBuildingInfo,
        label: "LOC_OPTIONS_KAYLEER_WONDERS_BUILDING_INFORMATION_TYPE",
        description: "LOC_OPTIONS_KAYLEER_WONDERS_BUILDING_INFORMATION_TYPE_DESCRIPTION",
        dropdownItems: buildingInfoOptions
    });
});

The important field on the dropdownItems is label which is the displayed text string in the dropdown box. You can then add fields on top of that to reference. In this case I just used value to set the value I wanted to get for each option. The updateListener also passes in a selectedIndex which you'll need to use to reference the dropdownItems. On the initialize you'll need to iterate over the dropdownItems to find the value you want to set as the selected index. This will also change how you're storing options data instead of booleans for checkboxes. I went with defined strings that I used for the dropdown options.

If you want to use other elements look at onUpdateOptionValue in ui/options/screen-options.js:180. Find the case for the element type you want to use and take a look at when it's calling option.updateListener to see what parameters are being passed. It looks like everything but dropdown passes option and value though. Dropdown is the odd one out with selectedIndex being used. All of the init listeners just pass in the option item. Take a look at createOptionComponentInternal in ui/options/options-helpers.js:20 to see what values are used for initializing the elements. I believe you might need to set these in the Options.addOption call instead of in the init listener though.
 
Thank you so much for the well-crafted tutorial! As a beginner, I was able to easily complete the configuration by following your steps and immediately apply it to my mod.
The mod community continues to grow and thrive because of contributors like you. I sincerely look forward to more amazing content from you!
=========================
真心感谢您精心制作的教程!作为新手,我依照您的步骤,轻松就完成了配置,还能立刻将其应用到我的mod中。
mod社区正因为有您这样的贡献者,才得以不断发展壮大,真心期待您分享更多精彩内容 。
 
i am making a small change to the template to remove this CSS rule:
CSS:
.option-frame .tab-bar__items .flex {
    justify-content: space-evenly;
    align-items: center;
}
@thecrazyscot noticed it was generating log messages like this:
Unable to parse declaration: justify-content - space-evenly

the layout engine apparently doesn't support that option, so i was getting the game's base formatting instead. i tested some alternatives and realized that entire CSS rule was redundant, so i'm removing it from the template. (the second CSS rule is still necessary to fix the tab spacing.)

@KayleeR and @F1rstDan: i see the same log message coming from your copies of the template. it's not an urgent problem, and you don't need to make a hotfix for this, but i recommend updating your copies of the template when it's convenient.
 
i am making a small change to the template to remove this CSS rule:
CSS:
.option-frame .tab-bar__items .flex {
    justify-content: space-evenly;
    align-items: center;
}
@thecrazyscot noticed it was generating log messages like this:


the layout engine apparently doesn't support that option, so i was getting the game's base formatting instead. i tested some alternatives and realized that entire CSS rule was redundant, so i'm removing it from the template. (the second CSS rule is still necessary to fix the tab spacing.)

@KayleeR and @F1rstDan: i see the same log message coming from your copies of the template. it's not an urgent problem, and you don't need to make a hotfix for this, but i recommend updating your copies of the template when it's convenient.
Thank you @beezany and @thecrazyscot for the special reminder, thank you!

感谢 @beezany@thecrazyscot 特意提醒,感恩!

 
version 1.1.1 changed the name of the Mods menu to Add-ons which is the same LOC_UI_CONTENT_MGR_SUBTITLE string we are currently using for the new options tab. we could change to a different LOC that still has the "Mods" text, but honestly i think it's better if we keep the tab name consistent with the new menu name. main thing is that we should all use the same LOC across mods, or users will randomly get different text depending on mod load order.
 
i searched the LOC data to find any other translations for "Mods" and the only candidate is LOC_UI_MP_HEADER_MODS which is related to the multiplayer staging screen (mp-staging-new.html). it has a couple of drawbacks.

first, the tag is orphaned. it's part of a set: LOC_UI_MP_HEADER_PLAYER, TEAM, CIV, LEADER, READY, KICK, RULES, and MODS, but the last two aren't actually used in the interface. that means that we can't count on it to be stable, and if Firaxis does start using the string they're likely to change the wording to match the Add-ons menu.

second, the tag doesn't have a matching tooltip like the current one does. that's less important, because the tab interface doesn't seem to use the tooltip anyway. we can continue to use LOC_UI_CONTENT_MGR_SUBTITLE_DESCRIPTION in any case. if the tooltip does show up somewhere, that text is good enough.

we could create our own LOC text based on LOC_UI_MP_HEADER_MODS and its translations, which would avoid the problems of using an orphaned tag, but then we'd all need to provide all of the translations in our own mods, which has a lot of practical problems. it's much better if we reuse an existing tag from the base game.

i don't have a strong opinion about LOC_UI_CONTENT_MGR_SUBTITLE (Add-ons) versus LOC_UI_MP_HEADER_MODS (Mods). as i said before, the most important thing is that we keep the text consistent between our mods. i slightly prefer leaving things as they are now (Add-ons) because it requires no extra work or coordination, and i think players will eventually expect the Options menu to match the main menu. that said, i've already seen a couple of players annoyed or confused by the main menu change, and they've been quite vocal about wanting "Mods" back.

if you have any strong opinions one way or the other, please share them here!
 
Back
Top Bottom