Nexus-Mods / NexusMods.App

Home of the development of the Nexus Mods App
https://nexus-mods.github.io/NexusMods.App/
GNU General Public License v3.0
1.05k stars 47 forks source link

Implement extensible system to allow games to have optional functionality #503

Open Al12rs opened 1 year ago

Al12rs commented 1 year ago

Requirements:

GameCapabilities:

Main concept:

GameCapabilities represent common features between games, examples would be:

Capabilities allow the main app and other components to detect whether a certain feature is required/present for the current game.

Capabilities also offer a place for games to expose APIs that are general for multiple games but not preset for all of them. (Getting Saves information, getting script extender data, getting configuration files location, checking if a folder is valid, plugin information).

This way new extensions or features can make use of those APIs without having to define new abstractions.

Each type of capability would be defined by a different abstract class, and these abstractions will need to be exposed by the App to all the extensions.

Capability types can expose methods and functionality useful for the support of the feature, or they could be simple marker types.

Examples of exposed methods:

Each game would have and expose a collection of GameCapability implementations. Game specific implementations of the capabilities would allow customization and configuration of the capability, for the specific game. Eg: name of the script extender, list of valid file extensions for Data installation, rules and sorting implementations.

There could be common implementations that can be reused by multiple games (e.g. multiple bethesda games using the same implementations).

The main application or other components (e.g. mod installers) would then be able to query the game for the supported capabilities, and check whether the currently managed game is supporting a specific capability this way.

Each abstract capability type will have a GameCapabilityId which is a statically defined GUID wrapped in a value object.

The app or component can then use the obtained capability to get details it needs for the support of the feature.

Examples:

[Open Issue: Should capabilities be configured by each game statically by creating new class implementations, or should they be configured dynamically by each game through constructors for example]

Reasoning:

The objective is to expose reusable game specific data/logic, without putting everything inside IGame.

This way games that don't use the features don't have to deal with Stub methods about things that aren't relevant to them, and feature specific logic and data can be moved to dedicated classes.

This system will hopefully make extending functionalities of the App easier and minimize the changes needed to various components for each addition:

This should make adding new games (the most common scenario) the operation that requires the least amount of work/changes (usually just adding implementations of a few classes), so that it can be easy for external developers.

Moving installers inside games:

As a separate concept that can be applied alongside GameCapabilities, mod installers should be provided by the games.

Currently all the installers are registered as singleton in the DI system, this means that if at one point we will have 50 games and each game implements their own installer, during each mod install, 50 different installers are queried for installation.

To avoid this, the idea is that each game provides a list of (potentially configured) installer instances that it supports. During mod install, the main app queries the game for the list of installers to use. The main app the proceeds to use these installer instances to complete the installation instead of using the DI list.

This way only installers that are relevant for the game are used. Installers don't need to know which games support them, since its the game that decides whether a particular installer is supported or not.

[Open Issue: should the list of supported mod installers be part of the IGame interface or instead be part of a GameCapability like ModInstallGameCapability?]

Remaining work:

Al12rs commented 1 year ago

Consider using IServiceProvider for storing the capabilities in the games.

Al12rs commented 1 year ago

Modorganizer 2 has a similar system called GameFeatures:

There is a GameFeatures project where all the interfaces are stored and all the components (extensions or not) can access: https://github.com/ModOrganizer2/modorganizer-game_features/tree/master/src

An example of such a feature is ModDataChecker: https://github.com/ModOrganizer2/modorganizer-game_features/blob/master/src/moddatachecker.h

It just has two methods:

The MO2 main application code checks if the currently managed game supports this feature, and if it does, it uses the dataLooksValid method to give a warning on the modlist for mods that don't appear to have any data valid for the game.

This gameFeature is also used by the basic installer (installerQuick) to try to recognize valid mod folders in a mod archive and then automatically fix the structure to get a valid mod install.

This gameFeature is also used by the manual mod installer to check whether a current configuration of a folder is valid.

This gameFeature is also used by the BAIN installer to check if sub directories contain game related data or not.

With the case of ModDataChecker, we have an example of code reuse for different aspects of the application and other extensions, even though the calling code is using the information for different purposes.

So in this case the advantage of the GameFeatures is that, even though there were 3 or more different customization points, instead of having to implement 3 different interfaces, for each customization point, each game just needs to implement one interface, that includes all the relevant information.

If new features of the App or other extensions need customization for the same thing, then they can reuse the existing ModDataChecker feature instead of having to add another interface and having to implement defaults and game specific implementations again.

Another example could be ScriptExtender: https://github.com/ModOrganizer2/modorganizer-game_features/blob/master/src/scriptextender.h

This interface has all the information regarding the script extender that might be needed by various components of the application (other extensions included):

https://github.com/ModOrganizer2/modorganizer-game_features/blob/cda8fb8de35ea49f26433c98f794b9ec88a3930c/src/scriptextender.h#L15-L37C36

This way, any code that might need to interact or be customized to support script extenders can make use of the capability to:

  1. know that the game uses a script extender
  2. get any details it might need to properly handle that.

This way, adding a new feature or component that needs to be aware of scriptExtender functionality doesn't require the creation of a new interface, implementation of default values, reimplementation of SKSE specific information, because it is all already available in the ScriptExtender capability.

Al12rs commented 9 months ago

I wouldn't say this is resolved still. Specifically I would consider this useful for things like showing the Game specific sections of the UI, such as mod Priority or Game specific loadorders (yeah err120, come up with a better name).

Specifically I would like to have a standard way of doing this rather than having multiple different solutions for each scenario.