This document explains how a solver works, and how to extend Armory by implementing your own if you want.
The five armor pieces (helm, chest, arms, waist, legs) are categorized as armor pieces. The charm is not an armor piece.
The charm is categorized as charm level (explanation bellow).
Armor pieces and charm are categorized as equipment.
A simpler hierarchical view is as follow:
- Equipment (represented by the
IEquipment
interface)- Armor pieces (represented by the
IArmorPiece
interface)- Helm
- Chest
- Arms
- Waist
- Legs
- Charm (represented by the
ICharmLevel
interface)
- Armor pieces (represented by the
A charm level (represented by the ICharmLevel
interface) is what you may actually call a charm in the game. A charm ICharm
represents a charm type, for example the Attack Charm. An ICharmLevel
is a specific charm of this type, for example the Attack Charm I, Attack Charm II or Attack Charm III.
My apologize for this being confusing.
A skill (represented by the ISkill
interface) is actually a general skill type, for example Attack Boost, and a specific level of this skill is called an ability (represented by the IAbility
interface), for example Attack Boost level 2 or Attack Boost level 3.
Decoration and jewel terms are used interchangeably.
A jewel is represented by the IJewel
interface. It has an array of IAbility
in case in the future Capcom decides to create jewels that grant more than one skill. For now it is always populated with a single element.
Before implementing a solver, it is important to understand how a classic solver works.
Note that all solvers do not necessarily work the same way, the implementation is entirely up to you.
The explanations in this document are given based on the default implementation, which is a naive brute-force solving, consisting of a function that tells whether a given combination of parameters is valid or not, and then testing all the combinations against that function.
In such a solver, two aspects are important.
- The test function must be as fast as possible.
- The least combinations possible must be tested.
The point 2 is actually the most important, because the amount of combinations grows exponentially with the amount of equipment involved.
To give an example, with the average case where about 15 armor pieces per category (15 helms, 15 chests, etc...) and 5 charms are elected, it makes 3'796'875 combinations to test.
So because all combinations have to be tested, it is very important to minimize the amount of equipment that take place in the solving, in order to minimize the amount of combinations to test.
Classically, a solver works in 3 phases:
This phase removes all equipment and decorations that do not match any desired skills. This is a trivial step, but very important nonetheless, because the next phase is the most complicated, so it is useful to start the next on a clean base.
This phase is actually the most complicated one. Its role is to mark which equipment will really be involved in the search.
The difference with the phase 1 is that phase 1 removes equipment, whereas phase 2 does not remove anything, it simply marks the equipment that will really be used in the solving. It means that is suggests not to use some equipment, and to prefer some over some others.
This lets the user tweak which equipment he or she wants to still use, or remove anyway, through the Advanced search
window.
Phases 1 and 2 occur during input selection, which are:
- Skill selection
- Weapon slots selection
- Decorations override
- Equipment override
- Rarity selection
- Gender selection
Phases 1 and 2 constitute what is called the solver data, which will be the working input of the phase 3.
This phase will construct a collection of combinations of equipment and jewels to test, based on election done in phase 2, and naively test all combinations, keeping the ones that satisfy the user's desired skills selection.
The test function is complicated to implement as well, but it's a very mechanical and logical work, so you just have to follow many logical rules in order to get a fully functional test function.
It is important to understand that the 3 phases of the default algorithm are not necessarily what must be done.
For example, the phase 1 removes what matches absolutely no desired skill, but you could still want to keep some equipment that match nothing, for a cosmetic purpose or any other reason that would make sense in your solver's context.
Your phase 2 could keep all equipment if you have a crazy blazing ultra fast test function. In such a case, why bother electing.
The only rules you have to respect are:
- Creation of the solver data (happens before the search and lets the user tweak stuffs)
- Finding solution to desired skills (happens during the search)
The rule 1 just described above is the phases 1 and 2 of the default algorithm. The rule 2 is the phase 3.
Here the term solver is used to describe both the solver data and the solver.
All types you will need in order to implement a custom solver are located in assemblies MHArmory.Search.Contracts
and MHArmory.Core
, and you mainly have to implement two interfaces, ISolverData
and ISolver
.
For a logical understanding purpose, the ISolver
interface is being described before the ISolverData
interface. Note that in the timeline of events, and even in construction of all the bricks, the ISolverData
comes first, since it ends up being the input of the ISolver
.
Hereafter is the IExtension
interface:
public interface IExtension
{
string Name { get; }
string Author { get; }
string Description { get; }
int Version { get; }
}
-
Name
: The name of the extension. It has to be unique among other extensions included in Armory. It is recommended to make it a bit verbose in order to avoid possible confusion with other extensions. -
Author
: The name of the extension author. Either your real name, or the name of your GitHub account, or a pseudonym, up to you. -
Description
: The description of the extension, can be used to describe the nature of the algorithm. -
Version
: This property is purely descriptive and no rules are enforced. Simply avoid going decrementing or changing things without incrementing the version. Don't be afraid to reach crazy high version number if needed.
Hereafter is the ISolver
interface:
public interface ISolver : IExtension
{
event Action<double> SearchProgress;
Task<IList<ArmorSetSearchResult>> SearchArmorSets(
ISolverData solverData,
CancellationToken cancellationToken
);
}
-
SearchProgress
event
This event is used to report the solver's progression when running. This event can be raised from any thread. The value to raise has to be in range 0.0 to 1.0 included. -
SearchArmorSets
method
This method receives the solver data, and a cancellation token that indicates the algorithm when to stop because the user requested a cancellation.
This method has to run asynchronously, and upon completion must return a collection of armor set search results.
This structure is returned by the SearchArmorSets
method of the ISolver
interface, and is described as follow:
(The design of this structure has many problems and may change in the future, but very slightly).
public struct ArmorSetSearchResult
{
public static readonly ArmorSetSearchResult NoMatch =
new ArmorSetSearchResult { IsMatch = false };
public bool IsMatch;
public IList<IArmorPiece> ArmorPieces;
public ICharmLevel Charm;
public IList<ArmorSetJewelResult> Jewels;
public int[] SpareSlots;
}
The static property NoMatch
is used by the test function of the default algorithm to return when the input parameters do not satisfy the desired skills.
The IsMatch
property has to be set to true when a set of equipment and jewels satisfies the desired skills.
When this is the case, the properties ArmorPieces
and Charm
have to be set, as well as the Jewels
property. The latter is not a collection of IJewel
, but a collection of ArmorSetJewelResult
(described later in this document).
Finally, you have to set the SpareSlots
property. This has to be a array of N elements where N is the maximum slot size. For the moment it's 3. The value at index i
indicates the amount of spare slots of size i + 1
.
For example, the value at index 2 indicates the amount of spare slots of size 3.
A value [2, 0, 1]
indicates there are 2 spare slots of size 1, and 1 spare slot of size 3.
Hereafter is the ISolverData
interface:
public interface ISolverData : IExtension
{
int[] WeaponSlots { get; }
ISolverDataEquipmentModel[] AllHeads { get; }
ISolverDataEquipmentModel[] AllChests { get; }
ISolverDataEquipmentModel[] AllGloves { get; }
ISolverDataEquipmentModel[] AllWaists { get; }
ISolverDataEquipmentModel[] AllLegs { get; }
ISolverDataEquipmentModel[] AllCharms { get; }
SolverDataJewelModel[] AllJewels { get; }
IAbility[] DesiredAbilities { get; }
void Setup(
IList<int> weaponSlots,
IEnumerable<IArmorPiece> heads,
IEnumerable<IArmorPiece> chests,
IEnumerable<IArmorPiece> gloves,
IEnumerable<IArmorPiece> waists,
IEnumerable<IArmorPiece> legs,
IEnumerable<ICharmLevel> charms,
IEnumerable<SolverDataJewelModel> jewels,
IEnumerable<IAbility> desiredAbilities
);
}
The Setup
method will be called each time the user changes input selection in Armory, with the following arguments:
-
weaponSlots
The slots of the weapon, always containing the maximum amount of slots. For the moment, the maximum amount of slots is 3, so this array will always contain 3 elements. Each element represent the size of the slots. 0 means the slot is unused, 1 means this is slot size 1, 2 for a slot size 2 and 3 for a slot size 3.
For example, if you selected a slot 3 and a slot 1, theweaponSlots
array would be[ 3, 1, 0 ]
. -
heads
,chests
,gloves
,waists
,legs
,charms
Those arguments contain the equipment that match the user selection, so they depend if the user selectedEquipment override
or not, and if yes, what the user selected as owned.
Note that thecharms
argument is a collection ofICharmLevel
instances. -
jewels
This argument provides you the available jewels that match the user selection, so they depend if the user selectedDecorations override
or not, and if yes, what the user selected as owned. It also tells you how many of each are available.
Note that if the user didn't override a decoration, the amount is set toint.MaxValue
so your algorithm doesn't really need a special path to handle an infinite amount of jewels. -
desiredAbilities
This argument provides you the core data of the very purpose of an armor set search application, the desired skills the user want to get once everything equipped.
It informs you about which skills the user wants, and for each skill, which level of that skill is desired.
The properties you have to implement are all read-only, and explained as follow:
-
WeaponSlots
This is essentially what you receive asweaponSlots
input parameter from theSetup
method. -
AllHeads
,AllChests
,AllGloves
,AllWaists
,AllLegs
,AllCharms
Those properties will inform the UI of which equipment have been kept, and among those ones, which ones have been elected. -
AllJewels
This is the remaining jewels you want to keep and pass to the solver. -
DesiredAbilities
Same as the weapon slots, this is essentially what you received as input parameterdesiredAbilities
from theSetup
method.
Note that both ISolverData
and ISolver
interfaces inherit from IExtension
. For your implementations to be registered in the application, you have to add an instance of them to the MHArmory.AvailableExtensions.*
arrays.
It has been decided not to allow dynamic code execution, hence extensions have to be built-in. This allows people to review the code that is going to be embedded, an protect users from possible malicious code execution on their machine.
Optionally, you can have your ISolverData
or ISolver
implementation to inherit from IConfigurable
in order to get a Configure button.
Hereafter is the IConfiguration
interface:
public interface IConfigurable
{
void Configure();
}
As you can see, it is extremely simple. Armory's responsibility is just to make a button available, and call the Configure
method upon button click, nothing else.
All that happens when this button is clicked is up to you. For this purpose, many types and UI related components have been moved to assembly MHArmory.Core.WPF
for you to easily add a configuration window, benefiting from all the good stuffs of the MVVM pattern.
Along the way, you could notice additional types, such as:
-
ISolverDataEquipmentModel
This interface is exceptional, this is the only one (withIConfiguration
maybe) that is not only solver-specific, and is used to glue the UI and the solver data, for the user to be able to communicate custom equipment election. -
SolverDataJewelModel
This is what you receive as input in theSetup
method of theISolverData
interface. It associates a jewel (represented by theIJewel
interface) with the amount of this jewel. -
IAbility
This interface represents a single level of a skill. For example, for the skill Attack Boost, what is called an ability is a level of this skill, so for example Attack Boost level 3. -
ArmorSetJewelResult
This type is a structure and represent a tuple value, jewel plus the amount of this same jewel. Somewhat similar with theSolverDataJewelModel
class, but used only in the search results. -
ArmorSetSearchResult
This type is a structure used by the solver to communicate a search result. It contains the matching armor pieces, charm, and jewels. It also indicates spare slots.