├── .gitignore ├── ModConfigMenu ├── ModConfigMenu │ ├── PublishedFileId.ID │ ├── Config │ │ ├── XComEditor.ini │ │ ├── XComModConfigMenuTest.ini │ │ ├── XComEngine.ini │ │ └── XComModConfigMenu.ini │ ├── Src │ │ ├── ModConfigMenuAPI │ │ │ ├── Classes │ │ │ │ ├── MCM_API_Label.uc │ │ │ │ ├── MCM_API_Button.uc │ │ │ │ ├── MCM_API_Checkbox.uc │ │ │ │ ├── MCM_API_Spinner.uc │ │ │ │ ├── MCM_API_Dropdown.uc │ │ │ │ ├── MCM_API_Slider.uc │ │ │ │ ├── MCM_API.uc │ │ │ │ ├── MCM_API_Instance.uc │ │ │ │ ├── MCM_API_Setting.uc │ │ │ │ ├── MCM_API_SettingsPage.uc │ │ │ │ └── MCM_API_SettingsGroup.uc │ │ │ ├── MCM_API_CfgHelpers.uci │ │ │ └── MCM_API_Includes.uci │ │ └── ModConfigMenu │ │ │ └── Classes │ │ │ ├── MCM_SettingFacade.uc │ │ │ ├── MCM_TestConfigStore.uc │ │ │ ├── MCM_OptionsMenuListenerHook.uc │ │ │ ├── MCM_Label.uc │ │ │ ├── MCM_XCOM2_UISlider.uc │ │ │ ├── MCM_TestHarnessListener.uc │ │ │ ├── MCM_CustomPageTestUI.uc │ │ │ ├── MCM_CustomPageTest.uc │ │ │ ├── MCM_GroupLabel.uc │ │ │ ├── MCM_Button.uc │ │ │ ├── MCM_SettingsTab.uc │ │ │ ├── MCM_GroupLabelSeparator.uc │ │ │ ├── MCM_Checkbox.uc │ │ │ ├── MCM_UISettingSeparator.uc │ │ │ ├── MCM_OptionsMenuListener.uc │ │ │ ├── MCM_SettingBase.uc │ │ │ ├── MCM_LabelFacade.uc │ │ │ ├── MCM_ButtonFacade.uc │ │ │ ├── MCM_Spinner.uc │ │ │ ├── MCM_CheckboxFacade.uc │ │ │ ├── MCM_Dropdown.uc │ │ │ ├── MCM_SettingsPanelFacade.uc │ │ │ ├── MCM_SpinnerFacade.uc │ │ │ ├── MCM_DropdownFacade.uc │ │ │ ├── MCM_SliderFacade.uc │ │ │ ├── MCM_SettingGroup.uc │ │ │ ├── MCM_SettingsPanel.uc │ │ │ ├── MCM_TestHarness.uc │ │ │ ├── MCM_Slider.uc │ │ │ └── MCM_OptionsScreen.uc │ ├── ModPreview.jpg │ ├── Content │ │ └── MCM.upk │ ├── ReadMe.txt │ ├── Localization │ │ └── ModConfigMenu.int │ └── ModConfigMenu.x2proj └── ModConfigMenu.XCOM_sln ├── MCM_API.zip ├── Res ├── screen1.jpg ├── screen2.jpg ├── gfx │ ├── Transparent.png │ ├── MainBackground.png │ ├── GroupLabelSeparator.png │ ├── MainHeaderSeparator.png │ ├── MainVerticalSeparator.png │ ├── MainBackground_Stretched.png │ ├── GroupLabelSeparator_Stretched.png │ └── MainHeaderSeparator_Stretched.png └── MCM_MainBackground.psd ├── ModConfigDependencyTest ├── ModConfigDependencyTest │ ├── ReadMe.txt │ ├── Config │ │ ├── XComEditor.ini │ │ ├── XComMCDT_ConfigSrc.ini │ │ └── XComEngine.ini │ ├── Src │ │ ├── ModConfigMenuAPI │ │ │ ├── Classes │ │ │ │ ├── MCM_API_Label.uc │ │ │ │ ├── MCM_API_Button.uc │ │ │ │ ├── MCM_API_Checkbox.uc │ │ │ │ ├── MCM_API_Spinner.uc │ │ │ │ ├── MCM_API_Dropdown.uc │ │ │ │ ├── MCM_API_Slider.uc │ │ │ │ ├── MCM_API.uc │ │ │ │ ├── MCM_API_Instance.uc │ │ │ │ ├── MCM_API_Setting.uc │ │ │ │ ├── MCM_API_SettingsPage.uc │ │ │ │ └── MCM_API_SettingsGroup.uc │ │ │ ├── MCM_API_CfgHelpers.uci │ │ │ └── MCM_API_Includes.uci │ │ └── ModConfigDependencyTest │ │ │ └── Classes │ │ │ ├── GameStateExample.uc │ │ │ ├── MCDT_ConfigSrc.uc │ │ │ ├── UIStub.uc │ │ │ ├── UIExampleListener.uc │ │ │ ├── GameStateExampleListener.uc │ │ │ ├── GameStateInitializer.uc │ │ │ └── UIExample.uc │ ├── ModPreview.jpg │ └── ModConfigDependencyTest.x2proj ├── ModConfigDependencyTest.v12.XCOM_suo └── ModConfigDependencyTest.XCOM_sln ├── documentation ├── img │ ├── addfiles1.png │ ├── addfiles2.png │ ├── addfiles3.png │ ├── engineini.png │ ├── newproject.png │ ├── pastefiles.png │ └── mcmapifiles.png ├── apidoc.md ├── intro.md ├── sharedcode.md ├── config.md ├── advanced.md └── tutorial.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.XCOM_suo -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/PublishedFileId.ID: -------------------------------------------------------------------------------- 1 | 667104300 2 | -------------------------------------------------------------------------------- /MCM_API.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/MCM_API.zip -------------------------------------------------------------------------------- /Res/screen1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/screen1.jpg -------------------------------------------------------------------------------- /Res/screen2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/screen2.jpg -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/ReadMe.txt: -------------------------------------------------------------------------------- 1 | You created an XCOM 2 Mod Project! -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Config/XComEditor.ini: -------------------------------------------------------------------------------- 1 | [ModPackages] 2 | +ModPackages=ModConfigMenu 3 | -------------------------------------------------------------------------------- /Res/gfx/Transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/gfx/Transparent.png -------------------------------------------------------------------------------- /Res/MCM_MainBackground.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/MCM_MainBackground.psd -------------------------------------------------------------------------------- /Res/gfx/MainBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/gfx/MainBackground.png -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_Label.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Label extends MCM_API_Setting; -------------------------------------------------------------------------------- /Res/gfx/GroupLabelSeparator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/gfx/GroupLabelSeparator.png -------------------------------------------------------------------------------- /Res/gfx/MainHeaderSeparator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/gfx/MainHeaderSeparator.png -------------------------------------------------------------------------------- /documentation/img/addfiles1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/documentation/img/addfiles1.png -------------------------------------------------------------------------------- /documentation/img/addfiles2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/documentation/img/addfiles2.png -------------------------------------------------------------------------------- /documentation/img/addfiles3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/documentation/img/addfiles3.png -------------------------------------------------------------------------------- /documentation/img/engineini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/documentation/img/engineini.png -------------------------------------------------------------------------------- /documentation/img/newproject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/documentation/img/newproject.png -------------------------------------------------------------------------------- /documentation/img/pastefiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/documentation/img/pastefiles.png -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Config/XComEditor.ini: -------------------------------------------------------------------------------- 1 | [ModPackages] 2 | +ModPackages=ModConfigDependencyTest -------------------------------------------------------------------------------- /Res/gfx/MainVerticalSeparator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/gfx/MainVerticalSeparator.png -------------------------------------------------------------------------------- /documentation/img/mcmapifiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/documentation/img/mcmapifiles.png -------------------------------------------------------------------------------- /Res/gfx/MainBackground_Stretched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/gfx/MainBackground_Stretched.png -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_Label.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Label extends MCM_API_Setting; -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/ModPreview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/ModConfigMenu/ModConfigMenu/ModPreview.jpg -------------------------------------------------------------------------------- /Res/gfx/GroupLabelSeparator_Stretched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/gfx/GroupLabelSeparator_Stretched.png -------------------------------------------------------------------------------- /Res/gfx/MainHeaderSeparator_Stretched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/Res/gfx/MainHeaderSeparator_Stretched.png -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Content/MCM.upk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/ModConfigMenu/ModConfigMenu/Content/MCM.upk -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/ReadMe.txt: -------------------------------------------------------------------------------- 1 | For usage and installation instructions, visit the project homepage at https://github.com/andrewgu/ModConfigMenu -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_Button.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Button extends MCM_API_Setting; 2 | 3 | function SimulateClick(); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest.v12.XCOM_suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/ModConfigDependencyTest/ModConfigDependencyTest.v12.XCOM_suo -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/ModPreview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewgu/ModConfigMenu/HEAD/ModConfigDependencyTest/ModConfigDependencyTest/ModPreview.jpg -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_Button.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Button extends MCM_API_Setting; 2 | 3 | function SimulateClick(); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigDependencyTest/Classes/GameStateExample.uc: -------------------------------------------------------------------------------- 1 | class GameStateExample extends object config(MCDT_Settings); 2 | 3 | function OnInit(MCM_API Api) 4 | { 5 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_Checkbox.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Checkbox extends MCM_API_Setting; 2 | 3 | function bool GetValue(); 4 | function SetValue(bool Checked, bool SuppressEvent); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Config/XComModConfigMenuTest.ini: -------------------------------------------------------------------------------- 1 | [ModConfigMenu.MCM_TestConfigStore] 2 | ENABLE_TEST_HARNESS=false 3 | CLICKED=false 4 | CHECKBOX=false 5 | SLIDER=0 6 | DROPDOWN="a" 7 | SPINNER="a" 8 | VERSION=2 -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_Checkbox.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Checkbox extends MCM_API_Setting; 2 | 3 | function bool GetValue(); 4 | function SetValue(bool Checked, bool SuppressEvent); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_SettingFacade.uc: -------------------------------------------------------------------------------- 1 | interface MCM_SettingFacade extends MCM_API_Setting; 2 | 3 | function UIMechaListItem InstantiateUI(UIList Parent); 4 | 5 | function AfterParentPageDisplayed(); 6 | 7 | function TriggerSaveEvent(); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_Spinner.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Spinner extends MCM_API_Setting; 2 | 3 | function string GetValue(); 4 | function SetValue(string Selection, bool SuppressEvent); 5 | function SetOptions(array NewOptions, string InitialSelection, bool SuppressEvent); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_Dropdown.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Dropdown extends MCM_API_Setting; 2 | 3 | function string GetValue(); 4 | function SetValue(string Selection, bool SuppressEvent); 5 | function SetOptions(array NewOptions, string InitialSelection, bool SuppressEvent); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Config/XComMCDT_ConfigSrc.ini: -------------------------------------------------------------------------------- 1 | [ModConfigDependencyTest.MCDT_ConfigSrc] 2 | CLICKED=false 3 | CHECKBOX=true 4 | SLIDER=100 5 | DROPDOWN="a" 6 | SPINNER="a" 7 | VERSION=1 8 | 9 | [ModConfigDependencyTest.GameStateInitializer] 10 | DEFAULT_CHECKED=false 11 | DEFAULT_SLIDER=50 -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_Spinner.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Spinner extends MCM_API_Setting; 2 | 3 | function string GetValue(); 4 | function SetValue(string Selection, bool SuppressEvent); 5 | function SetOptions(array NewOptions, string InitialSelection, bool SuppressEvent); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_Dropdown.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Dropdown extends MCM_API_Setting; 2 | 3 | function string GetValue(); 4 | function SetValue(string Selection, bool SuppressEvent); 5 | function SetOptions(array NewOptions, string InitialSelection, bool SuppressEvent); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Config/XComEngine.ini: -------------------------------------------------------------------------------- 1 | [UnrealEd.EditorEngine] 2 | +EditPackages=ModConfigMenuAPI 3 | 4 | [Engine.ScriptPackages] 5 | +NonNativePackages=ModConfigDependencyTest 6 | 7 | [Engine.Engine] 8 | ;+ModClassOverrides=(BaseGameClass="XComTacticalInput", ModClass="XComTacticalInput_LW") 9 | 10 | -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigDependencyTest/Classes/MCDT_ConfigSrc.uc: -------------------------------------------------------------------------------- 1 | class MCDT_ConfigSrc extends Object config(MCDT_ConfigSrc); 2 | 3 | var config bool CLICKED; 4 | var config bool CHECKBOX; 5 | var config float SLIDER; 6 | var config string DROPDOWN; 7 | var config string SPINNER; 8 | 9 | var config int VERSION; -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Config/XComEngine.ini: -------------------------------------------------------------------------------- 1 | [UnrealEd.EditorEngine] 2 | +EditPackages=ModConfigMenuAPI 3 | 4 | [Engine.ScriptPackages] 5 | +NonNativePackages=ModConfigMenu 6 | ;+NonNativePackages=ModConfigMenuAPI 7 | 8 | [Engine.Engine] 9 | ;+ModClassOverrides=(BaseGameClass="XComTacticalInput", ModClass="XComTacticalInput_LW") 10 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Localization/ModConfigMenu.int: -------------------------------------------------------------------------------- 1 | [MCM_OptionsMenuListener] 2 | m_strModMenuButton="MOD SETTINGS" 3 | 4 | [MCM_OptionsScreen] 5 | m_strSaveAndExit="Save and Exit" 6 | m_strCancel="Cancel" 7 | m_strTitle="Mod Settings" 8 | m_strSubtitle="" 9 | 10 | [MCM_SettingsPanel] 11 | m_strResetButton="Reset" 12 | m_strRevertButton="Discard" 13 | m_strApplyButton="Save" -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigDependencyTest/Classes/UIStub.uc: -------------------------------------------------------------------------------- 1 | // Ignore this, used for testing. 2 | class UIStub extends UIScreenListener; 3 | 4 | event OnInit(UIScreen Screen) 5 | { 6 | if (MCM_API(Screen) != none) 7 | { 8 | `log("MCDT: Dependency Stub Triggered."); 9 | } 10 | } 11 | 12 | defaultproperties 13 | { 14 | ScreenClass = none; 15 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_Slider.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Slider extends MCM_API_Setting; 2 | 3 | delegate string SliderValueDisplayFilter(float value); 4 | 5 | function float GetValue(); 6 | function SetValue(float Value, bool SuppressEvent); 7 | function SetBounds(float min, float max, float step, float newValue, bool SuppressEvent); 8 | 9 | function SetValueDisplayFilter(delegate Filter); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_Slider.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Slider extends MCM_API_Setting; 2 | 3 | delegate string SliderValueDisplayFilter(float value); 4 | 5 | function float GetValue(); 6 | function SetValue(float Value, bool SuppressEvent); 7 | function SetBounds(float min, float max, float step, float newValue, bool SuppressEvent); 8 | 9 | function SetValueDisplayFilter(delegate Filter); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_TestConfigStore.uc: -------------------------------------------------------------------------------- 1 | // Stores settings for MCM_TestHarness in order to persist across game runs. 2 | class MCM_TestConfigStore extends Object config(ModConfigMenuTest); 3 | 4 | var config bool ENABLE_TEST_HARNESS; 5 | 6 | var config bool CLICKED; 7 | var config bool CHECKBOX; 8 | var config float SLIDER; 9 | var config string DROPDOWN; 10 | var config string SPINNER; 11 | 12 | var config int VERSION; 13 | -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigDependencyTest/Classes/UIExampleListener.uc: -------------------------------------------------------------------------------- 1 | class UIExampleListener extends UIScreenListener; 2 | 3 | event OnInit(UIScreen Screen) 4 | { 5 | local UIExample example; 6 | 7 | if (MCM_API(Screen) != none) 8 | { 9 | example = new class'UIExample'; 10 | example.OnInit(Screen); 11 | } 12 | } 13 | 14 | defaultproperties 15 | { 16 | // The class you're listening for doesn't exist in this project, so you can't listen for it directly. 17 | ScreenClass = none; 18 | } -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigDependencyTest/Classes/GameStateExampleListener.uc: -------------------------------------------------------------------------------- 1 | class GameStateExampleListener extends UIScreenListener; 2 | 3 | event OnInit(UIScreen Screen) 4 | { 5 | local GameStateExample example; 6 | 7 | if (MCM_API(Screen) != none) 8 | { 9 | example = new class'GameStateExample'; 10 | example.OnInit(MCM_API(Screen)); 11 | } 12 | } 13 | 14 | defaultproperties 15 | { 16 | // The class you're listening for doesn't exist in this project, so you can't listen for it directly. 17 | ScreenClass = none; 18 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API; 2 | 3 | enum eGameMode 4 | { 5 | eGameMode_MainMenu, 6 | eGameMode_Strategy, 7 | eGameMode_Tactical, 8 | eGameMode_Multiplayer, 9 | eGameMode_Unknown 10 | }; 11 | 12 | // Using an int means that we don't have to redefine eGameMode if we add more possible future values. 13 | delegate ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode); 14 | 15 | // major and minor versions required in order to enforce compatibility constraints. 16 | function bool RegisterClientMod(int major, int minor, delegate SetupHandler); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API; 2 | 3 | enum eGameMode 4 | { 5 | eGameMode_MainMenu, 6 | eGameMode_Strategy, 7 | eGameMode_Tactical, 8 | eGameMode_Multiplayer, 9 | eGameMode_Unknown 10 | }; 11 | 12 | // Using an int means that we don't have to redefine eGameMode if we add more possible future values. 13 | delegate ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode); 14 | 15 | // major and minor versions required in order to enforce compatibility constraints. 16 | function bool RegisterClientMod(int major, int minor, delegate SetupHandler); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigDependencyTest/Classes/GameStateInitializer.uc: -------------------------------------------------------------------------------- 1 | class GameStateInitializer extends X2DownloadableContentInfo config (MCDT_ConfigSrc); 2 | 3 | var config int ExampleSettingDefault; 4 | 5 | static event InstallNewCampaign(XComGameState StartState) 6 | { 7 | /*local XComGameState_CampaignSettings CampaignSettings; 8 | 9 | if () 10 | { 11 | // Inject Bronzeman. 12 | foreach StartState.IterateByClassType(class'XComGameState_CampaignSettings', CampaignSettings) 13 | { 14 | // Magic! 15 | CampaignSettings.AddSecondWaveOption('BronzeMan_1744'); 16 | break; 17 | } 18 | }*/ 19 | } -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/MCM_API_CfgHelpers.uci: -------------------------------------------------------------------------------- 1 | // Contains helper functions for doing proper user-configurable save data. See documentation for patterns on how to do it. 2 | 3 | `define MCM_CH_VersionChecker(SrcVer,TrgVer)\ 4 | function bool MCM_CH_IMPL_VersionChecker()\ 5 | {\ 6 | return (`{TrgVer} < `{SrcVer});\ 7 | }\ 8 | function int MCM_CH_IMPL_VersionMax()\ 9 | {\ 10 | return (MCM_CH_IMPL_VersionChecker() ? (`{SrcVer}) : (`{TrgVer}));\ 11 | } 12 | 13 | `define MCM_CH_GetValue(SrcVal, TrgVal)\ 14 | ((MCM_CH_IMPL_VersionChecker()) ? (`{SrcVal}) : (`{TrgVal})) 15 | 16 | `define MCM_CH_GetCompositeVersion()\ 17 | MCM_CH_IMPL_VersionMax() 18 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Config/XComModConfigMenu.ini: -------------------------------------------------------------------------------- 1 | [ModConfigMenu.MCM_OptionsMenuListener] 2 | ENABLE_MENU=True 3 | USE_FLAT_DISPLAY_STYLE=False 4 | 5 | [ModConfigMenu.MCM_OptionsScreen] 6 | PANEL_X=100 7 | PANEL_Y=100 8 | TABLIST_WIDTH=240 9 | TABS_LIST_TOP_PADDING=0 10 | OPTIONS_HEIGHT=700 11 | OPTIONS_WIDTH=580 12 | OPTIONS_MARGIN=10 13 | HEADER_HEIGHT=80 14 | FOOTER_HEIGHT=50 15 | API_MAJOR_VERSION=1 16 | API_MINOR_VERSION=0 17 | SHOW_SOLDIER=False 18 | 19 | [ModConfigMenu.MCM_SettingsPanel] 20 | ;OPTIONS_HEIGHT 21 | PANEL_HEIGHT=700 22 | ; Equals OPTIONS_WIDTH - 2*OPTIONS_MARGIN, 23 | PANEL_WIDTH=540 24 | FOOTER_HEIGHT=40 25 | ;APPLY_BUTTON_X=360 26 | ;REVERT_BUTTON_X=190 27 | ;RESET_BUTTON_X = 20 28 | RESET_BUTTON_X = 380 -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_OptionsMenuListenerHook.uc: -------------------------------------------------------------------------------- 1 | class MCM_OptionsMenuListenerHook extends UIScreenListener; 2 | 3 | event OnInit(UIScreen Screen) 4 | { 5 | local MCM_OptionsMenuListener listener; 6 | 7 | //`log("Mod Options Hook."); 8 | 9 | if (UIOptionsPCSCreen(Screen) != none) 10 | { 11 | // By using a transient class instance to execute the actual code, 12 | // we prevent the "listener code" from accidentally attaching UI objects 13 | // to a UIScreenListener object. 14 | listener = new class'MCM_OptionsMenuListener'; 15 | listener.OnInit(UIOptionsPCSCreen(Screen)); 16 | } 17 | } 18 | 19 | defaultproperties 20 | { 21 | ScreenClass = none; 22 | //ScreenClass = 'UIOptionsPCSCreen'; 23 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_Label.uc: -------------------------------------------------------------------------------- 1 | class MCM_Label extends MCM_SettingBase implements(MCM_API_Label) config(ModConfigMenu); 2 | 3 | simulated function MCM_SettingBase InitSettingsItem(name _Name, eSettingType _Type, optional string _Label = "", optional string _Tooltip = "") 4 | { 5 | `log("Don't call InitSettingsItem directly in subclass of MCM_SettingBase."); 6 | 7 | return none; 8 | } 9 | 10 | // Fancy init process 11 | simulated function MCM_Label InitLabel(name _SettingName, MCM_API_Setting _ParentFacade, string _Label, string _Tooltip) 12 | { 13 | super.InitSettingsItem(_SettingName, eSettingType_Label, _Label, _Tooltip); 14 | 15 | UpdateDataDescription(_Label); 16 | SetHoverTooltip(_Tooltip); 17 | 18 | return self; 19 | } 20 | 21 | // No special methods in MCM_API_Label. -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_XCOM2_UISlider.uc: -------------------------------------------------------------------------------- 1 | // Replacement UISlider to fix the SetStepSize() function from mucking with position, 2 | // and the 1% minimum value. 3 | class MCM_XCOM2_UISlider extends UISlider; 4 | 5 | var bool PercentHasBeenSet; 6 | 7 | simulated function UISlider SetPercent(float newValue) 8 | { 9 | if (newValue < 0) newValue = 0; 10 | else if (newValue > 100) newValue = 100; 11 | 12 | if (percent != newValue || !PercentHasBeenSet) 13 | { 14 | PercentHasBeenSet = true; 15 | percent = newValue; 16 | mc.FunctionNum("setValue", newValue); 17 | } 18 | return self; 19 | } 20 | 21 | simulated function UISlider SetStepSize(float newValue) 22 | { 23 | if (stepSize != newValue) 24 | { 25 | stepSize = newValue; 26 | //mc.FunctionNum("setValue", newValue); // Is there a flash function for setting step size? 27 | } 28 | return self; 29 | } -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest.XCOM_sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # XCOM ModBuddy Solution File, Format Version 11.00 4 | VisualStudioVersion = 12.0.21005.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{5DAE07AF-E217-45C1-8DE7-FF99D6011E8A}") = "ModConfigDependencyTest", "ModConfigDependencyTest\ModConfigDependencyTest.x2proj", "{3F350978-33E7-493A-993B-92C1B54BF591}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Default|XCOM 2 = Default|XCOM 2 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {3F350978-33E7-493A-993B-92C1B54BF591}.Default|XCOM 2.ActiveCfg = Default|XCOM 2 14 | {3F350978-33E7-493A-993B-92C1B54BF591}.Default|XCOM 2.Build.0 = Default|XCOM 2 15 | EndGlobalSection 16 | GlobalSection(SolutionProperties) = preSolution 17 | HideSolutionNode = FALSE 18 | EndGlobalSection 19 | EndGlobal 20 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_TestHarnessListener.uc: -------------------------------------------------------------------------------- 1 | class MCM_TestHarnessListener extends UIScreenListener config(ModConfigMenuTestHarness); 2 | 3 | event OnInit(UIScreen Screen) 4 | { 5 | local MCM_TestHarness RegularTestHarness; 6 | local MCM_CustomPageTest CustomPageTestHarness; 7 | 8 | if (MCM_API(Screen) != none) 9 | { 10 | if (!(class'MCM_TestConfigStore'.default.ENABLE_TEST_HARNESS)) 11 | { 12 | `log("MCM Test Harness Disabled."); 13 | return; 14 | } 15 | } 16 | 17 | RegularTestHarness = new class'MCM_TestHarness'; 18 | CustomPageTestHarness = new class'MCM_CustomPageTest'; 19 | 20 | RegularTestHarness.OnInit(Screen); 21 | CustomPageTestHarness.OnInit(Screen); 22 | } 23 | 24 | defaultproperties 25 | { 26 | // Need this because you won't be able to listen for a concrete class type that doesn't exist yet. 27 | ScreenClass = none; 28 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_Instance.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Instance; 2 | 3 | // PageID is unique to the "Settings Page" that your mod has registered. 4 | delegate CustomSettingsPageCallback(UIScreen ParentScreen, int PageID); 5 | 6 | // Gives you a way to roll your own settings to be spawned via the shared menu. 7 | // You'll have to push a screen to the stack yourself. 8 | // This gives you a way to just hook in your own rolled settings page. 9 | function int NewCustomSettingsPage(string TabLabel, delegate Handler); 10 | 11 | // This lets you take advantage of ready-made UI components. 12 | // TabLabel is what the tab on the left should say. 13 | function MCM_API_SettingsPage NewSettingsPage(string TabLabel); 14 | 15 | // Returns the corresponding settings page with the given ID. Will return null (none) if it's a custom 16 | // settings page, because MCM doesn't want to know how you implemented your custom page. 17 | function MCM_API_SettingsPage GetSettingsPageByID(int PageID); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_Instance.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Instance; 2 | 3 | // PageID is unique to the "Settings Page" that your mod has registered. 4 | delegate CustomSettingsPageCallback(UIScreen ParentScreen, int PageID); 5 | 6 | // Gives you a way to roll your own settings to be spawned via the shared menu. 7 | // You'll have to push a screen to the stack yourself. 8 | // This gives you a way to just hook in your own rolled settings page. 9 | function int NewCustomSettingsPage(string TabLabel, delegate Handler); 10 | 11 | // This lets you take advantage of ready-made UI components. 12 | // TabLabel is what the tab on the left should say. 13 | function MCM_API_SettingsPage NewSettingsPage(string TabLabel); 14 | 15 | // Returns the corresponding settings page with the given ID. Will return null (none) if it's a custom 16 | // settings page, because MCM doesn't want to know how you implemented your custom page. 17 | function MCM_API_SettingsPage GetSettingsPageByID(int PageID); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/MCM_API_CfgHelpers.uci: -------------------------------------------------------------------------------- 1 | // Contains helper functions for doing proper user-configurable save data. See documentation for patterns on how to do it. 2 | 3 | `define MCM_CH_StaticVersionChecker(SrcVer,TrgVer)\ 4 | static function bool MCM_CH_IMPL_VersionChecker()\ 5 | {\ 6 | return (`{TrgVer} < `{SrcVer});\ 7 | }\ 8 | static function int MCM_CH_IMPL_VersionMax()\ 9 | {\ 10 | return (MCM_CH_IMPL_VersionChecker() ? (`{SrcVer}) : (`{TrgVer}));\ 11 | } 12 | 13 | `define MCM_CH_VersionChecker(SrcVer,TrgVer)\ 14 | function bool MCM_CH_IMPL_VersionChecker()\ 15 | {\ 16 | return (`{TrgVer} < `{SrcVer});\ 17 | }\ 18 | function int MCM_CH_IMPL_VersionMax()\ 19 | {\ 20 | return (MCM_CH_IMPL_VersionChecker() ? (`{SrcVer}) : (`{TrgVer}));\ 21 | } 22 | 23 | `define MCM_CH_GetValue(SrcVal, TrgVal)\ 24 | ((MCM_CH_IMPL_VersionChecker()) ? (`{SrcVal}) : (`{TrgVal})) 25 | 26 | `define MCM_CH_GetCompositeVersion()\ 27 | MCM_CH_IMPL_VersionMax() 28 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_CustomPageTestUI.uc: -------------------------------------------------------------------------------- 1 | class MCM_CustomPageTestUI extends UIScreen; 2 | 3 | var UIPanel Container; 4 | var UIImage BG; 5 | var UIButton SaveAndExitButton; 6 | 7 | simulated function InitScreen(XComPlayerController InitController, UIMovie InitMovie, optional name InitName) 8 | { 9 | `log("MCM Custom page initialized."); 10 | 11 | super.InitScreen(InitController, InitMovie, InitName); 12 | 13 | Container = Spawn(class'UIPanel', self).InitPanel('').SetPosition(100, 100).SetSize(800, 800); 14 | BG = Spawn(class'UIImage', Container).InitImage(,"img:///MCM.gfx.MainBackground"); 15 | 16 | // Save and exit button 17 | SaveAndExitButton = Spawn(class'UIButton', Container); 18 | SaveAndExitButton.InitButton(, "Exit", OnSaveAndExit); 19 | SaveAndExitButton.SetPosition(Container.width - 190, Container.height - 40); //Relative to this screen panel 20 | } 21 | 22 | simulated function InitUI() 23 | { 24 | } 25 | 26 | simulated function OnSaveAndExit(UIButton kButton) 27 | { 28 | Movie.Stack.Pop(self); 29 | } 30 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu.XCOM_sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # XCOM ModBuddy Solution File, Format Version 11.00 4 | VisualStudioVersion = 12.0.21005.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{5DAE07AF-E217-45C1-8DE7-FF99D6011E8A}") = "ModConfigMenu", "ModConfigMenu\ModConfigMenu.x2proj", "{3F350978-33E7-493A-993B-92C1B54BF591}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|XCOM 2 = Debug|XCOM 2 11 | Default|XCOM 2 = Default|XCOM 2 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {3F350978-33E7-493A-993B-92C1B54BF591}.Debug|XCOM 2.ActiveCfg = Debug|XCOM 2 15 | {3F350978-33E7-493A-993B-92C1B54BF591}.Debug|XCOM 2.Build.0 = Debug|XCOM 2 16 | {3F350978-33E7-493A-993B-92C1B54BF591}.Default|XCOM 2.ActiveCfg = Default|XCOM 2 17 | {3F350978-33E7-493A-993B-92C1B54BF591}.Default|XCOM 2.Build.0 = Default|XCOM 2 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/MCM_API_Includes.uci: -------------------------------------------------------------------------------- 1 | `define MCM_MAJOR_VERSION 1 2 | `define MCM_MINOR_VERSION 0 3 | 4 | `define MCM_API_Register(screenInst, handler)\ 5 | if (MCM_API(`{screenInst}) != none){MCM_API(`{screenInst}).RegisterClientMod(`{MCM_MAJOR_VERSION}, `{MCM_MINOR_VERSION}, `{handler});} 6 | 7 | 8 | `define MCM_API_BasicButtonHandler(FuncName)\ 9 | simulated function `{FuncName} (MCM_API_Setting Setting) 10 | 11 | `define MCM_API_BasicCheckboxSaveHandler(FuncName,VarName)\ 12 | simulated function `{FuncName} (MCM_API_Setting _Setting, bool _SettingValue) { `{VarName} = _SettingValue; } 13 | 14 | `define MCM_API_BasicSliderSaveHandler(FuncName,VarName)\ 15 | simulated function `{FuncName} (MCM_API_Setting _Setting, float _SettingValue) { `{VarName} = _SettingValue; } 16 | 17 | `define MCM_API_BasicSpinnerSaveHandler(FuncName,VarName)\ 18 | simulated function `{FuncName} (MCM_API_Setting _Setting, string _SettingValue) { `{VarName} = _SettingValue; } 19 | 20 | `define MCM_API_BasicDropdownSaveHandler(FuncName,VarName)\ 21 | simulated function `{FuncName} (MCM_API_Setting _Setting, string _SettingValue) { `{VarName} = _SettingValue; } 22 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_Setting.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Setting; 2 | 3 | enum eSettingType 4 | { 5 | eSettingType_Label, 6 | eSettingType_Button, 7 | eSettingType_Checkbox, 8 | eSettingType_Slider, 9 | eSettingType_Dropdown, 10 | eSettingType_Spinner, 11 | eSettingType_Unknown 12 | }; 13 | 14 | // Name is used for ID purposes, not for UI. 15 | function name GetName(); 16 | 17 | // Label is used for UI purposes, not for ID. 18 | function SetLabel(string NewLabel); 19 | function string GetLabel(); 20 | 21 | // When you mouse-over the setting. 22 | function SetHoverTooltip(string Tooltip); 23 | function string GetHoverTooltip(); 24 | 25 | // Lets you show an option but disable it because it shouldn't be configurable. 26 | // For example, if you don't want to allow tweaking during a mission. 27 | function SetEditable(bool IsEditable); 28 | 29 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 30 | // future "extension types". 31 | function int GetSettingType(); 32 | 33 | // Returns the group that the setting belongs to. 34 | function MCM_API_SettingsGroup GetParentGroup(); 35 | -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/MCM_API_Includes.uci: -------------------------------------------------------------------------------- 1 | `define MCM_MAJOR_VERSION 1 2 | `define MCM_MINOR_VERSION 0 3 | 4 | `define MCM_API_Register(screenInst, handler)\ 5 | if (MCM_API(`{screenInst}) != none){MCM_API(`{screenInst}).RegisterClientMod(`{MCM_MAJOR_VERSION}, `{MCM_MINOR_VERSION}, `{handler});} 6 | 7 | 8 | `define MCM_API_BasicButtonHandler(FuncName)\ 9 | simulated function `{FuncName} (MCM_API_Setting Setting) 10 | 11 | `define MCM_API_BasicCheckboxSaveHandler(FuncName,VarName)\ 12 | simulated function `{FuncName} (MCM_API_Setting _Setting, bool _SettingValue) { `{VarName} = _SettingValue; } 13 | 14 | `define MCM_API_BasicSliderSaveHandler(FuncName,VarName)\ 15 | simulated function `{FuncName} (MCM_API_Setting _Setting, float _SettingValue) { `{VarName} = _SettingValue; } 16 | 17 | `define MCM_API_BasicSpinnerSaveHandler(FuncName,VarName)\ 18 | simulated function `{FuncName} (MCM_API_Setting _Setting, string _SettingValue) { `{VarName} = _SettingValue; } 19 | 20 | `define MCM_API_BasicDropdownSaveHandler(FuncName,VarName)\ 21 | simulated function `{FuncName} (MCM_API_Setting _Setting, string _SettingValue) { `{VarName} = _SettingValue; } 22 | -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_Setting.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_Setting; 2 | 3 | enum eSettingType 4 | { 5 | eSettingType_Label, 6 | eSettingType_Button, 7 | eSettingType_Checkbox, 8 | eSettingType_Slider, 9 | eSettingType_Dropdown, 10 | eSettingType_Spinner, 11 | eSettingType_Unknown 12 | }; 13 | 14 | // Name is used for ID purposes, not for UI. 15 | function name GetName(); 16 | 17 | // Label is used for UI purposes, not for ID. 18 | function SetLabel(string NewLabel); 19 | function string GetLabel(); 20 | 21 | // When you mouse-over the setting. 22 | function SetHoverTooltip(string Tooltip); 23 | function string GetHoverTooltip(); 24 | 25 | // Lets you show an option but disable it because it shouldn't be configurable. 26 | // For example, if you don't want to allow tweaking during a mission. 27 | function SetEditable(bool IsEditable); 28 | 29 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 30 | // future "extension types". 31 | function int GetSettingType(); 32 | 33 | // Returns the group that the setting belongs to. 34 | function MCM_API_SettingsGroup GetParentGroup(); 35 | -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_SettingsPage.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_SettingsPage; 2 | 3 | delegate SaveStateHandler(MCM_API_SettingsPage SettingsPage); 4 | 5 | // Gives you a way to uniquely identify this settings page from all others, 6 | // guaranteed to be unique per "OnInit" of the mod settings menu. 7 | function int GetPageId(); 8 | 9 | function SetPageTitle(string title); 10 | 11 | // Use these to handle user triggered save/cancel events. 12 | function SetSaveHandler(delegate SaveHandler); 13 | function SetCancelHandler(delegate CancelHandler); 14 | 15 | // By default Reset button is not visible, you can choose to use it. 16 | function EnableResetButton(delegate ResetHandler); 17 | 18 | // Groups let you visually cluster settings. All settings belong to groups. 19 | function MCM_API_SettingsGroup AddGroup(name GroupName, string GroupLabel); 20 | function MCM_API_SettingsGroup GetGroupByName(name GroupName); 21 | function MCM_API_SettingsGroup GetGroupByIndex(int Index); 22 | function int GetGroupCount(); 23 | 24 | // Call to indicate "done making settings". Must call all of your AddGroup and Group.Add#### calls before this. 25 | function ShowSettings(); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_SettingsPage.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_SettingsPage; 2 | 3 | delegate SaveStateHandler(MCM_API_SettingsPage SettingsPage); 4 | 5 | // Gives you a way to uniquely identify this settings page from all others, 6 | // guaranteed to be unique per "OnInit" of the mod settings menu. 7 | function int GetPageId(); 8 | 9 | function SetPageTitle(string title); 10 | 11 | // Use these to handle user triggered save/cancel events. 12 | function SetSaveHandler(delegate SaveHandler); 13 | function SetCancelHandler(delegate CancelHandler); 14 | 15 | // By default Reset button is not visible, you can choose to use it. 16 | // You can call this more than once to change the reset handler. 17 | function EnableResetButton(delegate ResetHandler); 18 | 19 | // Groups let you visually cluster settings. All settings belong to groups. 20 | function MCM_API_SettingsGroup AddGroup(name GroupName, string GroupLabel); 21 | function MCM_API_SettingsGroup GetGroupByName(name GroupName); 22 | function MCM_API_SettingsGroup GetGroupByIndex(int Index); 23 | function int GetGroupCount(); 24 | 25 | // Call to indicate "done making settings". Must call all of your AddGroup and Group.Add#### calls before this. 26 | function ShowSettings(); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_CustomPageTest.uc: -------------------------------------------------------------------------------- 1 | //class MCM_CustomPageTest extends UIScreenListener config(ModConfigMenuTestHarness); 2 | class MCM_CustomPageTest extends Object config(ModConfigMenuTestHarness); 3 | 4 | `include(ModConfigMenu/Src/ModConfigMenuAPI/MCM_API_Includes.uci) 5 | 6 | delegate CustomSettingsPageCallback(UIScreen ParentScreen, int PageID); 7 | 8 | //event OnInit(UIScreen Screen) 9 | function OnInit(UIScreen Screen) 10 | { 11 | //if (!(class'MCM_TestConfigStore'.default.ENABLE_TEST_HARNESS)) 12 | //{ 13 | // `log("MCM Test Harness Disabled (Custom Page Test)."); 14 | // return; 15 | //} 16 | 17 | // Use the macro because it automates the version check based on the API version you're compiling against. 18 | `MCM_API_Register(Screen, ClientModCallback); 19 | } 20 | 21 | function ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode) 22 | { 23 | if (GameMode == eGameMode_MainMenu) 24 | { 25 | ConfigAPI.NewCustomSettingsPage("Custom Page", CustomHandler); 26 | } 27 | } 28 | 29 | function CustomHandler(UIScreen ParentScreen, int PageID) 30 | { 31 | MCM_CustomPageTestUI(ParentScreen.Movie.Stack.Push(ParentScreen.Spawn(class'MCM_CustomPageTestUI', ParentScreen), ParentScreen.Movie)).InitUI(); 32 | } 33 | 34 | defaultproperties 35 | { 36 | //ScreenClass = class'MCM_OptionsScreen'; 37 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_GroupLabel.uc: -------------------------------------------------------------------------------- 1 | // liigiuyg 2 | class MCM_GroupLabel extends Actor; 3 | 4 | var string GroupLabel; 5 | 6 | //var UIMechaListItem Instance; 7 | //var MCM_UISettingSeparator Instance; 8 | var MCM_GroupLabelSeparator Instance; 9 | 10 | simulated function MCM_GroupLabel InitGroupLabel(string Label) 11 | { 12 | GroupLabel = Label; 13 | Instance = none; 14 | 15 | return self; 16 | } 17 | 18 | simulated function MCM_GroupLabelSeparator InstantiateUI(UIList Parent) 19 | { 20 | //Instance = Spawn(class'UIMechaListItem', Parent.itemContainer).InitListItem(); 21 | //Instance.UpdateDataDescription(GroupLabel); 22 | 23 | Instance = Spawn(class'MCM_GroupLabelSeparator', Parent.itemContainer); 24 | // Shorten. 25 | Instance.InitSeparator(); 26 | //Instance.SetHeight(40); 27 | // Smaller text too. 28 | Instance.UpdateTitle(GetFormattedLabel()); 29 | Instance.Show(); 30 | Instance.EnableNavigation(); 31 | 32 | return Instance; 33 | } 34 | 35 | function string GetGroupLabel() 36 | { 37 | return GroupLabel; 38 | } 39 | 40 | function string GetFormattedLabel() 41 | { 42 | return class'UIUtilities_Text'.static.AddFontInfo(GroupLabel, false); 43 | } 44 | 45 | function SetGroupLabel(string Label) 46 | { 47 | GroupLabel = Label; 48 | 49 | if (Instance != none) 50 | { 51 | //Instance.Desc.SetText(Label); 52 | Instance.UpdateTitle(GetFormattedLabel()); 53 | } 54 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_Button.uc: -------------------------------------------------------------------------------- 1 | class MCM_Button extends MCM_SettingBase implements(MCM_API_Button) config(ModConfigMenu); 2 | 3 | var delegate ClickHandler; 4 | 5 | var MCM_API_Setting ParentFacade; 6 | 7 | delegate VoidSettingHandler(MCM_API_Setting Setting); 8 | 9 | simulated function MCM_SettingBase InitSettingsItem(name _Name, eSettingType _Type, optional string _Label = "", optional string _Tooltip = "") 10 | { 11 | `log("Don't call InitSettingsItem directly in subclass of MCM_SettingBase."); 12 | 13 | return none; 14 | } 15 | 16 | // Fancy init process 17 | simulated function MCM_Button InitButton(name _SettingName, MCM_API_Setting _ParentFacade, string _Label, string _Tooltip, string _ButtonLabel, delegate _OnClick) 18 | { 19 | super.InitSettingsItem(_SettingName, eSettingType_Button, _Label, _Tooltip); 20 | 21 | ClickHandler = _OnClick; 22 | ParentFacade = _ParentFacade; 23 | 24 | UpdateDataButton(_Label, _ButtonLabel, ButtonClickedCallback); 25 | SetHoverTooltip(_Tooltip); 26 | 27 | return self; 28 | } 29 | 30 | function ButtonClickedCallback(UIButton ButtonSource) 31 | { 32 | if(ClickHandler != none) 33 | { 34 | ClickHandler(ParentFacade); 35 | } 36 | } 37 | 38 | // MCM_API_Button implementation 39 | function SimulateClick() 40 | { 41 | if(ClickHandler != none) 42 | { 43 | ClickHandler(ParentFacade); 44 | } 45 | } -------------------------------------------------------------------------------- /documentation/apidoc.md: -------------------------------------------------------------------------------- 1 | # API Doc 2 | 3 | ### Interface `MCM_API` 4 | 5 | ##### `function bool RegisterClientMod(int major, int minor, delegate SetupHandler);` 6 | 7 | Registers your mod to ask MCM to call your SetupHandler. The major/minor version numbers are to give MCM a way to enforce a compatibility check to guard against unsupported mods. 8 | 9 | We recommend that you use the `MCM_API_Register` macro, which takes care of the major/minor version numbers for you. For more about the major/minor version, see the [advanced features](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/advanced.md). 10 | 11 | `SetupHandler` should be the function where you do page, group, and setting creation. 12 | 13 | ##### `delegate ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode);` 14 | 15 | The `ClientModCallback` callback takes two critical parameters: the API instance, and the game mode. The API instance is only ever provided during this callback because this is the only time you should be calling the Page/Group/Setting creation functions. 16 | 17 | Since `MCM_OptionsScreen` gets initialized once for every time the screen is pulled up, the `ClientModCallback` callback gets called once for every time the screen is pulled up. Thus it's save to assume that `GameMode` is the correct mode for the lifetime of the pages/groups/settings that you create from inside the handler. 18 | 19 | ##### `enum eGameMode` 20 | 21 | Recognized game modes that `ClientModCallback` might receive are: 22 | 23 | ``` 24 | eGameMode_MainMenu, 25 | eGameMode_Strategy, 26 | eGameMode_Tactical, 27 | eGameMode_Multiplayer, 28 | eGameMode_Unknown 29 | ``` 30 | 31 | ### Interface `MCM_API_Instance` 32 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_SettingsTab.uc: -------------------------------------------------------------------------------- 1 | class MCM_SettingsTab extends UIMechaListItem; 2 | 3 | var int SettingsPageID; 4 | var delegate OnClickHandler; 5 | var delegate CustomPageCallback; 6 | 7 | delegate ClickHandler(MCM_SettingsTab Caller, int PageID); 8 | delegate CustomSettingsPageCallback(UIScreen ParentScreen, int PageID); 9 | 10 | function MCM_SettingsTab InitSettingsTab(int PageID, string Label) 11 | { 12 | super.InitListItem(); 13 | 14 | SettingsPageID = PageID; 15 | OnClickHandler = none; 16 | CustomPageCallback = none; 17 | 18 | UpdateDataCheckbox(Label, "", false, CheckboxChangedCallback, CheckboxClickedCallback); 19 | 20 | return self; 21 | } 22 | 23 | function CheckboxChangedCallback(UICheckbox CheckboxControl) 24 | { 25 | if (OnClickHandler != none) 26 | { 27 | OnClickHandler(self, SettingsPageID); 28 | } 29 | 30 | if (CustomPageCallback == none) 31 | { 32 | // One does not uncheck by clicking. Doesn't work that way. 33 | SetChecked(true); 34 | } 35 | else 36 | { 37 | SetChecked(false); 38 | } 39 | } 40 | 41 | function CheckboxClickedCallback() 42 | { 43 | if (CustomPageCallback == none) 44 | { 45 | // One does not uncheck by clicking. Doesn't work that way. 46 | SetChecked(true); 47 | } 48 | else 49 | { 50 | SetChecked(false); 51 | } 52 | 53 | if (OnClickHandler != none) 54 | { 55 | OnClickHandler(self, SettingsPageID); 56 | } 57 | } 58 | 59 | function SetChecked(bool Checked) 60 | { 61 | // Don't need to generate change event since we care about the click. 62 | Checkbox.SetChecked(Checked, false); 63 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_GroupLabelSeparator.uc: -------------------------------------------------------------------------------- 1 | // Copy-pasted with slightly different values from MCM_UISettingSeparator. 2 | class MCM_GroupLabelSeparator extends UIImage; 3 | 4 | var UIText Title; 5 | var UIScrollingText Description; 6 | var int TITLE_X; 7 | var int TITLE_Y; 8 | var int DESC_X; 9 | var int DESC_Y; 10 | 11 | // Main function for initialization. 12 | // @param name InitName : Name of the panel 13 | // @param string Label : Title of the header 14 | // @param string Description : A description to show below (couple lines) 15 | simulated function MCM_GroupLabelSeparator InitSeparator(optional name InitName,optional string Label,optional string HelperText) { 16 | //local UIList List; 17 | 18 | InitImage(InitName,"img:///MCM.gfx.GroupLabelSeparator"); 19 | //InitImage(InitName,"img:///MCM.gfx.MainHeaderSeparator"); 20 | 21 | SetWidth(548); 22 | SetHeight(38); 23 | 24 | // Get parent width 25 | //List = UIList(GetParent(class'UIList')); 26 | //if(List != none && List.Width > 0) 27 | // Width = List.Width; 28 | 29 | Title = Spawn(class'UIText', self).InitText(,Label); 30 | Title.SetPosition(TITLE_X,TITLE_Y); 31 | 32 | Description = Spawn(class'UIScrollingText', self).InitScrollingText(,HelperText, Width-DESC_X,DESC_X,DESC_Y); 33 | 34 | return self; 35 | } 36 | 37 | // Set the Title (after init.) 38 | // @param string Label : the new title 39 | simulated function UpdateTitle(string Label) { 40 | Title.SetSubTitle(Label); 41 | } 42 | 43 | // Set the Description (after init.) 44 | // @param string HelperText : the new description 45 | simulated function UpdateDescription(string HelperText) { 46 | Description.SetText(HelperText); 47 | } 48 | 49 | defaultproperties 50 | { 51 | TITLE_X = 0; 52 | TITLE_Y = 0; 53 | DESC_X = 0; 54 | DESC_Y = 0; 55 | 56 | bProcessesMouseEvents = true; 57 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_Checkbox.uc: -------------------------------------------------------------------------------- 1 | class MCM_Checkbox extends MCM_SettingBase implements(MCM_API_Checkbox) config(ModConfigMenu); 2 | 3 | var MCM_API_Setting ParentFacade; 4 | var delegate ChangeHandler; 5 | 6 | delegate BoolSettingHandler(MCM_API_Setting _Setting, bool _SettingValue); 7 | 8 | simulated function MCM_SettingBase InitSettingsItem(name _Name, eSettingType _Type, optional string _Label = "", optional string _Tooltip = "") 9 | { 10 | `log("Don't call InitSettingsItem directly in subclass of MCM_SettingBase."); 11 | 12 | return none; 13 | } 14 | 15 | // Fancy init process 16 | simulated function MCM_Checkbox InitCheckbox(name _SettingName, MCM_API_Setting _ParentFacade, string _Label, string _Tooltip, bool initiallyChecked, delegate _OnChange) 17 | { 18 | super.InitSettingsItem(_SettingName, eSettingType_Checkbox, _Label, _Tooltip); 19 | 20 | ChangeHandler = _OnChange; 21 | ParentFacade = _ParentFacade; 22 | 23 | UpdateDataCheckbox(_Label, "", initiallyChecked, CheckboxChangedCallback); 24 | SetHoverTooltip(_Tooltip); 25 | 26 | return self; 27 | } 28 | 29 | function CheckboxChangedCallback(UICheckbox CheckboxControl) 30 | { 31 | if(ChangeHandler != none) 32 | { 33 | ChangeHandler(ParentFacade, self.GetValue()); 34 | } 35 | } 36 | 37 | // MCM_API_Checkbox implementation ============================================================================= 38 | 39 | simulated function bool GetValue() 40 | { 41 | return Checkbox.bChecked; 42 | } 43 | 44 | simulated function SetValue(bool Checked, bool SuppressEvent) 45 | { 46 | Checkbox.SetChecked(Checked, !SuppressEvent); 47 | } 48 | 49 | // Have to override to disable the underlying control. 50 | simulated function SetEditable(bool IsEditable) 51 | { 52 | super.SetEditable(IsEditable); 53 | Checkbox.SetReadOnly(!IsEditable); 54 | } -------------------------------------------------------------------------------- /documentation/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to ModConfigMenu 2 | ##### Quick Start 3 | 4 | Follow the [*Tutorial*](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/tutorial.md). 5 | You will also want to read 6 | [*Updating Mod Configurations Dynamically*](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/config.md) 7 | 8 | ##### Example Code 9 | 10 | See the [ModConfigDependencyTest project](https://github.com/andrewgu/ModConfigMenu/tree/master/ModConfigDependencyTest) in this repo for a working example. 11 | 12 | ##### Advanced Features 13 | 14 | This mod supports several very useful advanced features that let you make your settings page more dynamic. 15 | See the [*Advanced Features*](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/advanced.md) 16 | documentation for details. 17 | 18 | ##### API Documentation 19 | 20 | For a complete catalog of all of the functions available to you, see [*API Documentation*](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/apidoc.md) 21 | 22 | ### Key Concepts 23 | 24 | Here are MCM's core object types, which also correspond to its core concepts: 25 | 26 | 1. The *API Instance* (`MCM_API_Instance`) is the entry point for the MCM API. Your mod will create its settings pages from here. 27 | 2. The *Page* (`MCM_API_SettingsPage`) represents a visual page of settings, which you can access by clicking the corresponding tab in the options screen. Each page contains groups of settings. Most mods will use only one page. 28 | 3. The *Group* (`MCM_API_SettingsGroup`) is a visual grouping of settings. Most mods might only need one group, but more complex mods might want to organize settings into groups. 29 | 4. The *Setting* (`MCM_API_Setting` and other Settings classes) are the actual widgets: checkboxes, sliders, dropdowns, etc. Typically these widgets represent some adjustable setting in your mod. 30 | 31 | A typical workflow would look like this: 32 | 33 | 1. Call the API instance to create a page. 34 | 2. Add groups to the page. 35 | 3. Add settings to the groups. 36 | 4. Hook up the code to save settings to an INI file. 37 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_UISettingSeparator.uc: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------------- 2 | // ********* MOD CONFIG MENU ****************** 3 | // FILE: MCM_UISettingSeparator 4 | // AUTHOR: Super d -- 11/03/16 5 | // PURPOSE: Controls a setting separator/small header with title & description 6 | //--------------------------------------------------------------------------------------- 7 | class MCM_UISettingSeparator extends UIImage; 8 | 9 | var UIText Title; 10 | var UIScrollingText Description; 11 | var int TITLE_X; 12 | var int TITLE_Y; 13 | var int DESC_X; 14 | var int DESC_Y; 15 | 16 | // Main function for initialization. 17 | // @param name InitName : Name of the panel 18 | // @param string Label : Title of the header 19 | // @param string Description : A description to show below (couple lines) 20 | simulated function MCM_UISettingSeparator InitSeparator(optional name InitName,optional string Label,optional string HelperText) { 21 | //local UIList List; 22 | 23 | InitImage(InitName,"img:///MCM.gfx.MainHeaderSeparator"); 24 | 25 | SetWidth(548); 26 | SetHeight(60); 27 | 28 | // Get parent width 29 | //List = UIList(GetParent(class'UIList')); 30 | //if(List != none && List.Width > 0) 31 | // Width = List.Width; 32 | 33 | Title = Spawn(class'UIText', self).InitText(,Label); 34 | Title.SetPosition(TITLE_X,TITLE_Y); 35 | 36 | Description = Spawn(class'UIScrollingText', self).InitScrollingText(,HelperText, Width-DESC_X,DESC_X,DESC_Y); 37 | 38 | return self; 39 | } 40 | 41 | // Set the Title (after init.) 42 | // @param string Label : the new title 43 | simulated function UpdateTitle(string Label) { 44 | Title.SetSubTitle(Label); 45 | } 46 | 47 | // Set the Description (after init.) 48 | // @param string HelperText : the new description 49 | simulated function UpdateDescription(string HelperText) { 50 | Description.SetText(HelperText); 51 | } 52 | 53 | defaultproperties 54 | { 55 | TITLE_X = 0; 56 | TITLE_Y = 10; 57 | DESC_X = 0; 58 | DESC_Y = 10; 59 | 60 | bProcessesMouseEvents = true; 61 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI/Classes/MCM_API_SettingsGroup.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_SettingsGroup; 2 | 3 | delegate VoidSettingHandler(MCM_API_Setting Setting); 4 | delegate BoolSettingHandler(MCM_API_Setting Setting, bool SettingValue); 5 | delegate FloatSettingHandler(MCM_API_Setting Setting, float SettingValue); 6 | delegate StringSettingHandler(MCM_API_Setting Setting, string SettingValue); 7 | 8 | // For reference purposes, not display purposes. 9 | function name GetName(); 10 | 11 | // For display purposes, not reference purposes. 12 | function string GetLabel(); 13 | function SetLabel(string Label); 14 | 15 | function MCM_API_SettingsPage GetParentPage(); 16 | 17 | // Will return None if setting by that name isn't found. 18 | function MCM_API_Setting GetSettingByName(name SettingName); 19 | function MCM_API_Setting GetSettingByIndex(int Index); 20 | function int GetNumberOfSettings(); 21 | 22 | // It's done this way because of some UI issues where dropdowns don't behave unless you initialize 23 | // the settings UI widgets from bottom up. 24 | function MCM_API_Label AddLabel(name SettingName, string Label, string Tooltip); 25 | function MCM_API_Button AddButton(name SettingName, string Label, string Tooltip, string ButtonLabel, 26 | optional delegate ClickHandler); 27 | function MCM_API_Checkbox AddCheckbox(name SettingName, string Label, string Tooltip, bool InitiallyChecked, 28 | optional delegate SaveHandler, 29 | optional delegate ChangeHandler); 30 | function MCM_API_Slider AddSlider(name SettingName, string Label, string Tooltip, float SliderMin, float SliderMax, float SliderStep, float InitialValue, 31 | optional delegate SaveHandler, 32 | optional delegate ChangeHandler); 33 | function MCM_API_Spinner AddSpinner(name SettingName, string Label, string Tooltip, array Options, string Selection, 34 | optional delegate SaveHandler, 35 | optional delegate ChangeHandler); 36 | function MCM_API_Dropdown AddDropdown(name SettingName, string Label, string Tooltip, array Options, string Selection, 37 | optional delegate SaveHandler, 38 | optional delegate ChangeHandler); -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigMenuAPI/Classes/MCM_API_SettingsGroup.uc: -------------------------------------------------------------------------------- 1 | interface MCM_API_SettingsGroup; 2 | 3 | delegate VoidSettingHandler(MCM_API_Setting Setting); 4 | delegate BoolSettingHandler(MCM_API_Setting Setting, bool SettingValue); 5 | delegate FloatSettingHandler(MCM_API_Setting Setting, float SettingValue); 6 | delegate StringSettingHandler(MCM_API_Setting Setting, string SettingValue); 7 | 8 | // For reference purposes, not display purposes. 9 | function name GetName(); 10 | 11 | // For display purposes, not reference purposes. 12 | function string GetLabel(); 13 | function SetLabel(string Label); 14 | 15 | function MCM_API_SettingsPage GetParentPage(); 16 | 17 | // Will return None if setting by that name isn't found. 18 | function MCM_API_Setting GetSettingByName(name SettingName); 19 | function MCM_API_Setting GetSettingByIndex(int Index); 20 | function int GetNumberOfSettings(); 21 | 22 | // It's done this way because of some UI issues where dropdowns don't behave unless you initialize 23 | // the settings UI widgets from bottom up. 24 | function MCM_API_Label AddLabel(name SettingName, string Label, string Tooltip); 25 | function MCM_API_Button AddButton(name SettingName, string Label, string Tooltip, string ButtonLabel, 26 | optional delegate ClickHandler); 27 | function MCM_API_Checkbox AddCheckbox(name SettingName, string Label, string Tooltip, bool InitiallyChecked, 28 | optional delegate SaveHandler, 29 | optional delegate ChangeHandler); 30 | function MCM_API_Slider AddSlider(name SettingName, string Label, string Tooltip, float SliderMin, float SliderMax, float SliderStep, float InitialValue, 31 | optional delegate SaveHandler, 32 | optional delegate ChangeHandler); 33 | function MCM_API_Spinner AddSpinner(name SettingName, string Label, string Tooltip, array Options, string Selection, 34 | optional delegate SaveHandler, 35 | optional delegate ChangeHandler); 36 | function MCM_API_Dropdown AddDropdown(name SettingName, string Label, string Tooltip, array Options, string Selection, 37 | optional delegate SaveHandler, 38 | optional delegate ChangeHandler); -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_OptionsMenuListener.uc: -------------------------------------------------------------------------------- 1 | // Paired with MCM_OptionsMenuListenerHook to work around a GC bug related to screen listeners. 2 | // This hook makes it so that this "listener" that uses a instance reference to a UIScreen 3 | // is actually attached to the UIOptionsScreen and not to the UIScreenListener, thus giving 4 | // the UIScreenListener a workable object lifetime. 5 | // It's actually attached to the UIOptionsScreen in GC terms because the instance reference to 6 | // this object from MCM_OptionsMenuListenerHook is transient, which means this object is 7 | // orphaned immediately, so it's just a node on the object reference graph attached to the UI 8 | // itself. Since the GC graph looks like that, we can safely keep a reference to UI objects here 9 | // instead of the actual screen listener. 10 | 11 | //class MCM_OptionsMenuListener extends UIScreenListener config(ModConfigMenu); 12 | class MCM_OptionsMenuListener extends Object config(ModConfigMenu); 13 | 14 | var config bool ENABLE_MENU; 15 | var config bool USE_FLAT_DISPLAY_STYLE; 16 | 17 | var localized string m_strModMenuButton; 18 | 19 | //var UIOptionsPCScreen ParentScreen; 20 | //var UIButton ModOptionsButton; 21 | 22 | //event OnInit(UIScreen Screen) 23 | function OnInit(UIOptionsPCSCreen Screen) 24 | { 25 | //if(UIOptionsPCSCreen(Screen) != none) 26 | //{ 27 | //ParentScreen = Screen; 28 | 29 | if (ENABLE_MENU) 30 | { 31 | InjectModOptionsButton(Screen); 32 | } 33 | //} 34 | } 35 | 36 | simulated function InjectModOptionsButton(UIOptionsPCSCreen ParentScreen) 37 | { 38 | local UIButton ModOptionsButton; 39 | ModOptionsButton = ParentScreen.Spawn(class'UIButton', ParentScreen); 40 | ModOptionsButton.InitButton(, m_strModMenuButton, ShowModOptionsDialog); 41 | ModOptionsButton.SetPosition(500, 850); //Relative to this screen panel 42 | ModOptionsButton.AnimateIn(0); 43 | } 44 | 45 | simulated function ShowModOptionsDialog(UIButton kButton) 46 | { 47 | local UIMovie TargetMovie; 48 | local UIOptionsPCScreen ParentScreen; 49 | 50 | `log("Mod Options Dialog Called."); 51 | 52 | ParentScreen = UIOptionsPCScreen(kButton.ParentPanel); 53 | 54 | if (USE_FLAT_DISPLAY_STYLE) 55 | TargetMovie = None; 56 | else 57 | TargetMovie = ParentScreen.Movie; 58 | 59 | MCM_OptionsScreen(ParentScreen.Movie.Stack.Push(ParentScreen.Spawn(class'MCM_OptionsScreen', ParentScreen), TargetMovie)).InitModOptionsMenu(self); 60 | } 61 | 62 | defaultproperties 63 | { 64 | //ParentScreen = none; 65 | //ModOptionsButton = none; 66 | //ScreenClass = none; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_SettingBase.uc: -------------------------------------------------------------------------------- 1 | class MCM_SettingBase extends UIMechaListItem implements(MCM_API_Setting); 2 | 3 | var eSettingType SettingType; 4 | var name SettingName; 5 | // Don't need this because we can just use the Desc object. 6 | var string DisplayLabel; 7 | var string DisplayTooltip; 8 | 9 | 10 | // Force different init pattern. 11 | //simulated function UIMechaListItem InitListItem(optional name InitName, optional int defaultWidth = -1) 12 | //{ 13 | // `log("Don't use this."); 14 | // // Intentionally make this break stuff. 15 | // return none; 16 | //} 17 | 18 | // This is the real initializer. 19 | simulated function MCM_SettingBase InitSettingsItem(name _Name, eSettingType _Type, optional string _Label = "", optional string _Tooltip = "") 20 | { 21 | super.InitListItem(); 22 | 23 | SettingName = _Name; 24 | //ChangedHandler = Handler; 25 | SettingType = _Type; 26 | 27 | DisplayLabel = _Label; 28 | Desc.SetText(DisplayLabel); 29 | 30 | DisplayTooltip = _Tooltip; 31 | //SetHoverTooltip(DisplayTooltip); 32 | 33 | return self; 34 | } 35 | 36 | simulated function AfterParentPageDisplayed() 37 | { 38 | // Apparently you have to set it once the screen element is actually visible. 39 | BG.SetTooltipText(DisplayTooltip); 40 | } 41 | 42 | // MCM_API_Setting implementation ========================================================================== 43 | 44 | // Name is used for ID purposes, not for UI. 45 | simulated function name GetName() 46 | { 47 | return SettingName; 48 | } 49 | 50 | // Label is used for UI purposes, not for ID. 51 | simulated function SetLabel(string NewLabel) 52 | { 53 | DisplayLabel = NewLabel; 54 | Desc.SetText(NewLabel); 55 | } 56 | 57 | simulated function string GetLabel() 58 | { 59 | return DisplayLabel; 60 | } 61 | 62 | // When you mouse-over the setting. 63 | simulated function SetHoverTooltip(string Tooltip) 64 | { 65 | // Doesn't inherently store Tooltip text so we'll remember it. 66 | DisplayTooltip = Tooltip; 67 | 68 | BG.SetTooltipText(Tooltip); 69 | } 70 | 71 | simulated function string GetHoverTooltip() 72 | { 73 | return DisplayTooltip; 74 | } 75 | 76 | // Lets you show an option but disable it because it shouldn't be configurable. 77 | // For example, if you don't want to allow tweaking during a mission. 78 | simulated function SetEditable(bool IsEditable) 79 | { 80 | SetDisabled(!IsEditable); 81 | } 82 | 83 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 84 | // future "extension types". 85 | simulated function int GetSettingType() 86 | { 87 | return SettingType; 88 | } 89 | 90 | simulated function MCM_API_SettingsGroup GetParentGroup() 91 | { 92 | // MCM_SettingBase derived classes will never get GetParentGroup called because it's handled by the 93 | // Facade object that wraps it. 94 | return None; 95 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_LabelFacade.uc: -------------------------------------------------------------------------------- 1 | class MCM_LabelFacade extends Actor implements(MCM_SettingFacade, MCM_API_Setting, MCM_API_Label) config(ModConfigMenu); 2 | 3 | var name SettingName; 4 | var string Label; 5 | var string Tooltip; 6 | var bool Editable; 7 | 8 | var MCM_SettingGroup ParentGroup; 9 | 10 | var MCM_Label uiInstance; 11 | 12 | simulated function MCM_LabelFacade InitLabelFacade(name _Name, string _Label, string _Tooltip, 13 | MCM_SettingGroup _ParentGroup) 14 | { 15 | SettingName = _Name; 16 | Label = _Label; 17 | Tooltip = _Tooltip; 18 | Editable = false; 19 | 20 | ParentGroup = _ParentGroup; 21 | 22 | uiInstance = none; 23 | 24 | return self; 25 | } 26 | 27 | // MCM_SettingFacade implementation ==================================================================== 28 | 29 | simulated function UIMechaListItem InstantiateUI(UIList parent) 30 | { 31 | uiInstance = Spawn(class'MCM_Label', parent.itemContainer).InitLabel(SettingName, self, Label, Tooltip); 32 | uiInstance.Show(); 33 | uiInstance.EnableNavigation(); 34 | uiInstance.SetEditable(Editable); 35 | 36 | return uiInstance; 37 | } 38 | 39 | simulated function AfterParentPageDisplayed() 40 | { 41 | if (uiInstance != none) 42 | { 43 | uiInstance.AfterParentPageDisplayed(); 44 | } 45 | } 46 | 47 | function TriggerSaveEvent() 48 | { 49 | // Label doeesn't have a save event. 50 | } 51 | 52 | // MCM_API_Label implementation ==================================================================== 53 | 54 | // No label methods. 55 | 56 | // MCM_API_Setting implementation ==================================================================== 57 | 58 | // Name is used for ID purposes, not for UI. 59 | function name GetName() 60 | { 61 | return uiInstance != none ? uiInstance.GetName() : SettingName; 62 | } 63 | 64 | // Label is used for UI purposes, not for ID. 65 | function SetLabel(string NewLabel) 66 | { 67 | if (uiInstance != none) 68 | { 69 | uiInstance.SetLabel(NewLabel); 70 | } 71 | else 72 | { 73 | Label = NewLabel; 74 | } 75 | } 76 | 77 | function string GetLabel() 78 | { 79 | return uiInstance != none ? uiInstance.GetLabel() : Label; 80 | } 81 | 82 | // When you mouse-over the setting. 83 | function SetHoverTooltip(string _Tooltip) 84 | { 85 | if (uiInstance != none) 86 | { 87 | uiInstance.SetHoverTooltip(_Tooltip); 88 | } 89 | else 90 | { 91 | Tooltip = _Tooltip; 92 | } 93 | } 94 | 95 | function string GetHoverTooltip() 96 | { 97 | return uiInstance != none ? uiInstance.GetHoverTooltip() : Tooltip; 98 | } 99 | 100 | // Lets you show an option but disable it because it shouldn't be configurable. 101 | // For example, if you don't want to allow tweaking during a mission. 102 | function SetEditable(bool IsEditable) 103 | { 104 | if (uiInstance != none) 105 | { 106 | uiInstance.SetEditable(IsEditable); 107 | } 108 | else 109 | { 110 | Editable = IsEditable; 111 | } 112 | } 113 | 114 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 115 | // future "extension types". 116 | function int GetSettingType() 117 | { 118 | return eSettingType_Label; 119 | } 120 | 121 | function MCM_API_SettingsGroup GetParentGroup() 122 | { 123 | return ParentGroup; 124 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_ButtonFacade.uc: -------------------------------------------------------------------------------- 1 | class MCM_ButtonFacade extends Actor implements(MCM_SettingFacade, MCM_API_Setting, MCM_API_Button) config(ModConfigMenu); 2 | 3 | var name SettingName; 4 | var string Label; 5 | var string Tooltip; 6 | var bool Editable; 7 | 8 | var MCM_SettingGroup ParentGroup; 9 | 10 | var string ButtonLabel; 11 | 12 | var delegate ClickHandler; 13 | 14 | var MCM_Button uiInstance; 15 | 16 | delegate VoidSettingHandler(MCM_API_Setting _Setting); 17 | 18 | simulated function MCM_ButtonFacade InitButtonFacade(name _Name, string _Label, string _Tooltip, string _ButtonLabel, 19 | delegate _OnClick, MCM_SettingGroup _ParentGroup) 20 | { 21 | SettingName = _Name; 22 | Label = _Label; 23 | Tooltip = _Tooltip; 24 | Editable = true; 25 | 26 | ParentGroup = _ParentGroup; 27 | 28 | ButtonLabel = _ButtonLabel; 29 | 30 | ClickHandler = _OnClick; 31 | 32 | uiInstance = none; 33 | 34 | return self; 35 | } 36 | 37 | // MCM_SettingFacade implementation ==================================================================== 38 | 39 | simulated function UIMechaListItem InstantiateUI(UIList parent) 40 | { 41 | uiInstance = Spawn(class'MCM_Button', parent.itemContainer).InitButton(SettingName, self, Label, Tooltip, ButtonLabel, ClickHandler); 42 | uiInstance.Show(); 43 | uiInstance.EnableNavigation(); 44 | uiInstance.SetEditable(Editable); 45 | 46 | return uiInstance; 47 | } 48 | 49 | simulated function AfterParentPageDisplayed() 50 | { 51 | if (uiInstance != none) 52 | { 53 | uiInstance.AfterParentPageDisplayed(); 54 | } 55 | } 56 | 57 | function TriggerSaveEvent() 58 | { 59 | // Button doeesn't have a save event. 60 | } 61 | 62 | // MCM_API_Button implementation ==================================================================== 63 | 64 | function SimulateClick() 65 | { 66 | ClickHandler(self); 67 | } 68 | 69 | // MCM_API_Setting implementation ==================================================================== 70 | 71 | // Name is used for ID purposes, not for UI. 72 | function name GetName() 73 | { 74 | return uiInstance != none ? uiInstance.GetName() : SettingName; 75 | } 76 | 77 | // Label is used for UI purposes, not for ID. 78 | function SetLabel(string NewLabel) 79 | { 80 | if (uiInstance != none) 81 | { 82 | uiInstance.SetLabel(NewLabel); 83 | } 84 | else 85 | { 86 | Label = NewLabel; 87 | } 88 | } 89 | 90 | function string GetLabel() 91 | { 92 | return uiInstance != none ? uiInstance.GetLabel() : Label; 93 | } 94 | 95 | // When you mouse-over the setting. 96 | function SetHoverTooltip(string _Tooltip) 97 | { 98 | if (uiInstance != none) 99 | { 100 | uiInstance.SetHoverTooltip(_Tooltip); 101 | } 102 | else 103 | { 104 | Tooltip = _Tooltip; 105 | } 106 | } 107 | 108 | function string GetHoverTooltip() 109 | { 110 | return uiInstance != none ? uiInstance.GetHoverTooltip() : Tooltip; 111 | } 112 | 113 | // Lets you show an option but disable it because it shouldn't be configurable. 114 | // For example, if you don't want to allow tweaking during a mission. 115 | function SetEditable(bool IsEditable) 116 | { 117 | if (uiInstance != none) 118 | { 119 | uiInstance.SetEditable(IsEditable); 120 | } 121 | else 122 | { 123 | Editable = IsEditable; 124 | } 125 | } 126 | 127 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 128 | // future "extension types". 129 | function int GetSettingType() 130 | { 131 | return eSettingType_Button; 132 | } 133 | 134 | function MCM_API_SettingsGroup GetParentGroup() 135 | { 136 | return ParentGroup; 137 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mod Config Menu 2 | 3 | XCOM 2 Mod Config Menu: a shared settings menu for Xcom 2 mods. 4 | 5 | ### Features 6 | 7 | * **Optional dependency** - Your mod still works when MCM isn't installed. 8 | * **Very simple API** - A basic settings page only takes a few lines. You don't have to write UI code if you don't want to. 9 | * **Compiles with your code** - One function call makes the page, another makes the button, and if you're doing it wrong ModBuddy will tell you. 10 | * **Lots of useful features** - You can preview settings, dynamically change one setting based on another, even inject a custom settings UI. 11 | * **Built-in version control**: if your mod and the MCM versions are incompatible, MCM will handle it gracefully. 12 | 13 | ### XCOM Chimera Squad: Will there be a Chimera Squad version? 14 | 15 | Yes. I will begin work to port MCM to Chimera Squad after I finish my first playthrough. 16 | 17 | ### Gamers: How do I use this? 18 | 19 | 1. Install this mod via [Steam Workshop](http://steamcommunity.com/sharedfiles/filedetails/?id=667104300) or [Nexus](http://www.nexusmods.com/xcom2/mods/573/). 20 | 2. That's it! If you have any mods that use MCM, it'll just work. 21 | 22 | ### Developers: How do I use this? 23 | 24 | For details, see [the documentation](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/intro.md). 25 | 26 | 1. Install the MCM mod. Recommend you do this by cloning the repo and compiling in ModBuddy. 27 | 2. Add the API source files into your ModBuddy project, and make a few specific configuration changes. 28 | 3. **Warning:** If you use MCM, make sure that your mod loads configurations properly when MCM is missing or if the player never actually opens MCM. See the tutorial for specifics. 29 | 30 | ### Screenshots 31 | 32 | Mod Settings button in the options screen: 33 | 34 | ![Mod Settings button in the options screen](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/Res/screen1.jpg "Mod Settings button in the options screen") 35 | 36 | Example mod rendered in the MCM UI: 37 | 38 | ![Example mod rendered in the MCM UI](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/Res/screen2.jpg "Example mod rendered in the MCM UI") 39 | 40 | ### Credits 41 | 42 | Many thanks to everyone who has contributed code to this project! Specifically: 43 | 44 | * Superd22 45 | * BlueRaja 46 | * Patrick-Seymour 47 | * RealityMachina 48 | 49 | And also many thanks to others who have contributed to this project's ongoing development: 50 | 51 | * CMDBob 52 | * korgano 53 | * robojumper 54 | 55 | ### License 56 | 57 | If it's not covered by Firaxis, then it's covered by the MIT License: 58 | 59 | The MIT License (MIT) 60 | 61 | Copyright (c) 2016 Andrew Gu 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining a copy 64 | of this software and associated documentation files (the "Software"), to deal 65 | in the Software without restriction, including without limitation the rights 66 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 67 | copies of the Software, and to permit persons to whom the Software is 68 | furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all 71 | copies or substantial portions of the Software. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 75 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 76 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 77 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 78 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 79 | SOFTWARE. 80 | 81 | If you publish or release any work based on the code in this project, you must 82 | include attribution of credit to myself (Andrew Gu) as well as all contributors: 83 | 84 | * Superd22 85 | * BlueRaja 86 | * Patrick-Seymour 87 | * RealityMachina 88 | * CMDBob 89 | * korgano 90 | * robojumper 91 | -------------------------------------------------------------------------------- /documentation/sharedcode.md: -------------------------------------------------------------------------------- 1 | # Making Mods Share Code 2 | 3 | Compiling against shared modules is a core concept in programming. It's the most common way for different code to use shared base functionality. Unfortunately, it's not straightforward to do when modding Xcom 2. 4 | 5 | To keep programming simple, ModConfigMenu uses a workaround to make it possible to compile your code as if ModConfigMenu were part of the core game. This lets you keep your code clean and straightforward, and it lets you avoid jury-rigged message passing mechanisms. 6 | 7 | The method described here is how ModConfigMenuAPI works under the hood. 8 | 9 | ### The Problem 10 | 11 | Let's say you have two packages, `A` and `B`. `A` wants to use code from `B`, but you want `B` to be distributed as a separate mod. The challenge is that ModBuddy isn't built for this, and neither is Unreal Engine. When you compile `B` using the Xcom SDK, it's compiled against the base game. When you compile `A`, it's also compiled against the base game. But if you want to compile `A` with `B` included... it's not straightforward at all. The problem is that if you try to compile `A` with `B` included, it will produce two module files. When you try to load both mods, you will have a duplicate compilation of `B`, so you can't guarantee that Xcom 2 will load the original `B`. In theory this works if only one version of `B` is ever made, but then you could never patch `B`. That's bad. 12 | 13 | Now replace `A` with "Your Mod" and `B` with "ModConfigMenu. See the problem? 14 | 15 | ### Workaround 16 | 17 | The fix is to make it so that `B` doesn't need to change in order to patch `B`. But how? 18 | 19 | The key is that UnrealScript supports interfaces. They're definitions of functionality but contain no actual code themselves. So you make a mod `C` that implements all of `B`'s interfaces. Then when you program `A`, it can call into `B` as well without caring about `C`'s existence. So `B` always stays the same, but since `B` doesn't need to contain any real code, that's much easier to do. `C` is the actual code, and as long as changes to `C` don't require changing `B`, then everybody is happy! 20 | 21 | But what if `C`'s changes do need to change `B`? The answer is to make a new package of interfaces, `D`, so that `A` and `C` both use `D`. 22 | 23 | This is the approach ModConfigMenu takes. The core ModConfigMenu mod is `C`. ModConfigMenuAPI is `B`. Your mod is `A`. If ModConfigMenu adds more types of settings or other features that it wants to give to `A` in the future, we will create a package `D`. Maybe we'll call it `ModConfigMenuAPI2`. 24 | 25 | ### How to Compile `A` against `B` 26 | 27 | So you know how this works in principle. How does it actually work in a project? 28 | 29 | Xcom 2's folder structure for mods looks like this: 30 | ``` 31 | MyMod 32 | > MyMod.v12.XCOM_suo 33 | > MyMod.XCOM_sln 34 | MyMod 35 | > MyMod.x2proj 36 | Config 37 | ... 38 | Src 39 | MyMod 40 | Classes 41 | *.uc 42 | PackageA 43 | Classes 44 | *.uc 45 | PackageB 46 | Classes 47 | *.uc 48 | ``` 49 | 50 | So if you want more than one package in your mod, you need a folder for each package in the `Src` folder. 51 | 52 | Next, since each mod has a "primary package" and then a bunch of dependencies, we tell Unreal Engine to treat the extra packages as dependencies in `XComEngine.ini`. The first two lines are pretty typical for Xcom 2 mods. The last three lines are the magic lines. You don't see them in the SDK documentation, but that's how you do it. 53 | 54 | ``` 55 | [Engine.ScriptPackages] 56 | +NonNativePackages=MyMod 57 | 58 | [UnrealEd.EditorEngine] 59 | +EditPackages=PackageA 60 | +EditPackages=PackageB 61 | ``` 62 | 63 | ### How MCM Does It 64 | 65 | As you can probably tell, MCM uses exactly this approach. ModConfigMenu is a standalone mod that implements the API. Every mod that uses ModConfigMenu imports the API and compiles against it. Since the API package promises to never change, Unreal Engine will always load the correct version of the API, so we've effectively created a way for you to compile your mod against MCM, as if MCM were part of the core game. 66 | -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/ModConfigDependencyTest.x2proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fa9b04b1-c447-4ce3-b608-d6f7a7d0e648 5 | Mod Config Dependency Test 6 | Just a test for building against dependent libraries. 7 | 0 8 | ExampleClassOverride 9 | ExampleClassOverride 10 | {3f350978-33e7-493a-993b-92c1b54bf591} 11 | 12 | 13 | 14 | Content 15 | 16 | 17 | 18 | 19 | 20 | 21 | Content 22 | 23 | 24 | Content 25 | 26 | 27 | Content 28 | 29 | 30 | Content 31 | 32 | 33 | Content 34 | 35 | 36 | Content 37 | 38 | 39 | Content 40 | 41 | 42 | Content 43 | 44 | 45 | Content 46 | 47 | 48 | Content 49 | 50 | 51 | Content 52 | 53 | 54 | Content 55 | 56 | 57 | Content 58 | 59 | 60 | Content 61 | 62 | 63 | Content 64 | 65 | 66 | Content 67 | 68 | 69 | Content 70 | 71 | 72 | Content 73 | 74 | 75 | Content 76 | 77 | 78 | Content 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_Spinner.uc: -------------------------------------------------------------------------------- 1 | class MCM_Spinner extends MCM_SettingBase implements(MCM_API_Spinner) config(ModConfigMenu); 2 | 3 | var delegate ChangeHandler; 4 | 5 | var MCM_API_Setting ParentFacade; 6 | var array SpinnerOptions; 7 | var int SpinnerSelection; 8 | var bool TmpSuppressEvent; 9 | 10 | delegate StringSettingHandler(MCM_API_Setting Setting, string _SettingValue); 11 | 12 | simulated function MCM_SettingBase InitSettingsItem(name _Name, eSettingType _Type, optional string _Label = "", optional string _Tooltip = "") 13 | { 14 | `log("Don't call InitSettingsItem directly in subclass of MCM_SettingBase."); 15 | 16 | return none; 17 | } 18 | 19 | // Fancy init process 20 | simulated function MCM_Spinner InitSpinner(name _SettingName, MCM_API_Setting _ParentFacade, string _Label, string _Tooltip, array _Options, string _Selection, 21 | delegate _OnChange) 22 | { 23 | super.InitSettingsItem(_SettingName, eSettingType_Checkbox, _Label, _Tooltip); 24 | 25 | ChangeHandler = _OnChange; 26 | ParentFacade = _ParentFacade; 27 | 28 | CloneOptionsList(_Options); 29 | SpinnerSelection = GetSelectionIndex(_Options, _Selection); 30 | 31 | TmpSuppressEvent = true; 32 | UpdateDataSpinner(_Label, "", SpinnerChangedCallback); 33 | Spinner.SetValue(_Selection); 34 | TmpSuppressEvent = false; 35 | 36 | SetHoverTooltip(_Tooltip); 37 | 38 | return self; 39 | } 40 | 41 | // Helpers 42 | 43 | function CloneOptionsList(array OptionsList) 44 | { 45 | local int iter; 46 | SpinnerOptions.Length = 0; 47 | for (iter = 0; iter < OptionsList.Length; iter++) 48 | { 49 | SpinnerOptions.AddItem(OptionsList[iter]); 50 | } 51 | } 52 | 53 | function int GetSelectionIndex(array OptionsList, string SelectedOption) 54 | { 55 | local int iter; 56 | for (iter = 0; iter < OptionsList.Length; iter++) 57 | { 58 | if (SelectedOption == OptionsList[iter]) 59 | return iter; 60 | } 61 | 62 | return -1; 63 | } 64 | 65 | function SpinnerChangedCallback(UIListItemSpinner SpinnerControl, int Direction) 66 | { 67 | SpinnerSelection += Direction; 68 | // Clamp index. 69 | if (SpinnerSelection >= SpinnerOptions.Length) 70 | SpinnerSelection = SpinnerOptions.Length - 1; 71 | if (SpinnerSelection < 0) 72 | SpinnerSelection = 0; 73 | 74 | Spinner.SetValue(GetValue()); 75 | 76 | if (ChangeHandler != none && !TmpSuppressEvent) 77 | { 78 | ChangeHandler(ParentFacade, GetValue()); 79 | } 80 | } 81 | 82 | // MCM_API_Spinner implementation =========================================================================== 83 | 84 | function string GetValue() 85 | { 86 | return SpinnerOptions[SpinnerSelection]; 87 | } 88 | 89 | function SetValue(string Selection, bool SuppressEvent) 90 | { 91 | local int index; 92 | 93 | index = GetSelectionIndex(SpinnerOptions, Selection); 94 | // If found. 95 | if (index >= 0) 96 | { 97 | SpinnerSelection = index; 98 | TmpSuppressEvent = SuppressEvent; 99 | Spinner.SetValue(Selection); 100 | 101 | // SetValue doesn't trigger the event so we manually trigger it if needed. 102 | if (ChangeHandler != none && !TmpSuppressEvent) 103 | { 104 | ChangeHandler(ParentFacade, GetValue()); 105 | } 106 | 107 | TmpSuppressEvent = false; 108 | } 109 | } 110 | 111 | function SetOptions(array NewOptions, string InitialSelection, bool SuppressEvent) 112 | { 113 | CloneOptionsList(NewOptions); 114 | SpinnerSelection = GetSelectionIndex(NewOptions, InitialSelection); 115 | 116 | TmpSuppressEvent = SuppressEvent; 117 | Spinner.SetValue(InitialSelection); 118 | 119 | // SetValue doesn't trigger the event so we manually trigger it if needed. 120 | if (ChangeHandler != none && !TmpSuppressEvent) 121 | { 122 | ChangeHandler(ParentFacade, GetValue()); 123 | } 124 | 125 | TmpSuppressEvent = false; 126 | 127 | SetHoverTooltip(DisplayTooltip); 128 | } 129 | 130 | // Have to override to disable the underlying control. 131 | simulated function SetEditable(bool IsEditable) 132 | { 133 | super.SetEditable(IsEditable); 134 | 135 | if (IsEditable) 136 | { 137 | Spinner.Show(); 138 | } 139 | else 140 | { 141 | Spinner.Hide(); 142 | } 143 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_CheckboxFacade.uc: -------------------------------------------------------------------------------- 1 | class MCM_CheckboxFacade extends Actor implements(MCM_SettingFacade, MCM_API_Setting, MCM_API_Checkbox) config(ModConfigMenu); 2 | 3 | var name SettingName; 4 | var string Label; 5 | var string Tooltip; 6 | var bool Editable; 7 | 8 | var MCM_SettingGroup ParentGroup; 9 | 10 | var bool Checked; 11 | 12 | var delegate ChangeHandler; 13 | var delegate SaveHandler; 14 | 15 | var MCM_Checkbox uiInstance; 16 | 17 | delegate BoolSettingHandler(MCM_API_Setting _Setting, bool _SettingValue); 18 | 19 | simulated function MCM_CheckboxFacade InitCheckboxFacade(name _Name, string _Label, string _Tooltip, bool _Checked, 20 | delegate _OnChange, delegate _OnSave, 21 | MCM_SettingGroup _ParentGroup) 22 | { 23 | SettingName = _Name; 24 | Label = _Label; 25 | Tooltip = _Tooltip; 26 | Editable = true; 27 | 28 | ParentGroup = _ParentGroup; 29 | 30 | Checked = _Checked; 31 | 32 | ChangeHandler = _OnChange; 33 | SaveHandler = _OnSave; 34 | 35 | uiInstance = none; 36 | 37 | return self; 38 | } 39 | 40 | // MCM_SettingFacade implementation ==================================================================== 41 | 42 | simulated function UIMechaListItem InstantiateUI(UIList parent) 43 | { 44 | uiInstance = Spawn(class'MCM_Checkbox', parent.itemContainer).InitCheckbox(SettingName, self, Label, Tooltip, Checked, ChangeHandler); 45 | uiInstance.Show(); 46 | uiInstance.EnableNavigation(); 47 | uiInstance.SetEditable(Editable); 48 | 49 | return uiInstance; 50 | } 51 | 52 | simulated function AfterParentPageDisplayed() 53 | { 54 | if (uiInstance != none) 55 | { 56 | uiInstance.AfterParentPageDisplayed(); 57 | } 58 | } 59 | 60 | function TriggerSaveEvent() 61 | { 62 | if (uiInstance != none) 63 | { 64 | SaveHandler(self, uiInstance.GetValue()); 65 | } 66 | else 67 | { 68 | SaveHandler(self, Checked); 69 | } 70 | } 71 | 72 | // MCM_API_Checkbox implementation ==================================================================== 73 | 74 | function bool GetValue() 75 | { 76 | return uiInstance != none ? uiInstance.GetValue(): Checked; 77 | } 78 | 79 | function SetValue(bool _Checked, bool SuppressEvent) 80 | { 81 | if (uiInstance != none) 82 | { 83 | uiInstance.SetValue(_Checked, SuppressEvent); 84 | } 85 | else 86 | { 87 | // Degenerate case, should still fire event if changing without visible widget. 88 | Checked = _Checked; 89 | if (!SuppressEvent && ChangeHandler != none) 90 | { 91 | ChangeHandler(self, Checked); 92 | } 93 | } 94 | } 95 | 96 | // MCM_API_Setting implementation ==================================================================== 97 | 98 | // Name is used for ID purposes, not for UI. 99 | function name GetName() 100 | { 101 | return uiInstance != none ? uiInstance.GetName() : SettingName; 102 | } 103 | 104 | // Label is used for UI purposes, not for ID. 105 | function SetLabel(string NewLabel) 106 | { 107 | if (uiInstance != none) 108 | { 109 | uiInstance.SetLabel(NewLabel); 110 | } 111 | else 112 | { 113 | Label = NewLabel; 114 | } 115 | } 116 | 117 | function string GetLabel() 118 | { 119 | return uiInstance != none ? uiInstance.GetLabel() : Label; 120 | } 121 | 122 | // When you mouse-over the setting. 123 | function SetHoverTooltip(string _Tooltip) 124 | { 125 | if (uiInstance != none) 126 | { 127 | uiInstance.SetHoverTooltip(_Tooltip); 128 | } 129 | else 130 | { 131 | Tooltip = _Tooltip; 132 | } 133 | } 134 | 135 | function string GetHoverTooltip() 136 | { 137 | return uiInstance != none ? uiInstance.GetHoverTooltip() : Tooltip; 138 | } 139 | 140 | // Lets you show an option but disable it because it shouldn't be configurable. 141 | // For example, if you don't want to allow tweaking during a mission. 142 | function SetEditable(bool IsEditable) 143 | { 144 | if (uiInstance != none) 145 | { 146 | uiInstance.SetEditable(IsEditable); 147 | } 148 | else 149 | { 150 | Editable = IsEditable; 151 | } 152 | } 153 | 154 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 155 | // future "extension types". 156 | function int GetSettingType() 157 | { 158 | return eSettingType_Checkbox; 159 | } 160 | 161 | function MCM_API_SettingsGroup GetParentGroup() 162 | { 163 | return ParentGroup; 164 | } -------------------------------------------------------------------------------- /documentation/config.md: -------------------------------------------------------------------------------- 1 | # Updating Mod Configurations Dynamically 2 | 3 | ### The Problem 4 | 5 | The problem is twofold. The solution is also an unwieldy hack, but it's the best answer I've encountered so far. 6 | 7 | #### Problem 1: SaveConfig and StaticSaveConfig don't always work. 8 | 9 | ##### Where Xcom 2 pulls mod configurations 10 | 11 | Xcom 2 pulls your mod configurations from `XCOM 2/XComGame/Mods/[Your Mod]/Config`. These are the INI files that you include in your project when you are creating it with ModBuddy. 12 | 13 | ##### The canonical way to write updated settings to INI files 14 | 15 | UnrealScript provides two methods, `SaveConfig()` and `StaticSaveConfig()`, which both write updated INI data back into the corresponding INI files. `SaveConfig()` uses values from the current instance of the class, and `StaticSaveConfig()` uses the default values. 16 | 17 | Normally, if your class declaration looks like this: 18 | 19 | ``` 20 | class Foo extends Object config(Bar); 21 | ``` 22 | 23 | Then somewhere there will be a `XComBar.ini` file. Probably in your `My Games` folder. When you call either `class'Foo'.static.StaticSaveConfig()` or call `self.SaveConfig()` from inside a method belonging to `Foo`, changes will be written to `XComBar.ini`. 24 | 25 | ##### Mods have special behavior 26 | 27 | The problem is that for mods specifically, Xcom 2 loads the INI files from the mod folder, but doesn't write the changes back into those INI files when you call `SaveConfig` or `StaticSaveConfig`. 28 | 29 | ##### Workaround 30 | 31 | However, there is a workaround. If you declare a class to pull its config from a *non-existent* INI file, and then try to call `SaveConfig` or `StaticSaveConfig`, the game will create the missing INI file in your `My Games` folder. Any future attempts to load that class will result in pulling the settings from that file, and when you call SaveConfig, new values will get written into that file. 32 | 33 | So if you want to be able to write new settings back into the INI file from within the game, you need to set the class to pull configurations from an initially non-existent INI file, which forces the game to create it in the `My Games` folder instead of your mod's `Config` folder. 34 | 35 | #### Problem 2: Defining defaults and not overwriting when updating mods 36 | 37 | ##### Mod updates overwrite user settings. 38 | 39 | If the user modified the mod's INI files inside the `Config` folder, those files will get overwritten if the mod author updates the mod for Steam Workshop. I suspect Nexus Mod Manager has the same problem. This means the user loses any settings every time the mod author decides to release a tiny patch. Not a good situation. This also means it's hard to define defaults using INI files packaged with the mod because defaults will overwrite user settings on every update. 40 | 41 | ### The Solution 42 | 43 | In short: 44 | 45 | 1. In addition to your regular class, declare a mirror "defaults" class. Add a "VERSION" variable to both. For example, if your class `Foo` looked like this: 46 | 47 | ``` 48 | class Foo extends object config(Foo); 49 | var config int VERSION; 50 | var config int CONFIG_VARIABLE; 51 | // and so on... 52 | ``` 53 | 54 | Then create a parallel class like this: 55 | 56 | ``` 57 | class Foo_Defaults extends object config(Foo_Defaults); 58 | var config int VERSION; 59 | var config int CONFIG_VARIABLE; 60 | ``` 61 | 62 | 2. In your ModBuddy project, only create `XcomFoo_Defaults.ini`, don't create `XcomFoo.ini`. Since `XcomFoo.ini` is missing, the config vars in `Foo` will take on their data type defaults, i.e. false for booleans, 0 for integers, "" for strings, and so on. 63 | 3. Use the version number to figure out which settings to use. Since `XcomFoo.ini` is missing, `class'Foo'.default.VERSION` will use the UnrealScript "uninitialized value for an `int`," which will `0`. `class'Foo_Defaults'.default.VERSION` will be whatever you put in `XcomFoo_Defaults.ini`. 64 | 4. If the version number for `Foo` is older than `Foo_Defaults` then pull values from `Foo_Defaults` into `Foo`, update the version number in `Foo`, and call `SaveConfig()`. This will create `XcomFoo.ini`. In the future, a version number check would tell you to use the values from `Foo`. 65 | 66 | This works especially well for our purposes with ModConfigMenu because `XcomFoo.ini` will be in your `My Games` folder, meaning it will be *writeable*! 67 | 68 | Since you are using version numbers, it's also possible to release a new set of defaults that overwrite user settings if you really think that's necessary. And if you want to be really granular, you could in theory attach a version number to every configuration setting so that you can selectively update settings when you update your mod. 69 | 70 | ### Example Code 71 | 72 | If you want to see this mechanism in action, take a look at `MCM_TestHarness.uc` in the ModConfigMenu project. You will have to peek into the macros it uses, but it demonstrates how to combine version numbers with the parallel "defaults" class to make an in-game configurable INI file based on default values provided by the mod author. 73 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_Dropdown.uc: -------------------------------------------------------------------------------- 1 | class MCM_Dropdown extends MCM_SettingBase implements(MCM_API_Dropdown) config(ModConfigMenu); 2 | 3 | var delegate ChangeHandler; 4 | 5 | var MCM_API_Setting ParentFacade; 6 | var array DropdownOptions; 7 | var int DropdownSelection; 8 | var bool TmpSuppressEvent; 9 | 10 | delegate StringSettingHandler(MCM_API_Setting Setting, string _SettingValue); 11 | 12 | simulated function MCM_SettingBase InitSettingsItem(name _Name, eSettingType _Type, optional string _Label = "", optional string _Tooltip = "") 13 | { 14 | `log("Don't call InitSettingsItem directly in subclass of MCM_SettingBase."); 15 | 16 | return none; 17 | } 18 | 19 | // Fancy init process 20 | simulated function MCM_Dropdown InitDropdown(name _SettingName, MCM_API_Setting _ParentFacade, string _Label, string _Tooltip, array _Options, string _Selection, 21 | delegate _OnChange) 22 | { 23 | super.InitSettingsItem(_SettingName, eSettingType_Checkbox, _Label, _Tooltip); 24 | 25 | ChangeHandler = _OnChange; 26 | ParentFacade = _ParentFacade; 27 | 28 | CloneOptionsList(_Options); 29 | DropdownSelection = GetSelectionIndex(_Options, _Selection); 30 | 31 | TmpSuppressEvent = true; 32 | UpdateDataDropdown(_Label, _Options, DropdownSelection, DropdownChangedCallback); 33 | TmpSuppressEvent = false; 34 | 35 | SetHoverTooltip(_Tooltip); 36 | 37 | return self; 38 | } 39 | 40 | // Helpers 41 | 42 | function CloneOptionsList(array OptionsList) 43 | { 44 | local int iter; 45 | DropdownOptions.Length = 0; 46 | for (iter = 0; iter < OptionsList.Length; iter++) 47 | { 48 | DropdownOptions.AddItem(OptionsList[iter]); 49 | } 50 | } 51 | 52 | function int GetSelectionIndex(array OptionsList, string SelectedOption) 53 | { 54 | local int iter; 55 | for (iter = 0; iter < OptionsList.Length; iter++) 56 | { 57 | if (SelectedOption == OptionsList[iter]) 58 | return iter; 59 | } 60 | 61 | return -1; 62 | } 63 | 64 | function DropdownChangedCallback(UIDropdown DropdownControl) 65 | { 66 | DropdownSelection = DropdownControl.SelectedItem; 67 | 68 | if (ChangeHandler != none && !TmpSuppressEvent) 69 | { 70 | ChangeHandler(ParentFacade, DropdownControl.GetSelectedItemText()); 71 | } 72 | } 73 | 74 | // Need to tweak text boundary limits 75 | 76 | simulated function UIMechaListItem UpdateDataDropdown(string _Desc, 77 | array Data, 78 | int SelectedIndex, 79 | delegate _OnSelectionChange, 80 | optional delegate _OnClickDelegate = none) 81 | { 82 | local int i; 83 | 84 | SetWidgetType(EUILineItemType_Dropdown); 85 | if(Dropdown != none) 86 | { 87 | Dropdown.Remove(); 88 | Dropdown = none; 89 | } 90 | 91 | if( Dropdown == none ) 92 | { 93 | Dropdown = Spawn(class'UIDropdown', self); 94 | Dropdown.bIsNavigable = false; 95 | Dropdown.InitDropdown('DropdownMC'); 96 | Dropdown.SetPosition(width - 308, 24); 97 | } 98 | 99 | Dropdown.Clear(); 100 | 101 | for(i = 0; i < Data.Length; ++i) 102 | { 103 | Dropdown.AddItem(Data[i]); 104 | } 105 | 106 | Dropdown.SetLabel(""); 107 | Dropdown.SetSelected(SelectedIndex); 108 | Dropdown.Show(); 109 | 110 | //Desc.SetWidth(width - 308); 111 | Desc.SetWidth(width - 340); 112 | Desc.SetHTMLText(_Desc); 113 | Desc.Show(); 114 | 115 | OnClickDelegate = _OnClickDelegate; 116 | Dropdown.OnItemSelectedDelegate = _OnSelectionChange; 117 | return self; 118 | } 119 | 120 | // MCM_API_Dropdown implementation =========================================================================== 121 | 122 | function string GetValue() 123 | { 124 | return DropdownOptions[DropdownSelection]; 125 | } 126 | 127 | function SetValue(string Selection, bool SuppressEvent) 128 | { 129 | local int index; 130 | 131 | index = GetSelectionIndex(DropdownOptions, Selection); 132 | // If found. 133 | if (index >= 0) 134 | { 135 | DropdownSelection = index; 136 | TmpSuppressEvent = SuppressEvent; 137 | Dropdown.SetSelected(index); 138 | TmpSuppressEvent = false; 139 | } 140 | } 141 | 142 | function SetOptions(array NewOptions, string InitialSelection, bool SuppressEvent) 143 | { 144 | CloneOptionsList(NewOptions); 145 | DropdownSelection = GetSelectionIndex(NewOptions, InitialSelection); 146 | 147 | TmpSuppressEvent = SuppressEvent; 148 | UpdateDataDropdown(GetLabel(), NewOptions, DropdownSelection, DropdownChangedCallback); 149 | TmpSuppressEvent = false; 150 | 151 | SetHoverTooltip(DisplayTooltip); 152 | } 153 | 154 | // Have to override to disable the underlying control. 155 | simulated function SetEditable(bool IsEditable) 156 | { 157 | super.SetEditable(IsEditable); 158 | if (IsEditable) 159 | { 160 | Dropdown.Show(); 161 | } 162 | else 163 | { 164 | Dropdown.Hide(); 165 | } 166 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_SettingsPanelFacade.uc: -------------------------------------------------------------------------------- 1 | class MCM_SettingsPanelFacade extends Actor implements(MCM_API_SettingsPage) config(ModConfigMenu); 2 | 3 | var int OffsetX; 4 | var int OffsetY; 5 | 6 | var int PageID; 7 | var string Title; 8 | 9 | var delegate ResetHandler; 10 | var delegate CancelHandler; 11 | var delegate SaveHandler; 12 | 13 | var array SettingGroups; 14 | 15 | var UIPanel Container; 16 | var MCM_SettingsPanel UiInstance; 17 | 18 | delegate SaveStateHandler(MCM_API_SettingsPage SettingsPage); 19 | 20 | simulated function MCM_SettingsPanelFacade InitSettingsPanelFacade(int _PageID, string _Title, int UiOffsetX, int UiOffsetY, UIPanel _Container) 21 | { 22 | OffsetX = UiOffsetX; 23 | OffsetY = UiOffsetY; 24 | PageID = _PageID; 25 | Title = _Title; 26 | 27 | ResetHandler = none; 28 | SaveHandler = none; 29 | CancelHandler = none; 30 | 31 | Container = _Container; 32 | UiInstance = none; 33 | 34 | } 35 | 36 | // Internal 37 | 38 | simulated function MCM_SettingsPanel CreateUi() 39 | { 40 | local MCM_SettingsPanel SP; 41 | SP = Spawn(class'MCM_SettingsPanel', Container); 42 | SP.InitPanel(); 43 | SP.Facade = self; 44 | SP.SettingsPageID = PageID; 45 | SP.SetPosition(OffsetX, OffsetY); 46 | SP.SetPageTitle(Title); 47 | 48 | // Inject the identical group list from the facade into the panel 49 | SP.SettingGroups = SettingGroups; 50 | 51 | // Gotta make sure to forward handlers too 52 | SP.SetSaveHandler(SaveHandler); 53 | SP.SetCancelHandler(CancelHandler); 54 | if (ResetHandler != none) 55 | { 56 | SP.EnableResetButton(ResetHandler); 57 | } 58 | 59 | // This spawns all of the actual UI elements into the panel. 60 | SP.ShowSettings(); 61 | 62 | return SP; 63 | } 64 | 65 | // MCM_SettingsPanelFacade helpers for OptionsScreen implementation ============== 66 | 67 | simulated function Show() 68 | { 69 | if (UiInstance == none) 70 | { 71 | // .Show() is implicitly called. 72 | UiInstance = CreateUi(); 73 | } 74 | else 75 | { 76 | UiInstance.Show(); 77 | } 78 | } 79 | 80 | simulated function Hide() 81 | { 82 | if (UiInstance != none) 83 | { 84 | UiInstance.Hide(); 85 | } 86 | } 87 | 88 | simulated function TriggerSaveEvent() 89 | { 90 | //`log("MCM SettingsPanelFacade: triggered save event"); 91 | // If they never actually opened the panel, then no need to start triggering any events. 92 | if (UiInstance != none) 93 | { 94 | //`log("MCM SettingsPanelFacade: forwarding save event"); 95 | UiInstance.TriggerSaveEvent(); 96 | } 97 | } 98 | 99 | simulated function TriggerCancelEvent() 100 | { 101 | //`log("MCM SettingsPanelFacade: triggered cancel event"); 102 | // If they never actually opened the panel, then no need to start triggering any events. 103 | if (UiInstance != none) 104 | { 105 | //`log("MCM SettingsPanelFacade: forwarding cancel event"); 106 | UiInstance.TriggerCancelEvent(); 107 | } 108 | } 109 | 110 | // MCM_API_SettingsPage implementation =========================================== 111 | 112 | // Gives you a way to uniquely identify this settings page from all others, 113 | // guaranteed to be unique per "OnInit" of the mod settings menu. 114 | function int GetPageId() 115 | { 116 | return PageID; 117 | } 118 | 119 | function SetPageTitle(string _Title) 120 | { 121 | Title = _Title; 122 | 123 | if (UiInstance != none) 124 | { 125 | UiInstance.SetPageTitle(_Title); 126 | } 127 | } 128 | 129 | // Use these to handle user triggered save/cancel events. 130 | function SetSaveHandler(delegate _SaveHandler) 131 | { 132 | SaveHandler = _SaveHandler; 133 | 134 | if (UiInstance != none) 135 | { 136 | UiInstance.SetSaveHandler(_SaveHandler); 137 | } 138 | } 139 | 140 | function SetCancelHandler(delegate _CancelHandler) 141 | { 142 | CancelHandler = _CancelHandler; 143 | 144 | if (UiInstance != none) 145 | { 146 | UiInstance.SetCancelHandler(_CancelHandler); 147 | } 148 | } 149 | 150 | // By default Reset button is not visible, you can choose to use it. 151 | // You can call this more than once to change the reset handler. 152 | function EnableResetButton(delegate _ResetHandler) 153 | { 154 | ResetHandler = _ResetHandler; 155 | 156 | if (UiInstance != none) 157 | { 158 | UiInstance.EnableResetButton(_ResetHandler); 159 | } 160 | } 161 | 162 | // Groups let you visually cluster settings. All settings belong to groups. 163 | function MCM_API_SettingsGroup AddGroup(name GroupName, string GroupLabel) 164 | { 165 | local MCM_SettingGroup Grp; 166 | 167 | Grp = Spawn(class'MCM_SettingGroup', self).InitSettingGroup(GroupName, GroupLabel, self); 168 | SettingGroups.AddItem(Grp); 169 | 170 | return Grp; 171 | } 172 | 173 | function MCM_API_SettingsGroup GetGroupByName(name GroupName) 174 | { 175 | local MCM_SettingGroup iter; 176 | 177 | foreach SettingGroups(iter) 178 | { 179 | if (iter.GroupName == GroupName) 180 | return iter; 181 | } 182 | 183 | return none; 184 | } 185 | 186 | function MCM_API_SettingsGroup GetGroupByIndex(int Index) 187 | { 188 | if (Index >= 0 && Index < SettingGroups.Length) 189 | return SettingGroups[Index]; 190 | else 191 | return None; 192 | } 193 | 194 | function int GetGroupCount() 195 | { 196 | return SettingGroups.Length; 197 | } 198 | 199 | // Call to indicate "done making settings". Must call all of your AddGroup and Group.Add#### calls before this. 200 | function ShowSettings() 201 | { 202 | // This is a no-op, the actual call to generate UI elements will happen the first time the settings panel is opened. 203 | } -------------------------------------------------------------------------------- /ModConfigDependencyTest/ModConfigDependencyTest/Src/ModConfigDependencyTest/Classes/UIExample.uc: -------------------------------------------------------------------------------- 1 | //class UIExample extends UIScreenListener config(MCDT_Settings); 2 | class UIExample extends Object config(MCDT_Settings); 3 | 4 | `include(ModConfigDependencyTest/Src/ModConfigMenuAPI/MCM_API_Includes.uci) 5 | `include(ModConfigDependencyTest/Src/ModConfigMenuAPI/MCM_API_CfgHelpers.uci) 6 | 7 | var config bool CFG_CLICKED; 8 | var config bool CFG_CHECKBOX; 9 | var config float CFG_SLIDER; 10 | var config string CFG_DROPDOWN; 11 | var config string CFG_SPINNER; 12 | 13 | var MCM_API APIInst; 14 | 15 | var MCM_API_Label P1Label; 16 | var MCM_API_Button P1Button; 17 | var MCM_API_Checkbox P1Checkbox; 18 | var MCM_API_Slider P2Slider; 19 | var MCM_API_Slider P2SliderFloat; 20 | var MCM_API_Dropdown P2Dropdown; 21 | var MCM_API_Spinner P2Spinner; 22 | 23 | var MCM_API_SettingsPage Page1; 24 | 25 | var config int MCM_CH_IMPL_CFG_VERSION; 26 | 27 | `MCM_CH_VersionChecker(class'MCDT_ConfigSrc'.default.VERSION,MCM_CH_IMPL_CFG_VERSION) 28 | 29 | //event OnInit(UIScreen Screen) 30 | function OnInit(UIScreen Screen) 31 | { 32 | // Since it's listening for all UI classes, check here for the right screen, which will implement MCM_API. 33 | if (MCM_API(Screen) != none) 34 | { 35 | `log("Detected the options screen."); 36 | // Use the macro because it automates the version check based on the API version you're compiling against. 37 | `MCM_API_Register(Screen, ClientModCallback); 38 | } 39 | } 40 | 41 | simulated function ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode) 42 | { 43 | local MCM_API_SettingsGroup P1G1, P1G2, P1G3; 44 | local array Options; 45 | 46 | // Workaround that's needed in order to be able to "save" files. 47 | LoadInitialValues(); 48 | 49 | if (GameMode == eGameMode_MainMenu || GameMode == eGameMode_Strategy) 50 | { 51 | `log("Is in main menu or strategy menu, attempting to make page."); 52 | 53 | Page1 = ConfigAPI.NewSettingsPage("MCM Example"); 54 | Page1.SetPageTitle("MCM Example"); 55 | Page1.SetSaveHandler(SaveButtonClicked); 56 | Page1.SetCancelHandler(RevertButtonClicked); 57 | Page1.EnableResetButton(ResetButtonClicked); 58 | 59 | P1G1 = Page1.AddGroup('MCDT1', "General Settings"); 60 | P1G2 = Page1.AddGroup('MCDT2', "Group 1"); 61 | P1G3 = Page1.AddGroup('MCDT3', "Group 2"); 62 | 63 | P1Label = P1G1.AddLabel('label', "Label", "Label"); 64 | P1Button = P1G1.AddButton('button', "Button", "Button", "OK", ButtonClickedHandler); 65 | P1Checkbox = P1G1.AddCheckbox('checkbox', "Checkbox", "Checkbox", CFG_CHECKBOX, CheckboxSaveLogger); 66 | 67 | P2Slider = P1G2.AddSlider('slider', "Slider", "Slider", -30, 30, 1, CFG_SLIDER, SliderSaveLogger); 68 | P2SliderFloat = P1G2.AddSlider('floatslider', "Partial", "Partial", -30, 30, 0, -30.5, none); 69 | 70 | Options.Length = 0; 71 | Options.AddItem("a"); 72 | Options.AddItem("b"); 73 | Options.AddItem("c"); 74 | Options.AddItem("d"); 75 | Options.AddItem("e"); 76 | Options.AddItem("f"); 77 | Options.AddItem("g"); 78 | 79 | P2Spinner = P1G3.AddSpinner('spinner', "Spinner", "Spinner", Options, CFG_SPINNER, SpinnerSaveLogger); 80 | P2Dropdown = P1G3.AddDropdown('dropdown', "Dropdown", "Dropdown", Options, CFG_DROPDOWN, DropdownSaveLogger); 81 | 82 | if (GameMode == eGameMode_Strategy) 83 | P1Checkbox.SetEditable(false); 84 | 85 | Page1.ShowSettings(); 86 | } 87 | } 88 | 89 | `MCM_API_BasicCheckboxSaveHandler(CheckboxSaveLogger, CFG_CHECKBOX) 90 | `MCM_API_BasicSliderSaveHandler(SliderSaveLogger, CFG_SLIDER) 91 | `MCM_API_BasicDropdownSaveHandler(DropdownSaveLogger, CFG_DROPDOWN) 92 | `MCM_API_BasicSpinnerSaveHandler(SpinnerSaveLogger, CFG_SPINNER) 93 | 94 | `MCM_API_BasicButtonHandler(ButtonClickedHandler) 95 | { 96 | // Tests the slider positioning error. 97 | P2Slider.SetBounds(-200, 0, 20, P2Slider.GetValue(), true); 98 | 99 | CFG_CLICKED = true; 100 | } 101 | 102 | simulated function SaveButtonClicked(MCM_API_SettingsPage Page) 103 | { 104 | `log("MCM: Save button clicked"); 105 | 106 | self.MCM_CH_IMPL_CFG_VERSION = `MCM_CH_GetCompositeVersion(); 107 | self.SaveConfig(); 108 | } 109 | 110 | simulated function ResetButtonClicked(MCM_API_SettingsPage Page) 111 | { 112 | `log("MCM: Reset button clicked"); 113 | 114 | // Revert all of the settings except the float slider labeled "Partial" which is not saved anywhere and is not a real setting. 115 | CFG_CLICKED = false; 116 | P1Checkbox.SetValue(CFG_CHECKBOX, true); 117 | P2Slider.SetValue(CFG_SLIDER, true); 118 | P2Dropdown.SetValue(CFG_DROPDOWN, true); 119 | P2Spinner.SetValue(CFG_SPINNER, true); 120 | } 121 | 122 | simulated function RevertButtonClicked(MCM_API_SettingsPage Page) 123 | { 124 | // Don't need to do anything since values aren't written until at save-time when you use save handlers. 125 | `log("MCM: Cancel button clicked"); 126 | } 127 | 128 | // This shows how to either pull default values from a source config, or to use more user-defined values, gated by a version number mechanism. 129 | simulated function LoadInitialValues() 130 | { 131 | CFG_CLICKED = false; 132 | CFG_CHECKBOX = `MCM_CH_GetValue(class'MCDT_ConfigSrc'.default.CHECKBOX,CFG_CHECKBOX); 133 | CFG_SLIDER = `MCM_CH_GetValue(class'MCDT_ConfigSrc'.default.SLIDER,CFG_SLIDER); 134 | CFG_DROPDOWN = `MCM_CH_GetValue(class'MCDT_ConfigSrc'.default.DROPDOWN,CFG_DROPDOWN); 135 | CFG_SPINNER = `MCM_CH_GetValue(class'MCDT_ConfigSrc'.default.SPINNER,CFG_SPINNER); 136 | } 137 | 138 | defaultproperties 139 | { 140 | // The class you're listening for doesn't exist in this project, so you can't listen for it directly. 141 | //ScreenClass = none; 142 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_SpinnerFacade.uc: -------------------------------------------------------------------------------- 1 | class MCM_SpinnerFacade extends Actor implements(MCM_SettingFacade, MCM_API_Setting, MCM_API_Spinner) config(ModConfigMenu); 2 | 3 | var name SettingName; 4 | var string Label; 5 | var string Tooltip; 6 | var bool Editable; 7 | 8 | var array Options; 9 | var int SelectionIndex; 10 | 11 | var MCM_SettingGroup ParentGroup; 12 | 13 | var delegate ChangeHandler; 14 | var delegate SaveHandler; 15 | 16 | var MCM_Spinner uiInstance; 17 | 18 | delegate StringSettingHandler(MCM_API_Setting Setting, string _SettingValue); 19 | 20 | simulated function MCM_SpinnerFacade InitSpinnerFacade(name _Name, string _Label, string _Tooltip, 21 | array _Options, string _Selection, 22 | delegate _OnChange, delegate _OnSave, 23 | MCM_SettingGroup _ParentGroup) 24 | { 25 | SettingName = _Name; 26 | Label = _Label; 27 | Tooltip = _Tooltip; 28 | Editable = true; 29 | 30 | ParentGroup = _ParentGroup; 31 | 32 | CloneOptionsList(_Options); 33 | SelectionIndex = GetSelectionIndex(_Options, _Selection); 34 | 35 | ChangeHandler = _OnChange; 36 | SaveHandler = _OnSave; 37 | 38 | uiInstance = none; 39 | 40 | return self; 41 | } 42 | 43 | // Helpers 44 | 45 | function CloneOptionsList(array OptionsList) 46 | { 47 | local int iter; 48 | Options.Length = 0; 49 | for (iter = 0; iter < OptionsList.Length; iter++) 50 | { 51 | Options.AddItem(OptionsList[iter]); 52 | } 53 | } 54 | 55 | function int GetSelectionIndex(array OptionsList, string SelectedOption) 56 | { 57 | local int iter; 58 | for (iter = 0; iter < OptionsList.Length; iter++) 59 | { 60 | if (SelectedOption == OptionsList[iter]) 61 | return iter; 62 | } 63 | 64 | return -1; 65 | } 66 | 67 | // MCM_SettingFacade implementation ================================================================= 68 | function UIMechaListItem InstantiateUI(UIList Parent) 69 | { 70 | uiInstance = Spawn(class'MCM_Spinner', parent.itemContainer).InitSpinner(SettingName, self, Label, Tooltip, Options, Options[SelectionIndex], ChangeHandler); 71 | uiInstance.Show(); 72 | uiInstance.EnableNavigation(); 73 | uiInstance.SetEditable(Editable); 74 | 75 | return uiInstance; 76 | } 77 | 78 | simulated function AfterParentPageDisplayed() 79 | { 80 | if (uiInstance != none) 81 | { 82 | uiInstance.AfterParentPageDisplayed(); 83 | } 84 | } 85 | 86 | function TriggerSaveEvent() 87 | { 88 | if (uiInstance != none) 89 | { 90 | SaveHandler(self, uiInstance.GetValue()); 91 | } 92 | else 93 | { 94 | SaveHandler(self, Options[SelectionIndex]); 95 | } 96 | } 97 | 98 | // MCM_API_Spinner implementation ================================================================= 99 | 100 | function string GetValue() 101 | { 102 | return uiInstance != none ? uiInstance.GetValue() : Options[SelectionIndex]; 103 | } 104 | 105 | function SetValue(string Selection, bool SuppressEvent) 106 | { 107 | local int TmpIndex; 108 | 109 | if (uiInstance != none) 110 | { 111 | uiInstance.SetValue(Selection, SuppressEvent); 112 | } 113 | else 114 | { 115 | TmpIndex = GetSelectionIndex(Options, Selection); 116 | 117 | if (!SuppressEvent && TmpIndex >= 0 && TmpIndex != SelectionIndex) 118 | { 119 | ChangeHandler(self, Selection); 120 | } 121 | 122 | if (TmpIndex >= 0) 123 | { 124 | SelectionIndex = TmpIndex; 125 | } 126 | } 127 | } 128 | 129 | function SetOptions(array NewOptions, string InitialSelection, bool SuppressEvent) 130 | { 131 | local int TmpIndex; 132 | 133 | if (uiInstance != none) 134 | { 135 | uiInstance.SetOptions(NewOptions, InitialSelection, SuppressEvent); 136 | } 137 | else 138 | { 139 | CloneOptionsList(NewOptions); 140 | //SelectionIndex = GetSelectionIndex(_Options, _Selection); 141 | TmpIndex = GetSelectionIndex(NewOptions, InitialSelection); 142 | 143 | if (!SuppressEvent && TmpIndex >= 0 && TmpIndex != SelectionIndex) 144 | { 145 | ChangeHandler(self, Options[SelectionIndex]); 146 | } 147 | 148 | if (TmpIndex >= 0) 149 | { 150 | SelectionIndex = TmpIndex; 151 | } 152 | } 153 | } 154 | 155 | // MCM_API_Setting implementation ==================================================================== 156 | 157 | // Name is used for ID purposes, not for UI. 158 | function name GetName() 159 | { 160 | return uiInstance != none ? uiInstance.GetName() : SettingName; 161 | } 162 | 163 | // Label is used for UI purposes, not for ID. 164 | function SetLabel(string NewLabel) 165 | { 166 | if (uiInstance != none) 167 | { 168 | uiInstance.SetLabel(NewLabel); 169 | } 170 | else 171 | { 172 | Label = NewLabel; 173 | } 174 | } 175 | 176 | function string GetLabel() 177 | { 178 | return uiInstance != none ? uiInstance.GetLabel() : Label; 179 | } 180 | 181 | // When you mouse-over the setting. 182 | function SetHoverTooltip(string _Tooltip) 183 | { 184 | if (uiInstance != none) 185 | { 186 | uiInstance.SetHoverTooltip(_Tooltip); 187 | } 188 | else 189 | { 190 | Tooltip = _Tooltip; 191 | } 192 | } 193 | 194 | function string GetHoverTooltip() 195 | { 196 | return uiInstance != none ? uiInstance.GetHoverTooltip() : Tooltip; 197 | } 198 | 199 | // Lets you show an option but disable it because it shouldn't be configurable. 200 | // For example, if you don't want to allow tweaking during a mission. 201 | function SetEditable(bool IsEditable) 202 | { 203 | if (uiInstance != none) 204 | { 205 | uiInstance.SetEditable(IsEditable); 206 | } 207 | else 208 | { 209 | Editable = IsEditable; 210 | } 211 | } 212 | 213 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 214 | // future "extension types". 215 | function int GetSettingType() 216 | { 217 | return eSettingType_Dropdown; 218 | } 219 | 220 | function MCM_API_SettingsGroup GetParentGroup() 221 | { 222 | return ParentGroup; 223 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_DropdownFacade.uc: -------------------------------------------------------------------------------- 1 | class MCM_DropdownFacade extends Actor implements(MCM_SettingFacade, MCM_API_Setting, MCM_API_Dropdown) config(ModConfigMenu); 2 | 3 | var name SettingName; 4 | var string Label; 5 | var string Tooltip; 6 | var bool Editable; 7 | 8 | var MCM_SettingGroup ParentGroup; 9 | 10 | var array Options; 11 | var int SelectionIndex; 12 | 13 | var delegate ChangeHandler; 14 | var delegate SaveHandler; 15 | 16 | var MCM_Dropdown uiInstance; 17 | 18 | delegate StringSettingHandler(MCM_API_Setting Setting, string _SettingValue); 19 | 20 | simulated function MCM_DropdownFacade InitDropdownFacade(name _Name, string _Label, string _Tooltip, 21 | array _Options, string _Selection, 22 | delegate _OnChange, delegate _OnSave, 23 | MCM_SettingGroup _ParentGroup) 24 | { 25 | SettingName = _Name; 26 | Label = _Label; 27 | Tooltip = _Tooltip; 28 | Editable = true; 29 | 30 | ParentGroup = _ParentGroup; 31 | 32 | CloneOptionsList(_Options); 33 | SelectionIndex = GetSelectionIndex(_Options, _Selection); 34 | 35 | ChangeHandler = _OnChange; 36 | SaveHandler = _OnSave; 37 | 38 | uiInstance = none; 39 | 40 | return self; 41 | } 42 | 43 | // Helpers 44 | 45 | function CloneOptionsList(array OptionsList) 46 | { 47 | local int iter; 48 | Options.Length = 0; 49 | for (iter = 0; iter < OptionsList.Length; iter++) 50 | { 51 | Options.AddItem(OptionsList[iter]); 52 | } 53 | } 54 | 55 | simulated function AfterParentPageDisplayed() 56 | { 57 | if (uiInstance != none) 58 | { 59 | uiInstance.AfterParentPageDisplayed(); 60 | } 61 | } 62 | 63 | function int GetSelectionIndex(array OptionsList, string SelectedOption) 64 | { 65 | local int iter; 66 | for (iter = 0; iter < OptionsList.Length; iter++) 67 | { 68 | if (SelectedOption == OptionsList[iter]) 69 | return iter; 70 | } 71 | 72 | return -1; 73 | } 74 | 75 | // MCM_SettingFacade implementation ================================================================= 76 | function UIMechaListItem InstantiateUI(UIList Parent) 77 | { 78 | uiInstance = Spawn(class'MCM_Dropdown', parent.itemContainer).InitDropdown(SettingName, self, Label, Tooltip, Options, Options[SelectionIndex], ChangeHandler); 79 | uiInstance.Show(); 80 | uiInstance.EnableNavigation(); 81 | uiInstance.SetEditable(Editable); 82 | 83 | return uiInstance; 84 | } 85 | 86 | function TriggerSaveEvent() 87 | { 88 | if (uiInstance != none) 89 | { 90 | SaveHandler(self, uiInstance.GetValue()); 91 | } 92 | else 93 | { 94 | SaveHandler(self, Options[SelectionIndex]); 95 | } 96 | } 97 | 98 | // MCM_API_Dropdown implementation ================================================================= 99 | 100 | function string GetValue() 101 | { 102 | return uiInstance != none ? uiInstance.GetValue() : Options[SelectionIndex]; 103 | } 104 | 105 | function SetValue(string Selection, bool SuppressEvent) 106 | { 107 | local int TmpIndex; 108 | 109 | if (uiInstance != none) 110 | { 111 | uiInstance.SetValue(Selection, SuppressEvent); 112 | } 113 | else 114 | { 115 | TmpIndex = GetSelectionIndex(Options, Selection); 116 | 117 | if (!SuppressEvent && TmpIndex >= 0 && TmpIndex != SelectionIndex) 118 | { 119 | ChangeHandler(self, Selection); 120 | } 121 | 122 | if (TmpIndex >= 0) 123 | { 124 | SelectionIndex = TmpIndex; 125 | } 126 | } 127 | } 128 | 129 | function SetOptions(array NewOptions, string InitialSelection, bool SuppressEvent) 130 | { 131 | local int TmpIndex; 132 | 133 | if (uiInstance != none) 134 | { 135 | uiInstance.SetOptions(NewOptions, InitialSelection, SuppressEvent); 136 | } 137 | else 138 | { 139 | CloneOptionsList(NewOptions); 140 | //SelectionIndex = GetSelectionIndex(_Options, _Selection); 141 | TmpIndex = GetSelectionIndex(NewOptions, InitialSelection); 142 | 143 | if (!SuppressEvent && TmpIndex >= 0 && TmpIndex != SelectionIndex) 144 | { 145 | ChangeHandler(self, Options[SelectionIndex]); 146 | } 147 | 148 | if (TmpIndex >= 0) 149 | { 150 | SelectionIndex = TmpIndex; 151 | } 152 | } 153 | } 154 | 155 | // MCM_API_Setting implementation ==================================================================== 156 | 157 | // Name is used for ID purposes, not for UI. 158 | function name GetName() 159 | { 160 | return uiInstance != none ? uiInstance.GetName() : SettingName; 161 | } 162 | 163 | // Label is used for UI purposes, not for ID. 164 | function SetLabel(string NewLabel) 165 | { 166 | if (uiInstance != none) 167 | { 168 | uiInstance.SetLabel(NewLabel); 169 | } 170 | else 171 | { 172 | Label = NewLabel; 173 | } 174 | } 175 | 176 | function string GetLabel() 177 | { 178 | return uiInstance != none ? uiInstance.GetLabel() : Label; 179 | } 180 | 181 | // When you mouse-over the setting. 182 | function SetHoverTooltip(string _Tooltip) 183 | { 184 | if (uiInstance != none) 185 | { 186 | uiInstance.SetHoverTooltip(_Tooltip); 187 | } 188 | else 189 | { 190 | Tooltip = _Tooltip; 191 | } 192 | } 193 | 194 | function string GetHoverTooltip() 195 | { 196 | return uiInstance != none ? uiInstance.GetHoverTooltip() : Tooltip; 197 | } 198 | 199 | // Lets you show an option but disable it because it shouldn't be configurable. 200 | // For example, if you don't want to allow tweaking during a mission. 201 | function SetEditable(bool IsEditable) 202 | { 203 | if (uiInstance != none) 204 | { 205 | uiInstance.SetEditable(IsEditable); 206 | } 207 | else 208 | { 209 | Editable = IsEditable; 210 | } 211 | } 212 | 213 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 214 | // future "extension types". 215 | function int GetSettingType() 216 | { 217 | return eSettingType_Dropdown; 218 | } 219 | 220 | function MCM_API_SettingsGroup GetParentGroup() 221 | { 222 | return ParentGroup; 223 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_SliderFacade.uc: -------------------------------------------------------------------------------- 1 | class MCM_SliderFacade extends Actor implements(MCM_SettingFacade, MCM_API_Setting, MCM_API_Slider) config(ModConfigMenu); 2 | 3 | var name SettingName; 4 | var string Label; 5 | var string Tooltip; 6 | var bool Editable; 7 | 8 | var MCM_SettingGroup ParentGroup; 9 | 10 | var float SliderMin; 11 | var float SliderMax; 12 | var float SliderStep; 13 | var float SliderValue; 14 | 15 | var delegate ChangeHandler; 16 | var delegate SaveHandler; 17 | 18 | var delegate DisplayFilter; 19 | 20 | var MCM_Slider uiInstance; 21 | 22 | delegate FloatSettingHandler(MCM_API_Setting Setting, float _SettingValue); 23 | delegate string SliderValueDisplayFilter(float value); 24 | 25 | simulated function MCM_SliderFacade InitSliderFacade(name _Name, string _Label, string _Tooltip, 26 | float sMin, float sMax, float sStep, float sValue, 27 | delegate _OnChange, delegate _OnSave, 28 | MCM_SettingGroup _ParentGroup) 29 | { 30 | SettingName = _Name; 31 | ParentGroup = _ParentGroup; 32 | ChangeHandler = _OnChange; 33 | SaveHandler = _OnSave; 34 | DisplayFilter = none; 35 | uiInstance = none; 36 | 37 | SetLabel(_Label); 38 | SetHoverTooltip(_Tooltip); 39 | SetEditable(true); 40 | SetBounds(sMin, sMax, sStep, sValue, true); 41 | 42 | return self; 43 | } 44 | 45 | function string InnerDisplayFilter(float v) 46 | { 47 | if (DisplayFilter == none) 48 | { 49 | return DefaultDisplayFilter(v); 50 | } 51 | else 52 | { 53 | return DisplayFilter(v); 54 | } 55 | } 56 | 57 | function string DefaultDisplayFilter(float v) 58 | { 59 | local float min, max, step, ignorevalue; 60 | GetBounds(min, max, step, ignorevalue); 61 | return class'MCM_Slider'.static.DefaultFormatValueForDisplay(min, max, step, v); 62 | } 63 | 64 | // MCM_SettingFacade implementation ================================================================= 65 | function UIMechaListItem InstantiateUI(UIList Parent) 66 | { 67 | uiInstance = Spawn(class'MCM_Slider', parent.itemContainer).InitSlider(SettingName, self, Label, Tooltip, 68 | SliderMin, SliderMax, SliderStep, SliderValue, ChangeHandler); 69 | uiInstance.Show(); 70 | uiInstance.EnableNavigation(); 71 | uiInstance.SetEditable(Editable); 72 | // Always have one implemented. 73 | uiInstance.SetValueDisplayFilter(InnerDisplayFilter); 74 | 75 | return uiInstance; 76 | } 77 | 78 | simulated function AfterParentPageDisplayed() 79 | { 80 | if (uiInstance != none) 81 | { 82 | uiInstance.AfterParentPageDisplayed(); 83 | } 84 | } 85 | 86 | function TriggerSaveEvent() 87 | { 88 | if (uiInstance != none) 89 | { 90 | SaveHandler(self, uiInstance.GetValue()); 91 | } 92 | else 93 | { 94 | SaveHandler(self, SliderValue); 95 | } 96 | } 97 | 98 | // MCM_SettingDropdown implementation ================================================================== 99 | 100 | function float GetValue() 101 | { 102 | if (uiInstance != none) 103 | { 104 | return uiInstance.GetValue(); 105 | } 106 | else 107 | { 108 | return SliderValue; 109 | } 110 | } 111 | 112 | function SetValue(float Value, bool SuppressEvent) 113 | { 114 | if (uiInstance != none) 115 | { 116 | uiInstance.SetValue(Value, SuppressEvent); 117 | } 118 | else 119 | { 120 | SliderValue = class'MCM_Slider'.static.ClampAndSnapValue(SliderMin, SliderMax, SliderStep, Value); 121 | if (!SuppressEvent) 122 | { 123 | ChangeHandler(self, SliderValue); 124 | } 125 | } 126 | } 127 | 128 | function GetBounds(out float sMin, out float sMax, out float sStep, out float sValue) 129 | { 130 | if (uiInstance != none) 131 | { 132 | uiInstance.GetBounds(sMin, sMax, sStep, sValue); 133 | } 134 | else 135 | { 136 | sMin = SliderMin; 137 | sMax = SliderMax; 138 | sStep = SliderStep; 139 | sValue = SliderValue; 140 | } 141 | } 142 | 143 | function SetBounds(float min, float max, float step, float newValue, bool SuppressEvent) 144 | { 145 | if (uiInstance != none) 146 | { 147 | uiInstance.SetBounds(min, max, step, newValue, SuppressEvent); 148 | } 149 | else 150 | { 151 | SliderMin = min; 152 | SliderMax = max; 153 | SliderStep = step; 154 | SliderValue = class'MCM_Slider'.static.ClampAndSnapValue(SliderMin, SliderMax, SliderStep, newValue); 155 | 156 | if (!SuppressEvent) 157 | { 158 | ChangeHandler(self, SliderValue); 159 | } 160 | } 161 | } 162 | 163 | function SetValueDisplayFilter(delegate _DisplayFilter) 164 | { 165 | DisplayFilter = _DisplayFilter; 166 | } 167 | 168 | // MCM_API_Setting implementation ==================================================================== 169 | 170 | // Name is used for ID purposes, not for UI. 171 | function name GetName() 172 | { 173 | return uiInstance != none ? uiInstance.GetName() : SettingName; 174 | } 175 | 176 | // Label is used for UI purposes, not for ID. 177 | function SetLabel(string NewLabel) 178 | { 179 | if (uiInstance != none) 180 | { 181 | uiInstance.SetLabel(NewLabel); 182 | } 183 | else 184 | { 185 | Label = NewLabel; 186 | } 187 | } 188 | 189 | function string GetLabel() 190 | { 191 | return uiInstance != none ? uiInstance.GetLabel() : Label; 192 | } 193 | 194 | // When you mouse-over the setting. 195 | function SetHoverTooltip(string _Tooltip) 196 | { 197 | if (uiInstance != none) 198 | { 199 | uiInstance.SetHoverTooltip(_Tooltip); 200 | } 201 | else 202 | { 203 | Tooltip = _Tooltip; 204 | } 205 | } 206 | 207 | function string GetHoverTooltip() 208 | { 209 | return uiInstance != none ? uiInstance.GetHoverTooltip() : Tooltip; 210 | } 211 | 212 | // Lets you show an option but disable it because it shouldn't be configurable. 213 | // For example, if you don't want to allow tweaking during a mission. 214 | function SetEditable(bool IsEditable) 215 | { 216 | if (uiInstance != none) 217 | { 218 | uiInstance.SetEditable(IsEditable); 219 | } 220 | else 221 | { 222 | Editable = IsEditable; 223 | } 224 | } 225 | 226 | // Retrieves underlying setting type. Defined as an int to make setting types more extensible to support 227 | // future "extension types". 228 | function int GetSettingType() 229 | { 230 | return eSettingType_Slider; 231 | } 232 | 233 | function MCM_API_SettingsGroup GetParentGroup() 234 | { 235 | return ParentGroup; 236 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_SettingGroup.uc: -------------------------------------------------------------------------------- 1 | // This is an Unreal Script 2 | class MCM_SettingGroup extends Actor implements(MCM_API_SettingsGroup); 3 | 4 | var name GroupName; 5 | var MCM_GroupLabel GroupLabel; 6 | var array Settings; 7 | 8 | var MCM_SettingsPanelFacade ParentPanel; 9 | 10 | var bool UiInstantiated; 11 | 12 | delegate VoidSettingHandler(MCM_API_Setting Setting); 13 | delegate BoolSettingHandler(MCM_API_Setting Setting, bool SettingValue); 14 | delegate FloatSettingHandler(MCM_API_Setting Setting, float SettingValue); 15 | delegate StringSettingHandler(MCM_API_Setting Setting, string SettingValue); 16 | 17 | //delegate ListItemHandler(UIMechaListItem item); 18 | delegate ListItemHandler(UIPanel item); 19 | 20 | function MCM_SettingGroup InitSettingGroup(name _GroupName, string Label, MCM_SettingsPanelFacade _ParentPanel) 21 | { 22 | GroupName = _GroupName; 23 | ParentPanel = _ParentPanel; 24 | GroupLabel = Spawn(class'MCM_GroupLabel', self).InitGroupLabel(Label); 25 | UiInstantiated = false; 26 | 27 | return self; 28 | } 29 | 30 | // Helpers ========================================================================== 31 | 32 | function TriggerSaveEvents() 33 | { 34 | local int iter; 35 | for (iter = 0; iter < Settings.Length; iter++) 36 | { 37 | Settings[iter].TriggerSaveEvent(); 38 | } 39 | } 40 | 41 | function AddSetting(MCM_SettingFacade Instance) 42 | { 43 | if (UiInstantiated) 44 | { 45 | `log("MCM: Error: Cannot add more settings after ShowSettings() has been called."); 46 | } 47 | else 48 | { 49 | Settings.AddItem(Instance); 50 | } 51 | } 52 | 53 | // This is a signal to actually create the UI items. 54 | function InstantiateItems(delegate handler, UIList Parent) 55 | { 56 | local int iter; 57 | local MCM_SettingFacade TmpItem; 58 | 59 | if (UiInstantiated) 60 | return; 61 | 62 | // now items in reverse order. 63 | for (iter = Settings.Length-1; iter >= 0; iter--) 64 | { 65 | TmpItem = Settings[iter]; 66 | handler(TmpItem.InstantiateUI(Parent)); 67 | } 68 | 69 | // Group header last. 70 | handler(GroupLabel.InstantiateUI(Parent)); 71 | 72 | UiInstantiated = true; 73 | } 74 | 75 | simulated function AfterParentPageDisplayed() 76 | { 77 | local int iter; 78 | local MCM_SettingFacade TmpItem; 79 | 80 | if (UiInstantiated) 81 | { 82 | for (iter = 0; iter < Settings.Length; iter++) 83 | { 84 | TmpItem = Settings[iter]; 85 | TmpItem.AfterParentPageDisplayed(); 86 | } 87 | } 88 | } 89 | 90 | // MCM_API_SettingsGroup implementation ============================================= 91 | 92 | // For reference purposes, not display purposes. 93 | function name GetName() 94 | { 95 | return GroupName; 96 | } 97 | 98 | // For display purposes, not reference purposes. 99 | function string GetLabel() 100 | { 101 | return GroupLabel.GetGroupLabel(); 102 | } 103 | 104 | function SetLabel(string Label) 105 | { 106 | GroupLabel.SetGroupLabel(Label); 107 | } 108 | 109 | function MCM_API_SettingsPage GetParentPage() 110 | { 111 | return ParentPanel; 112 | } 113 | 114 | // Will return None if setting by that name isn't found. 115 | function MCM_API_Setting GetSettingByName(name _SettingName) 116 | { 117 | local int iter; 118 | local MCM_SettingFacade Tmp; 119 | 120 | for (iter = 0; iter < Settings.Length; iter++) 121 | { 122 | Tmp = Settings[iter]; 123 | if (Tmp.GetName() == _SettingName) 124 | { 125 | return Tmp; 126 | } 127 | } 128 | 129 | return none; 130 | } 131 | 132 | function MCM_API_Setting GetSettingByIndex(int Index) 133 | { 134 | if (Index >= 0 && Index < Settings.Length) 135 | { 136 | return Settings[Index]; 137 | } 138 | else 139 | { 140 | return none; 141 | } 142 | } 143 | 144 | function int GetNumberOfSettings() 145 | { 146 | return Settings.Length; 147 | } 148 | 149 | // The complicated stuff 150 | 151 | function MCM_API_Label AddLabel(name SettingName, string Label, string Tooltip) 152 | { 153 | local MCM_LabelFacade Instance; 154 | 155 | Instance = Spawn(class'MCM_LabelFacade', self).InitLabelFacade(SettingName, Label, Tooltip, self); 156 | AddSetting(Instance); 157 | 158 | return Instance; 159 | } 160 | 161 | function MCM_API_Button AddButton(name SettingName, string Label, string Tooltip, string ButtonLabel, 162 | optional delegate ClickHandler) 163 | { 164 | local MCM_ButtonFacade Instance; 165 | 166 | Instance = Spawn(class'MCM_ButtonFacade', self).InitButtonFacade(SettingName, Label, Tooltip, ButtonLabel, ClickHandler, self); 167 | AddSetting(Instance); 168 | 169 | return Instance; 170 | } 171 | 172 | function MCM_API_Checkbox AddCheckbox(name SettingName, string Label, string Tooltip, bool InitiallyChecked, 173 | optional delegate SaveHandler, 174 | optional delegate ChangeHandler) 175 | { 176 | local MCM_CheckboxFacade Instance; 177 | 178 | Instance = Spawn(class'MCM_CheckboxFacade', self).InitCheckboxFacade(SettingName, Label, Tooltip, InitiallyChecked, ChangeHandler, SaveHandler, self); 179 | AddSetting(Instance); 180 | 181 | return Instance; 182 | } 183 | 184 | function MCM_API_Slider AddSlider(name SettingName, string Label, string Tooltip, float SliderMin, float SliderMax, float SliderStep, float InitialValue, 185 | optional delegate SaveHandler, 186 | optional delegate ChangeHandler) 187 | { 188 | local MCM_SliderFacade Instance; 189 | 190 | Instance = Spawn(class'MCM_SliderFacade', self).InitSliderFacade(SettingName, Label, Tooltip, 191 | SliderMin, SliderMax, SliderStep, InitialValue, ChangeHandler, SaveHandler, self); 192 | AddSetting(Instance); 193 | 194 | return Instance; 195 | } 196 | 197 | function MCM_API_Spinner AddSpinner(name SettingName, string Label, string Tooltip, array Options, string Selection, 198 | optional delegate SaveHandler, 199 | optional delegate ChangeHandler) 200 | { 201 | local MCM_SpinnerFacade Instance; 202 | 203 | Instance = Spawn(class'MCM_SpinnerFacade', self).InitSpinnerFacade(SettingName, Label, Tooltip, 204 | Options, Selection, ChangeHandler, SaveHandler, self); 205 | AddSetting(Instance); 206 | 207 | return Instance; 208 | } 209 | 210 | function MCM_API_Dropdown AddDropdown(name SettingName, string Label, string Tooltip, array Options, string Selection, 211 | optional delegate SaveHandler, 212 | optional delegate ChangeHandler) 213 | { 214 | local MCM_DropdownFacade Instance; 215 | 216 | Instance = Spawn(class'MCM_DropdownFacade', self).InitDropdownFacade(SettingName, Label, Tooltip, 217 | Options, Selection, ChangeHandler, SaveHandler, self); 218 | AddSetting(Instance); 219 | 220 | return Instance; 221 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_SettingsPanel.uc: -------------------------------------------------------------------------------- 1 | class MCM_SettingsPanel extends UIPanel implements(MCM_API_SettingsPage) config(ModConfigMenu); 2 | 3 | var config int PANEL_HEIGHT; 4 | var config int PANEL_WIDTH; 5 | var config int FOOTER_HEIGHT; 6 | var config int RESET_BUTTON_X; 7 | var config int APPLY_BUTTON_X; 8 | var config int REVERT_BUTTON_X; 9 | 10 | var localized string m_strResetButton; 11 | var localized string m_strRevertButton; 12 | var localized string m_strApplyButton; 13 | 14 | var int SettingsPageID; 15 | 16 | // Parent facade 17 | var MCM_SettingsPanelFacade Facade; 18 | 19 | var UIList SettingsList; 20 | 21 | var MCM_UISettingSeparator TitleLine; 22 | var UIButton ResetButton; 23 | var string Title; 24 | 25 | var array SettingGroups; 26 | //var float SettingItemStartY; 27 | 28 | var delegate ResetHandler; 29 | var delegate CancelHandler; 30 | var delegate SaveHandler; 31 | 32 | delegate SettingChangedHandler(MCM_API_Setting Setting); 33 | delegate SaveStateHandler(MCM_API_SettingsPage SettingsPage); 34 | 35 | simulated function UIPanel InitPanel(optional name InitName, optional name InitLibID) 36 | { 37 | super.InitPanel(InitName, InitLibID); 38 | 39 | SetSize(PANEL_WIDTH, PANEL_HEIGHT); 40 | 41 | SettingsList = Spawn(class'UIList', self).InitList('OptionsList', 0, 0, PANEL_WIDTH, PANEL_HEIGHT - FOOTER_HEIGHT - 40); 42 | //SettingsList = Spawn(class'UIList', self).InitList('OptionsList', 0, 0, PANEL_WIDTH, PANEL_HEIGHT - FOOTER_HEIGHT - 20, , true); 43 | // Necessary to make sure dropdowns don't run past the bottom. 44 | //SettingsList.ScrollbarPadding = 500; 45 | SettingsList.SetSelectedNavigation(); 46 | SettingsList.Navigator.LoopSelection = true; 47 | 48 | // Delay spawning of title line to make sure topmost "line" is also last layer. 49 | // See ShowSettings(); 50 | TitleLine = none; 51 | //TitleLine = Spawn(class'MCM_UISettingSeparator', SettingsList.itemContainer); 52 | //TitleLine.InitSeparator(); 53 | //TitleLine.UpdateTitle("Mod Settings"); 54 | //TitleLine.SetY(0); 55 | //TitleLine.Show(); 56 | //TitleLine.EnableNavigation(); 57 | 58 | //SettingItemStartY = TitleLine.Height; 59 | 60 | ResetButton = Spawn(class'UIButton', self); 61 | ResetButton.InitButton(, m_strResetButton, OnResetClicked); 62 | ResetButton.SetPosition(RESET_BUTTON_X, PANEL_HEIGHT - FOOTER_HEIGHT + 3); //Relative to this screen panel 63 | ResetButton.Hide(); 64 | 65 | ResetHandler = none; 66 | SaveHandler = none; 67 | CancelHandler = none; 68 | 69 | return self; 70 | } 71 | 72 | simulated function OnInit() 73 | { 74 | super.OnInit(); 75 | } 76 | 77 | simulated function OnResetClicked(UIButton kButton) 78 | { 79 | if (ResetHandler != none) 80 | { 81 | //`log("MCM: Reset button clicked"); 82 | // Use the Facade object since it's the complete MCM_API_SettingsPage implementation. 83 | ResetHandler(Facade); 84 | } 85 | } 86 | 87 | simulated function Show() 88 | { 89 | local MCM_SettingGroup iter; 90 | 91 | super.Show(); 92 | 93 | // Now that it's visible, need to trigger the post-visibility update. 94 | foreach SettingGroups(iter) 95 | { 96 | iter.AfterParentPageDisplayed(); 97 | } 98 | } 99 | 100 | // Helpers for MCM_OptionsScreen ================================================================ 101 | 102 | simulated function TriggerSaveEvent() 103 | { 104 | local MCM_SettingGroup iter; 105 | 106 | foreach SettingGroups(iter) 107 | { 108 | iter.TriggerSaveEvents(); 109 | } 110 | 111 | if (SaveHandler != none) 112 | { 113 | // Use the Facade object since it's the complete MCM_API_SettingsPage implementation. 114 | SaveHandler(Facade); 115 | } 116 | } 117 | 118 | simulated function TriggerCancelEvent() 119 | { 120 | if (CancelHandler != none) 121 | { 122 | // Use the Facade object since it's the complete MCM_API_SettingsPage implementation. 123 | CancelHandler(Facade); 124 | } 125 | } 126 | 127 | // MCM_API_SettingsPage implementation =========================================== 128 | 129 | function int GetPageId() 130 | { 131 | return SettingsPageID; 132 | } 133 | 134 | // To do : probably add description to this function too ? Super d 135 | function SetPageTitle(string newTitle) 136 | { 137 | if(TitleLine != none) 138 | { 139 | TitleLine.UpdateTitle(newTitle); 140 | } 141 | Title = newTitle; 142 | } 143 | 144 | function SetSaveHandler(delegate _SaveHandler) 145 | { 146 | local MCM_SettingGroup grp; 147 | foreach SettingGroups(grp) 148 | { 149 | grp.TriggerSaveEvents(); 150 | } 151 | 152 | SaveHandler = _SaveHandler; 153 | } 154 | 155 | function SetCancelHandler(delegate _CancelHandler) 156 | { 157 | CancelHandler = _CancelHandler; 158 | } 159 | 160 | function EnableResetButton(delegate _ResetHandler) 161 | { 162 | ResetHandler = _ResetHandler; 163 | ResetButton.Show(); 164 | } 165 | 166 | // Function is implemented by the facade. 167 | function MCM_API_SettingsGroup AddGroup(name GroupName, string GroupLabel) 168 | { 169 | `log("MCM: Cannot add settings group because the UI is already fixed and instantiated."); 170 | return none; 171 | } 172 | 173 | // Function is implemented by the facade. 174 | function MCM_API_SettingsGroup GetGroupByName(name GroupName) 175 | { 176 | return Facade.GetGroupByName(GroupName); 177 | } 178 | 179 | // Function is implemented by the facade. 180 | function MCM_API_SettingsGroup GetGroupByIndex(int Index) 181 | { 182 | return Facade.GetGroupByIndex(Index); 183 | } 184 | 185 | // Function is implemented by the facade. 186 | function int GetGroupCount() 187 | { 188 | return Facade.GetGroupCount(); 189 | } 190 | 191 | // Assumes that groups are iterated in reverse order and items in groups are inserted in reverse order. 192 | //function OnSettingsLineInitialized(UIMechaListItem NextItem) 193 | function OnSettingsLineInitialized(UIPanel NextItem) 194 | { 195 | SettingsList.MoveItemToTop(NextItem); 196 | } 197 | 198 | function ShowSettings() 199 | { 200 | // This is where magic happens. 201 | local int groupIndex; 202 | local UIImage bottomPadding; 203 | 204 | // Adds padding at bottom to make sure that bottom options are visisble. 205 | bottomPadding = Spawn(class'UIImage', SettingsList.itemContainer); 206 | bottomPadding.bProcessesMouseEvents = true; 207 | bottomPadding.InitImage('MCMBottomPadding',"img:///MCM.gfx.Transparent"); 208 | bottomPadding.SetWidth(548); 209 | bottomPadding.SetHeight(150); 210 | OnSettingsLineInitialized(bottomPadding); 211 | 212 | for (groupIndex = SettingGroups.Length - 1; groupIndex >= 0; groupIndex--) 213 | { 214 | SettingGroups[groupIndex].InstantiateItems(OnSettingsLineInitialized, SettingsList); 215 | } 216 | 217 | TitleLine = Spawn(class'MCM_UISettingSeparator', SettingsList.itemContainer); 218 | TitleLine.InitSeparator(); 219 | TitleLine.UpdateTitle(Title != "" ? Title : "Mod Settings"); 220 | TitleLine.SetY(0); 221 | TitleLine.Show(); 222 | TitleLine.EnableNavigation(); 223 | SettingsList.MoveItemToTop(TitleLine); 224 | } 225 | 226 | defaultproperties 227 | { 228 | bProcessesMouseEvents = false; 229 | } -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/ModConfigMenu.x2proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fa9b04b1-c447-4ce3-b608-d6f7a7d0e648 5 | Mod Config Menu 6 | This mod (MCM) is an optional prerequisite for certain mods that enable in-game config options. 7 | 8 | For full details on using this mod, troubleshooting, or even building your own mods to use this menu, visit: https://github.com/andrewgu/ModConfigMenu 9 | 0 10 | ExampleClassOverride 11 | ExampleClassOverride 12 | {3f350978-33e7-493a-993b-92c1b54bf591} 13 | 14 | 15 | bin\Debug\ 16 | 17 | 18 | 19 | 20 | 21 | Content 22 | 23 | 24 | Content 25 | 26 | 27 | Content 28 | 29 | 30 | Content 31 | 32 | 33 | 34 | 35 | Content 36 | 37 | 38 | Content 39 | 40 | 41 | Content 42 | 43 | 44 | Content 45 | 46 | 47 | Content 48 | 49 | 50 | Content 51 | 52 | 53 | Content 54 | 55 | 56 | Content 57 | 58 | 59 | Content 60 | 61 | 62 | Content 63 | 64 | 65 | Content 66 | 67 | 68 | Content 69 | 70 | 71 | Content 72 | 73 | 74 | Content 75 | 76 | 77 | Content 78 | 79 | 80 | Content 81 | 82 | 83 | Content 84 | 85 | 86 | Content 87 | 88 | 89 | Content 90 | 91 | 92 | Content 93 | 94 | 95 | Content 96 | 97 | 98 | Content 99 | 100 | 101 | Content 102 | 103 | 104 | Content 105 | 106 | 107 | Content 108 | 109 | 110 | Content 111 | 112 | 113 | Content 114 | 115 | 116 | Content 117 | 118 | 119 | Content 120 | 121 | 122 | Content 123 | 124 | 125 | Content 126 | 127 | 128 | Content 129 | 130 | 131 | Content 132 | 133 | 134 | Content 135 | 136 | 137 | Content 138 | 139 | 140 | Content 141 | 142 | 143 | Content 144 | 145 | 146 | Content 147 | 148 | 149 | Content 150 | 151 | 152 | Content 153 | 154 | 155 | Content 156 | 157 | 158 | Content 159 | 160 | 161 | Content 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_TestHarness.uc: -------------------------------------------------------------------------------- 1 | //class MCM_TestHarness extends UIScreenListener config(ModConfigMenuTestHarness); 2 | class MCM_TestHarness extends Object config(ModConfigMenuTestHarness); 3 | 4 | `include(ModConfigMenu/Src/ModConfigMenuAPI/MCM_API_Includes.uci) 5 | `include(ModConfigMenu/Src/ModConfigMenuAPI/MCM_API_CfgHelpers.uci) 6 | 7 | var config bool CFG_CLICKED; 8 | var config bool CFG_CHECKBOX; 9 | var config float CFG_SLIDER; 10 | var config string CFG_DROPDOWN; 11 | var config string CFG_SPINNER; 12 | 13 | var MCM_API APIInst; 14 | 15 | var MCM_API_Label P1Label; 16 | var MCM_API_Button P1Button; 17 | var MCM_API_Checkbox P1Checkbox; 18 | var MCM_API_Slider P2Slider; 19 | var MCM_API_Dropdown P2Dropdown; 20 | var MCM_API_Spinner P2Spinner; 21 | 22 | var MCM_API_SettingsPage Page1; 23 | 24 | var config int MCM_CH_IMPL_CFG_VERSION; 25 | 26 | `MCM_CH_VersionChecker(class'MCM_TestConfigStore'.default.VERSION,MCM_CH_IMPL_CFG_VERSION) 27 | 28 | //event OnInit(UIScreen Screen) 29 | function OnInit(UIScreen Screen) 30 | { 31 | // Only the actual screen you want to hook onto will implement MCM_API, so this check lets you hook in. 32 | if (MCM_API(Screen) != none) 33 | { 34 | //if (!(class'MCM_TestConfigStore'.default.ENABLE_TEST_HARNESS)) 35 | //{ 36 | // `log("MCM Test Harness Disabled."); 37 | // return; 38 | //} 39 | 40 | // Workaround that's needed in order to be able to "save" files. 41 | LoadInitialValues(); 42 | 43 | // Use the macro because it automates the version check based on the API version you're compiling against. 44 | `MCM_API_Register(Screen, ClientModCallback); 45 | } 46 | } 47 | 48 | function ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode) 49 | { 50 | local MCM_API_SettingsGroup P1G1, P1G2, P1G3; 51 | local array Options; 52 | 53 | if (GameMode == eGameMode_MainMenu || GameMode == eGameMode_Strategy || GameMode == eGameMode_Tactical) 54 | { 55 | `log("Is in main menu or strategy menu, attempting to make page."); 56 | 57 | Page1 = ConfigAPI.NewSettingsPage("MCM_Test_1"); 58 | Page1.SetPageTitle("Page 1"); 59 | Page1.SetSaveHandler(SaveButtonClicked); 60 | Page1.SetCancelHandler(RevertButtonClicked); 61 | Page1.EnableResetButton(ResetButtonClicked); 62 | 63 | P1G1 = Page1.AddGroup('MCM_Test_P1_G1', "General Settings"); 64 | P1G2 = Page1.AddGroup('MCM_Test_P1_G2', "Group 1"); 65 | P1G3 = Page1.AddGroup('MCM_Test_P1_G3', "Group 2"); 66 | 67 | P1Label = P1G1.AddLabel('label', "Label", "Label"); 68 | P1Button = P1G1.AddButton('button', "Button", "Button", "OK", ButtonClickedHandler); 69 | P1Checkbox = P1G1.AddCheckbox('checkbox', "Checkbox", "Checkbox", CFG_CHECKBOX, CheckboxSaveLogger); 70 | 71 | P2Slider = P1G2.AddSlider('slider', "Really long description here", "Slider", 0, 200, 20, CFG_SLIDER, SliderSaveLogger); 72 | P2Slider.SetValueDisplayFilter(SliderValueDisplayFilter); 73 | 74 | Options.Length = 0; 75 | Options.AddItem("a"); 76 | Options.AddItem("b"); 77 | Options.AddItem("c"); 78 | Options.AddItem("d"); 79 | Options.AddItem("e"); 80 | Options.AddItem("f"); 81 | Options.AddItem("g"); 82 | 83 | P2Spinner = P1G3.AddSpinner('spinner', "Really long description here", "Spinner", Options, CFG_SPINNER, SpinnerSaveLogger); 84 | P2Dropdown = P1G3.AddDropdown('dropdown', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, DropdownSaveLogger); 85 | 86 | // Dummies to fill out the page for scrolling. 87 | P1G3.AddDropdown('dropdown1', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 88 | P1G3.AddDropdown('dropdown2', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 89 | P1G3.AddDropdown('dropdown3', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 90 | P1G3.AddDropdown('dropdown4', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 91 | P1G3.AddDropdown('dropdown5', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 92 | P1G3.AddDropdown('dropdown6', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 93 | P1G3.AddDropdown('dropdown7', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 94 | P1G3.AddDropdown('dropdown8', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 95 | P1G3.AddDropdown('dropdown9', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 96 | P1G3.AddDropdown('dropdown10', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 97 | P1G3.AddDropdown('dropdown11', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 98 | P1G3.AddDropdown('dropdown12', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 99 | P1G3.AddDropdown('dropdown13', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 100 | P1G3.AddDropdown('dropdown14', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 101 | P1G3.AddDropdown('dropdown15', "Really long description here", "Dropdown", Options, CFG_DROPDOWN, none); 102 | 103 | 104 | if (GameMode == eGameMode_Strategy) 105 | P1Checkbox.SetEditable(false); 106 | 107 | Page1.ShowSettings(); 108 | } 109 | } 110 | 111 | `MCM_API_BasicCheckboxSaveHandler(CheckboxSaveLogger, CFG_CHECKBOX) 112 | `MCM_API_BasicSliderSaveHandler(SliderSaveLogger, CFG_SLIDER) 113 | `MCM_API_BasicDropdownSaveHandler(DropdownSaveLogger, CFG_DROPDOWN) 114 | `MCM_API_BasicSpinnerSaveHandler(SpinnerSaveLogger, CFG_SPINNER) 115 | 116 | `MCM_API_BasicButtonHandler(ButtonClickedHandler) 117 | { 118 | //P2Slider.SetBounds(0, 100, 50, P2Slider.GetValue(), false); 119 | P2Slider.SetHoverTooltip("asdf"); 120 | CFG_CLICKED = true; 121 | } 122 | 123 | function int RoundFloat(float _v) 124 | { 125 | if (_v >= 0) 126 | return int(_v + 0.5); 127 | else 128 | return int (_v - 0.5); 129 | } 130 | 131 | function string SliderValueDisplayFilter(float _value) 132 | { 133 | return string(RoundFloat(_value)); 134 | } 135 | 136 | function SaveButtonClicked(MCM_API_SettingsPage Page) 137 | { 138 | `log("MCM: Save button clicked (TestHarness)"); 139 | 140 | self.MCM_CH_IMPL_CFG_VERSION = `MCM_CH_GetCompositeVersion(); 141 | self.SaveConfig(); 142 | } 143 | 144 | function ResetButtonClicked(MCM_API_SettingsPage Page) 145 | { 146 | `log("MCM: Reset button clicked"); 147 | 148 | // Revert all of the settings. 149 | CFG_CLICKED = false; 150 | P1Checkbox.SetValue(CFG_CHECKBOX, true); 151 | P2Slider.SetValue(CFG_SLIDER, true); 152 | P2Dropdown.SetValue(CFG_DROPDOWN, true); 153 | P2Spinner.SetValue(CFG_SPINNER, true); 154 | } 155 | 156 | function RevertButtonClicked(MCM_API_SettingsPage Page) 157 | { 158 | // Don't need to do anything since values aren't written until at save-time when you use save handlers. 159 | `log("MCM: Cancel button clicked (TestHarness)"); 160 | } 161 | 162 | // This shows how to either pull default values from a source config, or to use more user-defined values, gated by a version number mechanism. 163 | function LoadInitialValues() 164 | { 165 | CFG_CLICKED = false; 166 | CFG_CHECKBOX = `MCM_CH_GetValue(class'MCM_TestConfigStore'.default.CHECKBOX,CFG_CHECKBOX); 167 | CFG_SLIDER = `MCM_CH_GetValue(class'MCM_TestConfigStore'.default.SLIDER,CFG_SLIDER); 168 | CFG_DROPDOWN = `MCM_CH_GetValue(class'MCM_TestConfigStore'.default.DROPDOWN,CFG_DROPDOWN); 169 | CFG_SPINNER = `MCM_CH_GetValue(class'MCM_TestConfigStore'.default.SPINNER,CFG_SPINNER); 170 | } 171 | 172 | defaultproperties 173 | { 174 | // Need this because you won't be able to listen for a concrete class type that doesn't exist yet. 175 | //ScreenClass = none; 176 | } -------------------------------------------------------------------------------- /documentation/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced Features 2 | 3 | This is best read alongside the API documentation. We designed the API to maximize 4 | generality while keeping the API clean and modular, so really these features are 5 | just mixing and matching parts of the API in ways that the developers (yours truly!) anticipated. 6 | 7 | ### More than one Settings Page per mod 8 | 9 | Most mods shouldn't need more than one settings page, but you can do it for big mods or 10 | collection mods where it makes sense for the settings to be split up into multiple pages. 11 | 12 | To do this, just call `MCM_API_Instance.NewSettingsPage()` multiple times. 13 | You will be responsible for keeping track of the `MCM_API_SettingsPage` instances. If you 14 | use the same event handlers for multiple pages, it's up to you to remember which page is which. 15 | Either keep references to the MCM_API_SettingsPage objects, or remember their PageID's and use 16 | `GetSettingsPageByID(int PageID)` to retrieve the Page objects. 17 | 18 | #### A warning about PageID 19 | 20 | PageID is generated internally by MCM. It's given to you to give you a convenient way to reference 21 | Page objects, but there is no guarantee that the PageID for a settings page will persist beyond the 22 | lifetime of the settings page itself. The PageID might even change if the user clicks "Save and Exit" 23 | and reopens the "Mod Settings" menu. 24 | 25 | While in theory you could use the PageID and `GetSettingsByPageID` to break into other mods, I highly 26 | advise against doing so because the PageID is not a stable value. 27 | 28 | If you have some way of passing the PageID between mods, perhaps using the same [technique that allows you 29 | to talk to MCM](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/sharedcode.md), then 30 | a more robust solution would be to pass the reference to a `MCM_API_SettingsPage` object instead. 31 | 32 | ### Adding Groups or Settings dynamically 33 | 34 | Right now this isn't supported. Don't try to work around it, because you will dig up UI bugs that 35 | the `MCM_API_SettingsPage.ShowSettings()` pattern is specifically designed to hide. Maybe we'll find 36 | a better way to do this in the future. 37 | 38 | That said, you can dynamically enable/disable settings. Disabled settings look gray and aren't editable. 39 | So if you want a workaround, enable/disable stuff instead of hiding/showing it. 40 | 41 | ### When a user changes a setting, update another setting 42 | 43 | This is probably most common when there's a checkbox that toggles a feature on or off. The feature itself 44 | might have associated settings you don't want to make accessible unless the feature is enabled. 45 | 46 | To do this, you will need to use "Change Handlers". This refers to the second of the two handlers that 47 | the `MCM_API_SettingsGroup.Add*****()` functions optionally. Here's an example function definition: 48 | 49 | ``` 50 | function MCM_API_Dropdown AddDropdown(name SettingName, 51 | string Label, string Tooltip, array Options, string Selection, 52 | optional delegate SaveHandler, 53 | optional delegate ChangeHandler); 54 | ``` 55 | 56 | In simple cases `SaveHandler` is enough because you only really need to know the values the user chose 57 | when you want to save the settings. But if you need to update the UI based on choices the user is making 58 | before the user saves anything, then you should use the `ChangeHandler`, which is triggered every time 59 | a value changes, rather than only when the user saves. `ChangeHandler` uses the same signature as `SaveHandler`. 60 | 61 | The only exception to "every time a value changes" is if you are manually setting a value and opt to 62 | suppress the event. For example, in `MCM_API_Dropdown` the `SetValue` function looks like this: 63 | 64 | ``` 65 | function SetValue(string Selection, bool SuppressEvent); 66 | ``` 67 | 68 | When `SuppressEvent` is set to true, ChangeHandler will not be called as a result of changing the value 69 | of the dropdown. If `SuppressEvent` is false, then ChangeHandler will be called. 70 | 71 | The only thing you can't do as a response to changing a setting is outright adding/removing settings or groups. 72 | You *can* enable/disable settings dynamically. 73 | 74 | ### "Reset to factory defaults" 75 | 76 | Since MCM can't automate much of the work involved in applying default settings, the reset button is disabled by default. 77 | 78 | `MCM_API_SettingsPage` has a function named `EnableResetButton`. If called, the reset button will be revealed. You'll 79 | have to decide how you want to implement this because MCM can't remember the defaults for you, but this option is there 80 | if you want to provide that convenience to your users. 81 | 82 | ### Custom Settings Widgets 83 | 84 | At the moment this is not supported. We may add support for this in a future version. Until then, please let us know 85 | in the Issues tracker if you think that the widget type you want would be useful to others. 86 | 87 | If you absolutely must do something custom, you have the option of implementing a custom settings page. 88 | 89 | ### Custom Settings Pages 90 | 91 | Custom Settings Pages give you a way to build your own settings page, but hook it into the MCM menu just to keep 92 | everything in a central place. Custom pages are treated as popups. You will have to handle revealing/hiding the custom page. 93 | To create a custom page, you need to call `MCM_API_Instance.NewCustomSettingsPage`: 94 | 95 | ``` 96 | function int NewCustomSettingsPage(string TabLabel, delegate Handler); 97 | ``` 98 | 99 | `TabLabel` will be the name that appears on the left of the menu. `Handler` gets called when the user selects the tab. Your 100 | job is to implement the dialog that popups up and to gracefully handle returning control back to MCM when the user exits 101 | your custom settings page. You will have to implement your own saving/cancelling/resetting mechanism. 102 | 103 | An example of how to use `NewCustomSettingsPage` can be found in the `MCM_CustomPageTest.uc` and `MC_CustomPageTestUI.uc` 104 | files in the `ModConfigMenu` project. 105 | 106 | ### Limit options for main menu / Avenger screen / In-mission screen 107 | 108 | MCM will tell you about the game mode through the handler you passed into the version check. You can see an example in 109 | `MCM_TestHarness.uc` in `ModConfigMenu`. The specific delegate is declared as: 110 | 111 | ``` 112 | delegate ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode); 113 | ``` 114 | 115 | In `MCM_TestHarness.uc` you can see how this is used to selectively disable an option if you're in between missions, and 116 | completely skip revealing any options at all in-mission. 117 | 118 | ``` 119 | function ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode) 120 | { 121 | local MCM_API_SettingsGroup P1G1, P1G2, P1G3; 122 | local array Options; 123 | 124 | if (GameMode == eGameMode_MainMenu || GameMode == eGameMode_Strategy) 125 | { 126 | `log("Is in main menu or strategy menu, attempting to make page."); 127 | 128 | //Snip 129 | 130 | if (GameMode == eGameMode_Strategy) 131 | { 132 | P1Checkbox.SetEditable(false); 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | Here are the possible values for the GameMode: 139 | 140 | ``` 141 | enum eGameMode 142 | { 143 | eGameMode_MainMenu, 144 | eGameMode_Strategy, 145 | eGameMode_Tactical, 146 | eGameMode_Multiplayer, 147 | eGameMode_Unknown 148 | }; 149 | ``` 150 | 151 | ### Using MCM for Per-Campaign Settings 152 | 153 | Technically MCM is agnostic to where you are loading/saving the settings, so it's quite possible to use MCM for campaign settings. In general this means you need to do two things: 154 | 155 | 1. Figure out how you're loading/saving the settings into the game state, or wherever else you are storing them. 156 | 2. Use the game mode information to only enable settings when you're in Strategy or Tactical views, but not the main menu or multiplayer. 157 | 158 | You may have to manually implement the save handlers for individual settings, and you definitely will have to implement the handler for the "Save and Exit" button yourself in order to write settings into the game state. 159 | 160 | You will not be able to use any of the `MCM_CH_***` macros because they are meant for loading/saving to config INI's, but the other `MCM_API_***` macros may still be useful. You should still use the macro-based version check. 161 | 162 | ### Formatted text 163 | 164 | Formatted text (size, color, weight) should work for all group labels and settings labels. Just be careful about: 165 | 1. making it look ugly 166 | 2. cutting text off if it doens't fit into the widgets. 167 | 168 | ### Version Checking, under the hood. 169 | 170 | The version check is mostly made automatic using macros in `MCM_API_Includes.uci`. This means it grabs the version numbers 171 | that come with whichever copy of the API files you got. This should be sufficient for pretty much everyone. 172 | 173 | However, if you want to be extra strict about version numbers, you can do this manually. MCM uses a Major/Minor number system, 174 | where 1/0 is the first public release, and 1/10 would be major version 1, minor version 10. As long as the version of MCM 175 | installed is the same major version as your mod, and is at least as recent as the minor version of your mod, everything should work. 176 | 177 | However, it may be that the user hasn't updated MCM in a while and your mod uses a more recent minor version. In that case, if you 178 | know that your mod isn't doing anything special to the newer minor version, it might make sense to hard-code to an earlier minor 179 | version. 180 | 181 | To figure out the version number MCM is using, look at `XComModConfigMenu.ini`. 182 | 183 | Here's an example of how you would manually handle the version check instead of using the `MCM_API_Register` macro: 184 | 185 | ``` 186 | event OnInit(UIScreen Screen) 187 | { 188 | if (MCM_API(Screen) != none) 189 | { 190 | MCM_API(Screen).RegisterClientMod(1, 0, ClientModCallback); 191 | } 192 | } 193 | ``` 194 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_Slider.uc: -------------------------------------------------------------------------------- 1 | class MCM_Slider extends MCM_SettingBase implements(MCM_API_Slider) config(ModConfigMenu); 2 | 3 | var MCM_API_Setting ParentFacade; 4 | var delegate ChangeHandler; 5 | 6 | var float SliderMin; 7 | var float SliderMax; 8 | var float SliderStep; 9 | var float SliderValue; 10 | 11 | var UIScrollingText SliderValueDisplay; 12 | 13 | var bool SuppressEvent; 14 | 15 | var delegate DisplayFilter; 16 | 17 | delegate string SliderValueDisplayFilter(float _value); 18 | delegate FloatSettingHandler(MCM_API_Setting Setting, float _SettingValue); 19 | 20 | simulated function MCM_SettingBase InitSettingsItem(name _Name, eSettingType _Type, optional string _Label = "", optional string _Tooltip = "") 21 | { 22 | `log("Don't call InitSettingsItem directly in subclass of MCM_SettingBase."); 23 | 24 | return none; 25 | } 26 | 27 | // Fancy init process 28 | simulated function MCM_Slider InitSlider(name _SettingName, MCM_API_Setting _ParentFacade, string _Label, string _Tooltip, 29 | float sMin, float sMax, float sStep, float sValue, delegate _OnChange) 30 | { 31 | super.InitSettingsItem(_SettingName, eSettingType_Slider, _Label, _Tooltip); 32 | 33 | SliderValueDisplay = Spawn(class'UIScrollingText', self); 34 | SliderValueDisplay.bIsNavigable = false; 35 | SliderValueDisplay.bAnimateOnInit = bAnimateOnInit; 36 | SliderValueDisplay.InitScrollingText('SliderValueTextControl',,90,260); 37 | 38 | SuppressEvent = false; 39 | 40 | ChangeHandler = _OnChange; 41 | ParentFacade = _ParentFacade; 42 | 43 | SliderMin = sMin; 44 | SliderMax = sMax; 45 | SliderStep = sStep; 46 | 47 | if (sStep == 0) 48 | { 49 | // Special case default value, 10 steps in whole bar. 50 | SliderStep = (SliderMax - SliderMin) * 0.01f; 51 | } 52 | 53 | SliderValue = ClampAndSnapValue(sMin, sMax, sStep, sValue); 54 | 55 | // Need to calculate the above stats before calling to make sure UpdateDataSlider can compute step size. 56 | UpdateDataSlider(_Label, "", GetSliderPositionFromValue(SliderMin, SliderMax, SliderValue), , SliderChangedCallback); 57 | 58 | // Initially no filter. 59 | DisplayFilter = none; 60 | UpdateSliderValueDisplay(); 61 | 62 | SetHoverTooltip(_Tooltip); 63 | 64 | return self; 65 | } 66 | 67 | function SliderChangedCallback(UISlider SliderControl) 68 | { 69 | if (!SuppressEvent) 70 | { 71 | // Safe to put this inside the SuppressEvent guard because SuppressEvent is only set via methods that modify the SliderValue directly. 72 | SliderValue = ClampAndSnapValue(SliderMin, SliderMax, SliderStep, GetSliderValueFromPosition(SliderMin, SliderMax, Slider.percent)); 73 | UpdateSliderValueDisplay(); 74 | if(ChangeHandler != none) 75 | { 76 | ChangeHandler(ParentFacade, self.GetValue()); 77 | } 78 | } 79 | } 80 | 81 | simulated function AfterParentPageDisplayed() 82 | { 83 | super.AfterParentPageDisplayed(); 84 | } 85 | 86 | function UpdateSliderValueDisplay() 87 | { 88 | //SliderValueDisplay.SetHTMLText("

" $ string(GetValue()) $ "

"); 89 | if (DisplayFilter == none) 90 | { 91 | SliderValueDisplay.SetText(DefaultDisplayFilter(GetValue())); 92 | } 93 | else 94 | { 95 | SliderValueDisplay.SetText(DisplayFilter(GetValue())); 96 | } 97 | } 98 | 99 | function string DefaultDisplayFilter(float _value) 100 | { 101 | return DefaultFormatValueForDisplay(SliderMin, SliderMax, SliderStep, _value); 102 | } 103 | 104 | // =============================================== Patching some unhelpful stuff that UpdateDataSlider does. 105 | 106 | simulated function UpdateDataSlider(string _Desc, 107 | string _SliderLabel, 108 | optional int _SliderPosition, 109 | optional delegate _OnClickDelegate = none, 110 | optional delegate _OnSliderChangedDelegate = none) 111 | { 112 | SetWidgetType(EUILineItemType_Slider); 113 | 114 | if( Slider == none ) 115 | { 116 | Slider = Spawn(class'MCM_XCOM2_UISlider', self); 117 | Slider.bIsNavigable = false; 118 | Slider.bAnimateOnInit = false; 119 | Slider.InitSlider('SliderMC'); 120 | Slider.Navigator.HorizontalNavigation = true; 121 | //Slider.SetPosition(width - 420, 0); 122 | Slider.SetX(width - 420); 123 | } 124 | 125 | // Needed to make sure arrow buttons for increment/decrement work. 126 | Slider.SetStepSize(PercentPerStep(SliderMin, SliderMax, SliderStep)); 127 | 128 | Slider.SetPercent(_SliderPosition); 129 | Slider.SetText(_SliderLabel); 130 | Slider.Show(); 131 | 132 | // Since we have a narrower settings object, we're just going to hard-code this as 250 because it's wide enough. 133 | //Desc.SetWidth(width - 420); 134 | Desc.SetWidth(250); 135 | 136 | Desc.SetHTMLText(_Desc); 137 | Desc.Show(); 138 | 139 | OnClickDelegate = _OnClickDelegate; 140 | OnSliderChangedCallback = _OnSliderChangedDelegate; 141 | Slider.onChangedDelegate = _OnSliderChangedDelegate; 142 | } 143 | 144 | // MCM_API_Slider implementation ============================================================================= 145 | 146 | simulated function float GetValue() 147 | { 148 | return SliderValue; 149 | } 150 | 151 | simulated function SetValue(float _Value, bool _SuppressEvent) 152 | { 153 | SliderValue = ClampAndSnapValue(SliderMin, SliderMax, SliderStep, _Value); 154 | 155 | SuppressEvent = _SuppressEvent; 156 | Slider.SetPercent(GetSliderPositionFromValue(SliderMin, SliderMax, SliderValue)); 157 | UpdateSliderValueDisplay(); 158 | SuppressEvent = false; 159 | } 160 | 161 | function GetBounds(out float sMin, out float sMax, out float sStep, out float sValue) 162 | { 163 | sMin = SliderMin; 164 | sMax = SliderMax; 165 | sStep = SliderStep; 166 | sValue = SliderValue; 167 | } 168 | 169 | simulated function SetBounds(float min, float max, float step, float newValue, bool _SuppressEvent) 170 | { 171 | SliderMin = min; 172 | SliderMax = max; 173 | SliderStep = step; 174 | 175 | if (step == 0) 176 | { 177 | // Special case default value, 10 steps in whole bar. 178 | SliderStep = (SliderMax - SliderMin) * 0.01f; 179 | } 180 | 181 | SliderValue = ClampAndSnapValue(min, max, step, newValue); 182 | 183 | SuppressEvent = _SuppressEvent; 184 | // Update increment/decrement arrow button step sizes. 185 | Slider.SetStepSize(PercentPerStep(SliderMin, SliderMax, SliderStep)); 186 | Slider.SetPercent(GetSliderPositionFromValue(SliderMin, SliderMax, SliderValue)); 187 | UpdateSliderValueDisplay(); 188 | SuppressEvent = false; 189 | } 190 | 191 | simulated function SetValueDisplayFilter(delegate _DisplayFilter) 192 | { 193 | DisplayFilter = _DisplayFilter; 194 | UpdateSliderValueDisplay(); 195 | } 196 | 197 | // Have to override to disable the underlying control. 198 | simulated function SetEditable(bool IsEditable) 199 | { 200 | super.SetEditable(IsEditable); 201 | //SliderValueDisplay.SetDisabled(!IsEditable); 202 | 203 | // Hiding *everything* for consistency's sake. Until there's a better answer. 204 | if (IsEditable) 205 | { 206 | SliderValueDisplay.Show(); 207 | Slider.Show(); 208 | } 209 | else 210 | { 211 | SliderValueDisplay.Hide(); 212 | Slider.Hide(); 213 | } 214 | } 215 | 216 | // =============================================== Value/Position conversions 217 | 218 | // For an indicated numerical value, get the percent on the slider that represents 219 | // that value. 220 | static function float GetSliderPositionFromValue(float sMin, float sMax, float sValue) 221 | { 222 | // The weird 99 is because range is [1,100] and not [0,100]. 223 | //return 1.0 + 99.0 * (sValue - sMin)/(sMax - sMin); 224 | return 100.0 * (sValue - sMin) / (sMax - sMin); 225 | } 226 | 227 | // For an indicated percent position on the slider, get the value that is represented 228 | // by that position. 229 | static function float GetSliderValueFromPosition(float sMin, float sMax, float sPercent) 230 | { 231 | // The weird 99 is because range is [1,100] and not [0,100]. 232 | //return sMin + (sMax - sMin) * ((sPercent-1.0) / 99.0); 233 | return sMin + (sMax - sMin) * (sPercent / 100.0); 234 | } 235 | 236 | // Clamps the indicated value to between the min/max, and also snaps it to the nearest 237 | // stepping point between them. 238 | static function float ClampAndSnapValue(float sMin, float sMax, float sStep, float sValue) 239 | { 240 | if (sValue <= sMin) return sMin; 241 | if (sValue >= sMax) return sMax; 242 | if (sStep > 0) 243 | { 244 | return float(int((sValue - sMin) / sStep)) * sStep + sMin; 245 | } 246 | return sValue; 247 | } 248 | 249 | static function float PercentPerStep(float sMin, float sMax, float sStep) 250 | { 251 | if (sStep == 0) return 0; 252 | return sStep / (sMax - sMin) * 100.0; 253 | } 254 | 255 | // =============================================== Default value display code 256 | 257 | // The default function to format a value into a string for display. Is used by both the 258 | // Slider and SliderFacade, so put here for less duplication. 259 | static function string DefaultFormatValueForDisplay(float sMin, float sMax, float sStep, float sValue) 260 | { 261 | local string retstring; 262 | local int i; 263 | 264 | retstring = string(sValue); 265 | 266 | // if there is a decimal, strip out any ending zeroes (and the decimal if necessary) 267 | i = InStr(retstring, "."); 268 | if (i > 0) { 269 | while (Right(retstring, 1) == "0") retstring = Left(retstring, Len(retstring) - 1); 270 | if (Right(retstring, 1) == ".") retstring = Left(retstring, Len(retstring) - 1); 271 | } 272 | 273 | return retstring; 274 | } 275 | -------------------------------------------------------------------------------- /ModConfigMenu/ModConfigMenu/Src/ModConfigMenu/Classes/MCM_OptionsScreen.uc: -------------------------------------------------------------------------------- 1 | class MCM_OptionsScreen extends UIScreen implements(MCM_API, MCM_API_Instance) 2 | config(ModConfigMenu) dependson(UIDialogueBox); 3 | 4 | var config int PANEL_X; 5 | var config int PANEL_Y; 6 | var config int TABLIST_WIDTH; 7 | var config int TABS_LIST_TOP_PADDING; 8 | var config int OPTIONS_HEIGHT; 9 | var config int OPTIONS_WIDTH; 10 | var config int OPTIONS_MARGIN; 11 | var config int HEADER_HEIGHT; 12 | var config int FOOTER_HEIGHT; 13 | 14 | // If false, then hide soldier during Options menu in order to improve visibility and avoid blocking 15 | // Save and Exit button. Allows for bigger menu. 16 | var config bool SHOW_SOLDIER; 17 | 18 | // Needs major version match and requested minor version needs to be <= actual minor version. 19 | var config int API_MAJOR_VERSION; 20 | var config int API_MINOR_VERSION; 21 | 22 | var localized string m_strTitle; 23 | var localized string m_strSubtitle; 24 | var localized string m_strSaveAndExit; 25 | var localized string m_strCancel; 26 | 27 | var MCM_OptionsMenuListener ParentListener; 28 | 29 | var UIPanel Container; 30 | var UIImage BG; 31 | var UIImage VSeparator; 32 | var UIX2PanelHeader TitleHeader; 33 | 34 | var UIList TabsList; 35 | var int SettingsPageCounter; 36 | var int SelectedPageID; 37 | var array SettingsTabs; 38 | var array SettingsPanels; 39 | var UIButton SaveAndExitButton; 40 | var UIButton CancelButton; 41 | 42 | var int CurrentGameMode; 43 | 44 | // Pawn hiding code thanks to Patrick-Seymour 45 | var bool SoldierVisible; 46 | struct PawnAndComponents { 47 | var XComUnitPawn Pawn; 48 | var array Comps; 49 | }; 50 | var array PawnAndComps; 51 | 52 | delegate ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode); 53 | delegate OnClickedDelegate(UIButton Button); 54 | delegate SettingsTabDelegate(MCM_SettingsTab Caller, int PageID); 55 | delegate CustomSettingsPageCallback(UIScreen ParentScreen, int PageID); 56 | 57 | simulated function InitScreen(XComPlayerController InitController, UIMovie InitMovie, optional name InitName) 58 | { 59 | `log("MCM InitScreen called."); 60 | 61 | super.InitScreen(InitController, InitMovie, InitName); 62 | 63 | UpdateGameMode(); 64 | CreateSkeleton(); 65 | 66 | `log("MCM InitScreen complete."); 67 | } 68 | 69 | simulated function UpdateGameMode() 70 | { 71 | local EUIMode uimode; 72 | 73 | if (`XENGINE.IsMultiplayerGame()) 74 | { 75 | CurrentGameMode = eGameMode_Multiplayer; 76 | } 77 | else 78 | { 79 | uimode = Movie.Pres.m_eUIMode; 80 | 81 | if (uimode == eUIMode_Tactical) 82 | CurrentGameMode = eGameMode_Tactical; 83 | else if (uimode == eUIMode_Strategy) 84 | CurrentGameMode = eGameMode_Strategy; 85 | else if (uimode == eUIMode_Shell) 86 | CurrentGameMode = eGameMode_MainMenu; 87 | else 88 | CurrentGameMode = eGameMode_Unknown; 89 | } 90 | } 91 | 92 | simulated function OnInit() 93 | { 94 | super.OnInit(); 95 | 96 | `log("MCM Core: On Init Called."); 97 | 98 | if (CurrentGameMode == eGameMode_MainMenu && SHOW_SOLDIER == false) 99 | { 100 | `log("MCM Core: hiding soldier guy on main menu for visibility."); 101 | HideSoldierIfMainMenu(); 102 | } 103 | } 104 | 105 | simulated function OnRemoved() 106 | { 107 | if (CurrentGameMode == eGameMode_MainMenu && SHOW_SOLDIER == false) 108 | { 109 | `log("MCM Core: unhiding soldier guy on main menu for visibility."); 110 | ShowSoldierIfMainMenu(); 111 | } 112 | } 113 | 114 | simulated function InitModOptionsMenu(MCM_OptionsMenuListener listener) 115 | { 116 | ParentListener = listener; 117 | `log("MCM InitModOptionsMenu called."); 118 | } 119 | 120 | simulated function CreateSkeleton() 121 | { 122 | local int TotalWidth; 123 | local int TotalHeight; 124 | 125 | TotalWidth = TABLIST_WIDTH + OPTIONS_WIDTH; 126 | TotalHeight = HEADER_HEIGHT + OPTIONS_HEIGHT + FOOTER_HEIGHT; 127 | 128 | Container = Spawn(class'UIPanel', self).InitPanel('').SetPosition(PANEL_X, PANEL_Y).SetSize(TotalWidth, TotalHeight); 129 | 130 | BG = Spawn(class'UIImage', Container).InitImage(,"img:///MCM.gfx.MainBackground"); 131 | BG.SetPosition(0,0).SetSize(TotalWidth, TotalHeight); 132 | 133 | VSeparator = Spawn(class'UIImage', Container).InitImage(,"img:///MCM.gfx.MainVerticalSeparator"); 134 | VSeparator.SetPosition(TABLIST_WIDTH,HEADER_HEIGHT); 135 | 136 | // Save and exit button 137 | SaveAndExitButton = Spawn(class'UIButton', Container); 138 | SaveAndExitButton.InitButton(, m_strSaveAndExit, OnSaveAndExit); 139 | SaveAndExitButton.SetPosition(Container.width - 190, Container.height - 40); //Relative to this screen panel 140 | SaveAndExitButton.AnimateIn(0); 141 | 142 | CancelButton = Spawn(class'UIButton', Container); 143 | CancelButton.InitButton(, m_strCancel, OnCancel); 144 | CancelButton.SetPosition(Container.width - 190 - 170, Container.height - 40); //Relative to this screen panel 145 | CancelButton.AnimateIn(0); 146 | 147 | TitleHeader = Spawn(class'UIX2PanelHeader', Container); 148 | TitleHeader.InitPanelHeader('', m_strTitle, m_strSubtitle); 149 | TitleHeader.SetHeaderWidth(Container.width - 20); 150 | TitleHeader.SetPosition(10, 10); 151 | 152 | TabsList = Spawn(class'UIList', Container).InitList('ModTabSelectList', 10, HEADER_HEIGHT + TABS_LIST_TOP_PADDING, TABLIST_WIDTH - 30, OPTIONS_HEIGHT); 153 | TabsList.SetSelectedNavigation(); 154 | TabsList.Navigator.LoopSelection = true; 155 | 156 | //Container.Navigator.AddControl(SaveAndExitButton); 157 | //Container.Navigator.AddControl(CancelButton); 158 | 159 | // Start with nothing selected. 160 | Container.Navigator.SetSelected(none); 161 | //TabsList.Navigator.SelectFirstAvailable(); 162 | } 163 | 164 | // Special button handlers ======================================================================== 165 | 166 | simulated function OnSaveAndExit(UIButton kButton) 167 | { 168 | local MCM_SettingsPanelFacade TmpPage; 169 | 170 | // Save all. 171 | foreach SettingsPanels(TmpPage) 172 | { 173 | TmpPage.TriggerSaveEvent(); 174 | } 175 | 176 | Movie.Stack.Pop(self); 177 | } 178 | 179 | simulated function OnCancel(UIButton kButton) 180 | { 181 | local MCM_SettingsPanelFacade TmpPage; 182 | 183 | // Cancel all. 184 | foreach SettingsPanels(TmpPage) 185 | { 186 | TmpPage.TriggerCancelEvent(); 187 | } 188 | 189 | Movie.Stack.Pop(self); 190 | } 191 | 192 | // Keyboard input ============================================================================ 193 | 194 | simulated function bool OnUnrealCommand(int cmd, int arg) 195 | { 196 | if( !CheckInputIsReleaseOrDirectionRepeat(cmd, arg) ) 197 | return false; 198 | 199 | switch( cmd ) 200 | { 201 | case class'UIUtilities_Input'.const.FXS_BUTTON_B: 202 | case class'UIUtilities_Input'.const.FXS_KEY_ESCAPE: 203 | OnCancel(none); 204 | break; 205 | } 206 | 207 | return super.OnUnrealCommand(cmd, arg); 208 | } 209 | 210 | // Show/hide the soldier pawn =================================================================== 211 | // Implementation thanks to Patrick-Seymour, who provided this code. 212 | 213 | simulated function HideSoldier() 214 | { 215 | local XComUnitPawn Pawn; 216 | local PrimitiveComponent Comp; 217 | local PawnAndComponents PawnAndComp; 218 | PawnAndComps.Length = 0; 219 | foreach `XWORLDINFO.AllActors(class'XComUnitPawn', Pawn) 220 | { 221 | PawnAndComp.Pawn = Pawn; 222 | PawnAndComp.Comps.Length = 0; 223 | foreach Pawn.AllOwnedComponents(class'PrimitiveComponent', Comp) 224 | { 225 | if (!Comp.HiddenGame) 226 | { 227 | Comp.SetHidden(true); 228 | PawnAndComp.Comps.AddItem(Comp); 229 | } 230 | } 231 | PawnAndComps.AddItem(PawnAndComp); 232 | } 233 | 234 | SoldierVisible = false; 235 | } 236 | 237 | simulated function ShowSoldier() 238 | { 239 | local XComUnitPawn Pawn; 240 | local PrimitiveComponent Comp; 241 | local int i, j; 242 | foreach `XWORLDINFO.AllActors(class'XComUnitPawn', Pawn) 243 | { 244 | for (i = 0; i < PawnAndComps.Length; ++i) 245 | { 246 | if (PawnAndComps[i].Pawn == Pawn) 247 | { 248 | foreach Pawn.AllOwnedComponents(class'PrimitiveComponent', Comp) 249 | { 250 | for (j = 0; j < PawnAndComps[i].Comps.Length; ++j) 251 | { 252 | if (PawnAndComps[i].Comps[j] == Comp) 253 | { 254 | Comp.SetHidden(false); 255 | break; 256 | } 257 | } 258 | } 259 | break; 260 | } 261 | } 262 | } 263 | PawnAndComps.Length = 0; 264 | 265 | SoldierVisible = true; 266 | } 267 | 268 | simulated function HideSoldierIfMainMenu() 269 | { 270 | if (CurrentGameMode == eGameMode_MainMenu && SoldierVisible) 271 | { 272 | HideSoldier(); 273 | } 274 | } 275 | 276 | simulated function ShowSoldierIfMainMenu() 277 | { 278 | if (CurrentGameMode == eGameMode_MainMenu && !SoldierVisible) 279 | { 280 | ShowSoldier(); 281 | } 282 | } 283 | 284 | // Helpers for MCM_API_Instance =================================================================== 285 | 286 | simulated function MCM_SettingsPanelFacade GetPanelByPageID(int PageID) 287 | { 288 | local MCM_SettingsPanelFacade TmpPage; 289 | 290 | foreach SettingsPanels(TmpPage) 291 | { 292 | if (TmpPage.GetPageId() == PageID) 293 | return TmpPage; 294 | } 295 | 296 | return None; 297 | } 298 | 299 | simulated function ChoosePanelByPageID(int PageID) 300 | { 301 | //local MCM_SettingsPanel CurrentSettingsPage; 302 | local MCM_SettingsTab TmpButton; 303 | local MCM_SettingsPanelFacade TmpPage; 304 | 305 | // Are we changing pages? Do nothing if not changing pages. 306 | if (PageID != SelectedPageID) 307 | { 308 | SelectedPageID = PageID; 309 | 310 | // Now choose the panel. 311 | foreach SettingsPanels(TmpPage) 312 | { 313 | if (TmpPage.GetPageId() != SelectedPageID) 314 | { 315 | TmpPage.Hide(); 316 | } 317 | else 318 | { 319 | `log("MCM: Found correct panel, showing."); 320 | TmpPage.Show(); 321 | } 322 | } 323 | 324 | // Refresh the button. This is important if we're cancelling a tab change. 325 | foreach SettingsTabs(TmpButton) 326 | { 327 | if (TmpButton.SettingsPageID == SelectedPageID) 328 | { 329 | TmpButton.SetChecked(true); 330 | } 331 | else 332 | { 333 | TmpButton.SetChecked(false); 334 | } 335 | } 336 | } 337 | } 338 | 339 | simulated function TabClickedHandler(MCM_SettingsTab Caller, int PageID) 340 | { 341 | `log("MCM Tab clicked: " $ string(PageID)); 342 | //TabsList.SetSelectedItem(kButton, true); 343 | ChoosePanelByPageID(PageID); 344 | } 345 | 346 | simulated function AddTabsListButton(string TabLabel, int PageID) 347 | { 348 | local MCM_SettingsTab Item; 349 | Item = Spawn(class'MCM_SettingsTab', TabsList.ItemContainer).InitSettingsTab(PageID, TabLabel); 350 | Item.OnClickHandler = TabClickedHandler; 351 | 352 | SettingsTabs.AddItem(Item); 353 | } 354 | 355 | function MCM_API_SettingsPage MakeSettingsPage(string TabLabel, int PageID) 356 | { 357 | local MCM_SettingsPanelFacade SP; 358 | SP = Spawn(class'MCM_SettingsPanelFacade', self); 359 | SP.InitSettingsPanelFacade(PageID, TabLabel, TABLIST_WIDTH + OPTIONS_MARGIN, HEADER_HEIGHT, Container); 360 | 361 | // Register panel. 362 | SettingsPanels.AddItem(SP); 363 | 364 | return SP; 365 | } 366 | 367 | simulated function CustomTabClickedHandler(MCM_SettingsTab Caller, int PageID) 368 | { 369 | `log("MCM Custom Screen Tab clicked"); 370 | if (Caller.CustomPageCallback != none) 371 | { 372 | Caller.SetChecked(false); 373 | Caller.CustomPageCallback(self, PageID); 374 | } 375 | } 376 | 377 | // MCM_API_Instance implementation =============================================================== 378 | 379 | function MCM_API_SettingsPage NewSettingsPage(string TabLabel) 380 | { 381 | local int PageID; 382 | 383 | PageID = SettingsPageCounter; 384 | SettingsPageCounter++; 385 | 386 | AddTabsListButton(TabLabel, PageID); 387 | 388 | return MakeSettingsPage(TabLabel, PageID); 389 | } 390 | 391 | function int NewCustomSettingsPage(string TabLabel, delegate Handler) 392 | { 393 | local MCM_SettingsTab Item; 394 | local int PageID; 395 | 396 | PageID = SettingsPageCounter; 397 | SettingsPageCounter++; 398 | 399 | Item = Spawn(class'MCM_SettingsTab', TabsList.ItemContainer).InitSettingsTab(PageID, TabLabel); 400 | Item.CustomPageCallback = Handler; 401 | Item.OnClickHandler = CustomTabClickedHandler; 402 | 403 | return PageID; 404 | } 405 | 406 | function MCM_API_SettingsPage GetSettingsPageByID(int PageID) 407 | { 408 | local MCM_SettingsPanelFacade TmpPage; 409 | 410 | foreach SettingsPanels(TmpPage) 411 | { 412 | if (TmpPage.GetPageId() == PageID) 413 | { 414 | return TmpPage; 415 | } 416 | } 417 | 418 | return None; 419 | } 420 | 421 | 422 | // MCM_API implementation ======================================================================== 423 | 424 | function bool RegisterClientMod(int major, int minor, delegate SetupHandler) 425 | { 426 | if (major == API_MAJOR_VERSION && minor <= API_MINOR_VERSION) 427 | { 428 | SetupHandler(self, CurrentGameMode); 429 | return true; 430 | } 431 | else 432 | { 433 | return false; 434 | } 435 | } 436 | 437 | // Defaults ====================================================================================== 438 | 439 | defaultproperties 440 | { 441 | ParentListener = None; 442 | SettingsPageCounter = 0; 443 | SelectedPageID = -1; 444 | 445 | SoldierVisible = true; 446 | 447 | InputState= eInputState_Evaluate; 448 | 449 | bAlwaysTick = true 450 | bConsumeMouseEvents=true 451 | } -------------------------------------------------------------------------------- /documentation/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | ### Setting up your project 4 | 5 | Before we get into writing the code, we need to set up the API files you will compile your mod against. 6 | [See here for more information on how this works.](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/sharedcode.md) 7 | You don't really need to know the nitty gritty details, just follow these steps. 8 | 9 | First, create a new mod project. 10 | 11 | ![](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/documentation/img/newproject.png) 12 | 13 | Next, you will need to get a copy of the API files. You can either download this repository and 14 | copy the `ModConfigMenu/ModConfigMenu/Src/ModConfigMenuAPI` folder, 15 | or download and extract the [compressed package](https://github.com/andrewgu/ModConfigMenu/blob/master/MCM_API.zip?raw=true) 16 | into the `src` folder in your mod: 17 | 18 | ![](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/documentation/img/pastefiles.png) 19 | 20 | Paste the ModConfigMenuAPI source package. You'll know you did it right if the `src/ModConfigMenuAPI` folder looks like this: 21 | 22 | ![](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/documentation/img/mcmapifiles.png) 23 | 24 | In your project, you will want to add the files so that ModBuddy knows to compile them. 25 | The easiest way to do this is to directly edit the `.x2proj` file. First, close ModBuddy, then open the file in a text editor. 26 | In this example the path of the `.x2proj` file is `Documents\Firaxis ModBuddy\XCOM\MCM_Tutorial\MCM_Tutorial\MCM_Tutorial.x2proj`. 27 | 28 | In the section that has lines that look like ``, insert these two lines: 29 | 30 | ``` 31 | 32 | 33 | ``` 34 | 35 | See this screenshot for reference: 36 | 37 | ![](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/documentation/img/addfiles1.png) 38 | 39 | In the section that has lines that look like ``, insert this block of lines: 40 | 41 | ``` 42 | 43 | Content 44 | 45 | 46 | Content 47 | 48 | 49 | Content 50 | 51 | 52 | Content 53 | 54 | 55 | Content 56 | 57 | 58 | Content 59 | 60 | 61 | Content 62 | 63 | 64 | Content 65 | 66 | 67 | Content 68 | 69 | 70 | Content 71 | 72 | 73 | Content 74 | 75 | 76 | Content 77 | 78 | 79 | Content 80 | 81 | ``` 82 | 83 | See this screenshot for reference: 84 | 85 | ![](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/documentation/img/addfiles2.png) 86 | 87 | Once you've done this, save your edits and reopen your mod in ModBuddy. If you did this right, it should look like this: 88 | 89 | ![](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/documentation/img/addfiles3.png) 90 | 91 | ### Configuring your INI files 92 | 93 | Now that you've added the API files, you need to configure your mod to be able to compile against the API interfaces. 94 | You will need to add two lines to XComEngine.ini: 95 | 96 | ``` 97 | [UnrealEd.EditorEngine] 98 | +EditPackages=ModConfigMenuAPI 99 | ``` 100 | 101 | It should look like this: 102 | 103 | ![](https://raw.githubusercontent.com/andrewgu/ModConfigMenu/master/documentation/img/engineini.png) 104 | 105 | At this point, you should be able to build your mod with no errors. 106 | 107 | ### Hooking Into MCM 108 | 109 | Now that you've set up the API files, you're ready to start writing the actual code. In this example, we're going to add a page with one setting in it. 110 | 111 | We're going to hook into MCM using a screen listener. In this example I created a file named `ExampleListener.uc`: 112 | 113 | ``` 114 | class ExampleListener extends UIScreenListener config(NonexistentConfigName); 115 | 116 | `include(MCM_Tutorial/Src/ModConfigMenuAPI/MCM_API_Includes.uci) 117 | `include(MCM_Tutorial/Src/ModConfigMenuAPI/MCM_API_CfgHelpers.uci) 118 | ``` 119 | 120 | We're also going to include two macro files because they contain useful helper code. Notice the name of the project in the file paths. 121 | 122 | The part where "NonexistentConfigName" does not point to any INI file in your project is important in order to make it possible to save settings dynamically. 123 | [See this for a more detailed discussion.](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/config.md) 124 | 125 | Now we need to make a configuration variable that stores the setting we're going to make. 126 | We will also need a "version" variable which will be necessary later on in order for your mod to provide default values. 127 | 128 | ``` 129 | var config bool CHECKBOX_VALUE; 130 | var config int CONFIG_VERSION; 131 | ``` 132 | 133 | Now that we have variables out of the way, we're going to make `ExampleListener` listen for the UI screen that MCM creates. The problem is, since the class 134 | you actually want to listen for (`MCM_OptionsScreen`) doesn't exist when you're compiling your mod, you can't listen to it directly. Instead, you will have to 135 | check for it in the `OnInit` event. 136 | 137 | ``` 138 | defaultproperties 139 | { 140 | ScreenClass = none; 141 | } 142 | ``` 143 | 144 | Now we're going to use the `OnInit` event to check for the right screen type and to hook into MCM: 145 | 146 | ``` 147 | event OnInit(UIScreen Screen) 148 | { 149 | // Everything out here runs on every UIScreen. Not great but necessary. 150 | if (MCM_API(Screen) != none) 151 | { 152 | // Everything in here runs only when you need to touch MCM. 153 | `MCM_API_Register(Screen, ClientModCallback); 154 | } 155 | } 156 | ``` 157 | 158 | This declares that a function named `ClientModCallback` will be doing the work of building the page and settings. Here's the function in code, which we will implement in the next step: 159 | 160 | ``` 161 | simulated function ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode) 162 | { 163 | // Code goes here. 164 | } 165 | ``` 166 | 167 | ### Creating a settings page 168 | 169 | So we have our scaffolding in place. Now we're going to build the page. It's actually really straightforward, so here it is in a few lines: 170 | 171 | ``` 172 | simulated function ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode) 173 | { 174 | local MCM_API_SettingsPage Page; 175 | local MCM_API_SettingsGroup Group; 176 | 177 | LoadSavedSettings(); 178 | 179 | Page = ConfigAPI.NewSettingsPage("Example Tab Label"); 180 | Page.SetPageTitle("Example Title"); 181 | Page.SetSaveHandler(SaveButtonClicked); 182 | 183 | Group = Page.AddGroup('Group1', "General Settings"); 184 | 185 | Group.AddCheckbox('checkbox', "Example Checkbox", "Example Checkbox Tooltip", CHECKBOX_VALUE, CheckboxSaveHandler); 186 | 187 | Page.ShowSettings(); 188 | } 189 | ``` 190 | 191 | A few key points: 192 | 193 | 1. `LoadSavedSettings` is a function to load the user's current settings. We'll fill it out in the next section. 194 | 2. Notice that we're passing in CHECKBOX_VALUE. This makes sure that when we load the settings page, the checkbox is showing the player's current settings. 195 | 3. `CheckboxSaveHandler` and `SaveButtonClicked` are called in that order when the user clicks the "Save and Exit" button. We'll implement them in the next section to save the changed settings. 196 | 4. You need to call `ShowSettings()` on the page after adding your groups and settings in order to make them show up. After you call `ShowSettings()`, you should not attempt to add any more groups or settings. 197 | 198 | ### Loading and Saving settings 199 | 200 | Alright, so we've made the UI, now we just need to hook up the loading/saving functionality. 201 | 202 | But first, we need to have somewhere to load default settings from. If you want more details on how exactly this works, [see here](https://github.com/andrewgu/ModConfigMenu/blob/master/documentation/config.md). 203 | 204 | We're going to make two files: `MCM_Tutorial_Defaults.uc` and `XComMCM_Tutorial_Defaults.ini`. They look like this: 205 | 206 | ``` 207 | // MCM_Tutorial_Defaults.uc 208 | class MCM_Tutorial_Defaults extends Object config(MCM_Tutorial_Defaults); 209 | 210 | var config bool SETTING; 211 | var config int VERSION; 212 | ``` 213 | 214 | ``` 215 | ; XComMCM_Tutorial_Defaults.ini 216 | [MCM_Tutorial.MCM_Tutorial_Defaults] 217 | SETTING=true 218 | VERSION=1 219 | ``` 220 | 221 | We will use the `VERSION` variable to tell us whether to use the default setting or to use the saved setting. 222 | 223 | Now that we have the default settings set up, we're going to load them. We're using the `MCM_CH_VersionChecker` macro to declare that this class is pulling defaults from `MCM_Tutorial_Defaults`. Then we're using `MCM_CH_GetValue` to pull the right value from either the defaults or from a saved configuration. 224 | 225 | ``` 226 | `MCM_CH_VersionChecker(class'MCM_Tutorial_Defaults'.default.VERSION,CONFIG_VERSION) 227 | 228 | simulated function LoadSavedSettings() 229 | { 230 | CHECKBOX_VALUE = `MCM_CH_GetValue(class'MCM_Tutorial_Defaults'.default.SETTING,CHECKBOX_VALUE); 231 | } 232 | ``` 233 | 234 | And finally, we need to handle saving. Fortunately there's an easy macro we can use for `CheckboxSaveHandler`: 235 | 236 | ``` 237 | `MCM_API_BasicCheckboxSaveHandler(CheckboxSaveHandler, CHECKBOX_VALUE) 238 | ``` 239 | 240 | This macro basically says to write the state of the checkbox into CHECKBOX_VALUE when the user clicks "Save and Exit". 241 | 242 | And then in `SaveButtonClicked` we're going to actually save: 243 | 244 | ``` 245 | simulated function SaveButtonClicked(MCM_API_SettingsPage Page) 246 | { 247 | self.CONFIG_VERSION = `MCM_CH_GetCompositeVersion(); 248 | self.SaveConfig(); 249 | } 250 | ``` 251 | 252 | The first line updates the version number to tell the code to load from the saved settings in the future instead of the defaults. Then calling `self.SaveConfig()` finally saves the new settings so that they'll be there next time you start the game. 253 | 254 | Put everything together, and `ExampleListener.uc` looks like this: 255 | 256 | ``` 257 | class ExampleListener extends UIScreenListener config(NonexistentConfigName); 258 | 259 | `include(MCM_Tutorial/Src/ModConfigMenuAPI/MCM_API_Includes.uci) 260 | `include(MCM_Tutorial/Src/ModConfigMenuAPI/MCM_API_CfgHelpers.uci) 261 | 262 | var config bool CHECKBOX_VALUE; 263 | var config int CONFIG_VERSION; 264 | 265 | event OnInit(UIScreen Screen) 266 | { 267 | if (MCM_API(Screen) != none) 268 | { 269 | `MCM_API_Register(Screen, ClientModCallback); 270 | } 271 | } 272 | 273 | simulated function ClientModCallback(MCM_API_Instance ConfigAPI, int GameMode) 274 | { 275 | local MCM_API_SettingsPage Page; 276 | local MCM_API_SettingsGroup Group; 277 | 278 | LoadSavedSettings(); 279 | 280 | Page = ConfigAPI.NewSettingsPage("Example Tab Label"); 281 | Page.SetPageTitle("Example Title"); 282 | Page.SetSaveHandler(SaveButtonClicked); 283 | 284 | Group = Page.AddGroup('Group1', "General Settings"); 285 | 286 | Group.AddCheckbox('checkbox', "Example Checkbox", "Example Checkbox Tooltip", CHECKBOX_VALUE, CheckboxSaveHandler); 287 | 288 | Page.ShowSettings(); 289 | } 290 | 291 | `MCM_CH_VersionChecker(class'MCM_Tutorial_Defaults'.default.VERSION,CONFIG_VERSION) 292 | 293 | simulated function LoadSavedSettings() 294 | { 295 | CHECKBOX_VALUE = `MCM_CH_GetValue(class'MCM_Tutorial_Defaults'.default.SETTING,CHECKBOX_VALUE); 296 | } 297 | 298 | `MCM_API_BasicCheckboxSaveHandler(CheckboxSaveHandler, CHECKBOX_VALUE) 299 | 300 | simulated function SaveButtonClicked(MCM_API_SettingsPage Page) 301 | { 302 | self.CONFIG_VERSION = `MCM_CH_GetCompositeVersion(); 303 | self.SaveConfig(); 304 | } 305 | 306 | defaultproperties 307 | { 308 | ScreenClass = none; 309 | } 310 | ``` 311 | 312 | Short, and no UI code! 313 | 314 | ### Avoiding the Missing INI Pitfall 315 | 316 | If MCM was not installed, or if the player never actually went into MCM and saved any settings, you will be missing an INI file. If you try to load settings from a missing INI file, shit will break and everything will go to hell. 317 | 318 | Fortunately, there are a few ways to avoid that entirely: 319 | 320 | 1. My preferred method: load settings both from the default settings INI included in your mod and from the INI file where you are keeping the player's settings, and do a version check each time. It means every time you might use a config var, replace it with a `MCM_CH_GetValue` macro, like this: 321 | 322 | ``` 323 | class SomeClassInMyMod extends Object; 324 | 325 | // In the header somewhere 326 | `include(MCM_Tutorial/Src/ModConfigMenuAPI/MCM_API_CfgHelpers.uci) 327 | 328 | // Somewhere in the function definitions 329 | `MCM_CH_VersionChecker(class'MCM_Tutorial_Defaults'.default.VERSION,class'ExampleListener'.default.CONFIG_VERSION) 330 | 331 | int function ExampleFunctionInMyMod() 332 | { 333 | // Doing it this way means that it'll pull from defaults if the INI file wasn't created. 334 | return `MCM_CH_GetValue(class'MCM_Tutorial_Defaults'.default.SETTING,class'ExampleListener'.default.CHECKBOX_VALUE); 335 | } 336 | ``` 337 | 338 | 2. Do a lazy initialization check before the first time your settings variables are read on each startup. So if you need to read settings during template loading, call the lazy initializer before you start making templates. If you need it for UI things, call the lazy initializer first in the `OnInit` event. And so on. Here's an example lazy initializer you might use to do the version check and create the INI file if needed. In this example you would make sure to call `class'LazyIniHandler'.static.LazyInitializer()` before any point where your mod would need its config values. 339 | 340 | ``` 341 | class LazyIniHAndler extends Object; 342 | 343 | // In the header somewhere 344 | `include(MCM_Tutorial/Src/ModConfigMenuAPI/MCM_API_CfgHelpers.uci) 345 | 346 | var bool IniFileCreated; 347 | 348 | // Somewhere in the function definitions 349 | `MCM_CH_VersionChecker(class'MCM_Tutorial_Defaults'.default.VERSION,class'ExampleListener'.default.CONFIG_VERSION) 350 | 351 | static function LazyInitializer() 352 | { 353 | // Early out. 354 | if (default.IniFileCreated) 355 | return; 356 | 357 | // Only works if you correctly labeled the default's version to be >= 1. 358 | if (class'MCM_Tutorial_Defaults'.default.VERSION > class'ExampleListener'.default.CONFIG_VERSION) 359 | { 360 | static.UpdateSavedIniFile(); 361 | } 362 | 363 | default.IniFileCreated = true; 364 | } 365 | 366 | static function UpdateSavedIniFile() 367 | { 368 | // This updates values or grabs defaults if the INI is missing. 369 | class'ExampleListener'.default.CHECKBOX_VALUE = 370 | `MCM_CH_GetValue(class'MCM_Tutorial_Defaults'.default.SETTING,class'ExampleListener'.default.CHECKBOX_VALUE); 371 | 372 | // This saves updates or creates the INI if it's missing. 373 | class'ExampleListener'.static.StaticSaveConfig(); 374 | } 375 | 376 | defaultproperties 377 | { 378 | IniFileCreated = false; 379 | } 380 | 381 | ``` 382 | 383 | In both cases, you can use the `MCM_CH_***` API calls to make your job a bit easier. 384 | 385 | ### Avoid storing Actors, UI objects, and MCM_API objects as instance variables 386 | 387 | This is a very easy pitfall to fall into. Notice how the tutorial code carefully avoids storing anything other than primitive data types at the class scope. Everything that isn't a primitive is stored at local scope. 388 | 389 | We do it this way because screen listeners do not get garbage collected when Actors, UI objects, and MCM_API objects get garbage collected. This creates a problem because any lingering references will prevent cleanup on objects that needed it. That in turn causes hard to debug problems like graphical glitches and infinite loops when loading saves. So don't do it. 390 | 391 | If you absolutely must do it, then I recommend splitting the UIScreenListener into two parts. Like this: 392 | 393 | ``` 394 | class ExampleListenerHook extends UIScreenListener; 395 | 396 | event OnInit(UIScreen Screen) 397 | { 398 | local ExampleListener listener; 399 | if (MCM_API(Screen) != none) 400 | { 401 | listener = new class'ExampleListener'; 402 | listener.OnInit(Screen); 403 | } 404 | } 405 | 406 | defaultproperties 407 | { 408 | ScreenClass = none; 409 | } 410 | ``` 411 | 412 | ``` 413 | class ExampleListener extends Object config(NonexistentConfigName); 414 | 415 | function OnInit(UIScreen Screen) 416 | { 417 | `MCM_API_Register(Screen, ClientModCallback); 418 | } 419 | 420 | // And so on with the rest of the original ExampleListener. 421 | 422 | defaultproperties 423 | { 424 | // Don't need ScreenClass since it's not a UIScreenListener anymore. 425 | } 426 | ``` 427 | 428 | By putting the actual meat of the code in a transient reference instead of a UIScreenListener, this allows the garbage collector to destroy everything cleanly even though the UIScreenListener is never garbage collected. 429 | 430 | ### Testing 431 | 432 | To test, you need to have the ModConfigMenu mod installed. You can do it two ways: either install it from Steam Workshop, or compile this mod from source. You can find the source code in this repository. 433 | 434 | Before you release your mod, you should also test that your mod works without MCM installed, and works when MCM hasn't been run. This means your mod still needs to work when the INI file where you store settings does not exist. 435 | --------------------------------------------------------------------------------