Jump to content

RFC - Mod Options Menu


Lucubration

Recommended Posts

Foreword

As mods are getting more complex - my own included - I'm seeing a need to move some mod options from .ini files into the in-game UI to be more accessible to users. I was going to pursue a custom solution for my particular case, but I can imagine that might lead to a cluttered UI if a number of mods all began doing the same.

 

With this in mind, I put together a little system here for inserting a "Mod Options" menu item into the normal, in-game Options menu. The technique uses screen listeners, the event manager and some dynamic type resolution to allow any interested mods to utilize this same pathway for displaying their own mod options screen for the user.

 

I've gone ahead and published this Mod Options Menu despite nothing using it just yet. I'd to submit the design here as a request for comments. If this is a pattern that's of interest to you, I'd very much appreciate it if you would take a minute to help me go over the API design to make sure that it will be robust enough and suitable for all of our use. I'm mostly interested in making sure that the contract between the API and clients is well-designed; I expect that the actual UI design will change over time relatively transparent to any client mods.

 

Nexus link: http://www.nexusmods.com/xcom2/mods/522/?

Workshop link: http://steamcommunity.com/sharedfiles/filedetails/?id=652998069

 

Design

I begin this pattern by using a UIScreenListener to see when the Xcom 2 main Options menu pops up. I'm inserting an extra button right down by the Credits button (at least in the Shell menu) that opens up the mod list. This is where things get interesting.

 

Depending on which XComPlayerController spawned the Options screen, I'm creating one of three subclasses of the mod list screen (for the Shell, HQ, and Tactical game modes). In the superclass, there is much ugly UI code going on to set up the list and so on, and right at the start of it all I'm registering for a new event 'ModOptionsMenu_AddItem',

 

 

 

simulated function InitScreen(XComPlayerController InitController, UIMovie InitMovie, optional name InitName)
{
	local X2EventManager  EventManager;
	local Object ThisObj;

	EventManager = `XEVENTMGR;
	ThisObj = self;
	EventManager.RegisterForEvent(ThisObj, 'ModOptionsMenu_AddItem', OnAddItem, ELD_Immediate);

	super.InitScreen(InitController, InitMovie, InitName);
...
}

 

 

 

The next piece of the puzzle here is the contract between the client mods and the API, and it involves another UIScreenListener implemented by the client. This one listens for the Mod Options Menu screen(s) and triggers the 'ModOptionsMenu_AddItem' event to advertise itself to the API. In this example I have it listening for all three different subclasses.

 

The event data included in the 'ModOptionsMenu_AddItem' event contains the display name that should appear in the Mod Options Menu list of mods, and the name of a UIScreen subclass that should be instantiated on behalf of the client mod when the user selects that mod's name from the list.

 

As you can see, I'm using this strange 'XGParamTag' class to transfer the event data. I had this hard requirement for myself that I had to use only types available in the Xcom 2 source scripts so that any mod could consume this pattern. The 'XGParamTag' class was the simplest object I could find in their codebase that could actually be cast as an 'object' and still contain the information we need for the pattern to function (a string or two). Yeah, it's a hack.

 

 

 

class ExampleMod_UIModMenuScreenListener extends UIScreenListener;

event OnInit(UIScreen Screen)
{
	local XGParamTag MenuTextParams;

	if (Screen.IsA('UIModOptionsMenu_ModOptionsMenu'))
	{
		if (Screen.IsA('UIModOptionsMenu_HQModOptionsMenu'))
		{
			`LOG("Mod Options Menu: Example Mod observed HQ Mod Options Menu screen. Triggering event.");

			MenuTextParams = new class'XGParamTag';
			MenuTextParams.StrValue0 = "My Example Mod";
			MenuTextParams.StrValue1 = "ExampleMod.ExampleMod_UIHQModOptionsScreen";

			`XEVENTMGR.TriggerEvent('ModOptionsMenu_AddItem', MenuTextParams,, none);
		}
		else if (Screen.IsA('UIModOptionsMenu_ShellModOptionsMenu'))
		{
			`LOG("Mod Options Menu: Example Mod observed Shell Mod Options Menu screen. Triggering event.");

			MenuTextParams = new class'XGParamTag';
			MenuTextParams.StrValue0 = "My Example Mod";
			MenuTextParams.StrValue1 = "ExampleMod.ExampleMod_UIShellModOptionsScreen";

			`XEVENTMGR.TriggerEvent('ModOptionsMenu_AddItem', MenuTextParams,, none);
		}
		else if (Screen.IsA('UIModOptionsMenu_TacticalModOptionsMenu'))
		{
			`LOG("Mod Options Menu: Example Mod observed Tactical Mod Options Menu screen. Triggering event.");

			MenuTextParams = new class'XGParamTag';
			MenuTextParams.StrValue0 = "My Example Mod";
			MenuTextParams.StrValue1 = "ExampleMod.ExampleMod_UITacticalModOptionsScreen";

			`XEVENTMGR.TriggerEvent('ModOptionsMenu_AddItem', MenuTextParams,, none);
		}
	}
}

 

 

 

The Mod Options Menu screen then receives the 'ModOptionsMenu_AddItem' event and adds the item to the list.

 

 

 

simulated function EventListenerReturn OnAddItem(Object EventData, Object EventSource, XComGameState GameState, Name EventID)
{
	local XGParamTag MenuTextParams;
	
	MenuTextParams = XGParamTag(EventData);

	`LOG("Mod Options Menu: Mod Options Menu received event " @ MenuTextParams.StrValue0 @ ", " @ MenuTextParams.StrValue1 @ ".");

	if (MenuTextParams == none || MenuTextParams.StrValue0 == "" || MenuTextParams.StrValue1 == "")
		return ELR_NoInterrupt;

	CreateListItem(MenuTextParams.StrValue0, MenuTextParams.StrValue1);

	return ELR_NoInterrupt;
} 

 

 

 

The final step occurs when the client clicks on a mod name in the mod list. The Mod Options Menu screen instantiates the class specified by the client mod and pushes it to the top of the screen stack.

 

 

 

simulated function OpenModOptions()
{
	local int				ItemIndex;
	local string			ModUIClassName;
	local class<UIScreen>	ModUIClass;
	local UIScreen			ModOptionsScreen;

	`LOG("Mod Options Menu: Mod Options button clicked.");

	ItemIndex = List.GetItemIndex(List.GetSelectedItem());
	ModUIClassName = ModUIClassNames[ItemIndex];
	ModUIClass = class<UIScreen>(PC.Pres.DynamicLoadObject(ModUIClassName, class'Class'));

	if (ModUIClass != none)
	{
		if (Movie.Stack.GetScreen(ModUIClass) == none)
		{
			ModOptionsScreen = PC.Pres.Spawn(ModUIClass, PC.Pres);
			Movie.Stack.Push(ModOptionsScreen);

			`LOG("Mod Options Menu: Mod " @ ModUIClassName @ " UI spawned.");
		}
	}
	else
	{
		`REDSCREEN("Mod Options Menu: Failed to create mod " @ ModDisplayNames[ItemIndex] @ " options screen class " @ ModUIClassName @ ".");
	}
} 

 

 

 

I've also put together a very simple example mod that uses this API (attached to this post).

 

Afterword

So. Is this pattern something you'd be interested in using? Would it have to be improved in some way for you to consider using it?

Link to comment
Share on other sites

That is incredibly f*#@ing awesome. Thank you. I have literally just implemented my own notifications system to inform people of updates to my mod, in-game, because no-one ever checks the workshop.

 

I was having a think about how to integrate options into the core game like that, and the problem for me (at least) is that much of my stuff can't be changed at run-time - things like altering settings on tech templates, which are protected by default, and require the game to reload. Annoyingly.

 

That being said, this is still awesome. Good job.

Link to comment
Share on other sites

That is incredibly f***ing awesome. Thank you. I have literally just implemented my own notifications system to inform people of updates to my mod, in-game, because no-one ever checks the workshop.

 

I was having a think about how to integrate options into the core game like that, and the problem for me (at least) is that much of my stuff can't be changed at run-time - things like altering settings on tech templates, which are protected by default, and require the game to reload. Annoyingly.

 

That being said, this is still awesome. Good job.

 

Write-protected template members is definitely one of the more annoying things I've run into with the SDK so far. I haven't found any solution other than to just replace the template outright using the template manager override.

Link to comment
Share on other sites

Awesome work! I love that there are so many of us trying to find a good answer to this problem.

 

Just wanted to add an extra link to the discussion. A few of us had been discussing a different approach that was nearing completion as well. Kept it in a public forum so that anybody could see the details. You can see both the existing code and the lively API design debate here: https://github.com/andrewgu/ModConfigMenu

Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...