Skip to content

Mod Phases

Sheep-y edited this page Jan 29, 2021 · 12 revisions

This information is for mod authors.

Table of Contents

Phases

A mod may be loaded and called at various phases of the game, in this order:

  1. SplashMod - after assemblies are loaded, before logos (once).
  2. Init, Initialize, MainMod, HomeMod - between "hottest year" and first loading screen, in this order (once each).
  3. HomeOnShow - Before Main Menu is displayed (repeated).
  4. HomeOnHide - When Main Menu switches to a loading screen (repeated).
  5. GameMod, then GeoscapeMod / TacticalMod - When a game is loaded, including new campaign (once each).
  6. GameOnShow, then GeosacpeOnShow / TacticalOnShow - When the game come out from loading screen (repeated).
  7. GeosacpeOnHide / TacticalOnHide, then GameOnHide - When the game switches to a loading screen (repeated).

The phases are loosely grouped into a few types:

  • Init, Initialize, and MainMod are legacy phases.
  • HomeMod, HomeOnShow, HomeOnHide, GameMod, GameOnShow, GameOnHide, GeoscapeMod, GeoscapeOnShow, GeoscapeOnHide, TacticalMod, TacticalOnShow, and TacticalOnHide are Modnix 3 phases.
  • SplashMod is neither, and is always called once when the game is launched, in all Modnix versions.
  • ActionMod is called to handle Actions, which can be at any phase.
  • DisarmMod is called through API by other mods, which can be at any phase or out of phase.

In Modnix 3, if a DLL mod is hooked to any Modnix 3 phases, its legacy phases will not be called.

Otherwise, the last of the legacy hooks will be called. e.g. If a mod has both Init and MainMod, MainMod will be called.

Compatibility: Modnix 2 and before are not aware of Modnix 3 phases and the two special phases. Modnix 1 and 2 only supports SplashMod, Init, Initialize, and MainMod; other phases will be ignored.

SplashMod

Called very early, just after main assemblies are loaded, before logos. Saves have not been scanned, and most game data should be unavailable.

SplashMods should be careful and refer to only the minimal game data and/or classes that is necessary, to avoid accidentally triggering out-of-order data loading. This includes all fields declared by the mod class loaded by SplashMod.

In short, if you do not need SplashMod, do NOT declare SplashMod.

MainMod

Called after basic assets are loaded, before the hottest year cinematic. Virtually the same time as PPML.

PPML mods are also loaded at this phase. Init goes first, then Initialize, then MainMod.

MainMod is considered legacy; a mod may implement it to stay backward compatible with Modnix 2.x and below, while putting real code in the new phases.

New mods can implement HomeMod for roughly the same effect as MainMod, but most mods should move to GameMod, TacticalMod, or GeoscapeMod which speeds up game launch, and in the future may allow mods to be disabled from the home screen.

HomeMod

Called before the home screen is loaded. Mods that need to modify the home screen / main menu should use this phase.

This phase only triggers once. Subsequence return to the home screen will not re-run the phase. Use OnShow/OnHide for repeated triggers.

GameMod

Called before a game is loaded, whether Geoscape or Tactical. New game included. Mods that modify both sides of the game should use this phase, such as equipment stats.

This phase only triggers once. Subsequence loads will not re-run the phase. Use OnShow/OnHide for repeated triggers.

GeoscapeMod

Called before a Geoscape game is loaded, whether new game, loading from save, or returning from a tactical mission. Mods that modify the geoscape side of the game should use this phase, such as research.

This phase only triggers once. Subsequence loads will not re-run the phase. Use OnShow/OnHide for repeated triggers.

TacticalMod

Called the first time a Tactical game is loaded, whether tutorial, loading from save, or entering a new tactical mission. Mods that modify the tactical side of the game should use this phase, such as combat rules.

This phase only triggers once. Subsequence loads will not re-run the phase. Use OnShow/OnHide for repeated triggers.

OnShow/OnHide

OnShow and OnHide phases are triggered after/before the loading screen that enters/exits the relevant phase.

  • HomeOnShow - Triggered when home screen is shown on screen. The screen is loaded and preped.
  • HomeOnHide - Triggered when home screen switches to loading screen.
  • GameOnShow - Triggered when either Geoscape or Tactical game is show on screen.
  • GeosacpeOnShow - Triggered when a Geoscape game is show on screen, after GameOnShow.
  • GeosacpeOnHide - Triggered when a Geoscape game switches to loading screen, before GameOnHide.
  • TacticalOnShow - Triggered when a Tactical game is show on screen, after GameOnShow.
  • TacticalOnHide - Triggered when a Tactical game switches to loading screen, before GameOnHide.
  • GameOnHide - Triggered when either Geoscape or Tactical game switches to loading screen.

OnHide are also called before the game exits, before the loading screen enters play.

Use OnHide to cleanup. Do not use it to save data. OnHide is not guaranteed to be triggered, for example when player press Alt+F4.

Special Phases

In addition to the above standard mod phases, there are also a few non-standard mod phases.

Preload

A mod may declares the dlls to be preloaded in its Preloads field. These dlls will be loaded before the game is loaded. Serious.

Because Preload runs so early, it will not and cannot run any methods or actions, and are generally not recognised as a valid phase.

ActionMod

The ActionMod method is implemented by mods to handle plain-text Actions. It is usually called at the end of each standard mod phases, except DisarmMod phase which may be called anytime.

See Action Specs for details.

DisarmMod

Mods that support this phase can be un-initiated and re-initiated through the API. There are multiple caveats, so please read carefully and don't implement it light-heartedly.

Disarm:

  1. At any time, a mod may call the "mod_disarm" API to uninitiate a mod that support this phase, and has not been disarmed.
  2. Any API command that the mod added, will be removed.
  3. If it is a DLL mod with the public method, and has already been loaded, the DisarmMod method(s) will be called.
  4. If it is an Action mod, the actions that belongs to the DisarmMod phase will then be executed.
  5. The mod is now "disarmed", and will not be included in subsequence phases.
  6. The API then returns.

Rearm:

  1. At anytime, a mod may call the "mod_rearm" API to reinitiate an unloaded mod.
  2. The mod is no longer considered disarmed, and will be included in future phases.
  3. For each phases that the game has experienced, the mod will (re)experience in the same order.
  4. The API then returns.

Now the details and caveats:

  • The mod loader would not call DisarmMod on its own. Only a mod can trigger it by calling the API.
  • Whatever a mod do, it must keep track of them and revert them in the DisarmMod phase. The loader cannot track what a mod did.
  • Repeat, it is up to the mod to stop and undo its past acts, reset states, free objects, release resources, etc.
  • Once a mod is loaded, it cannot be removed from game memory. For dll mods, this means all static variables will be kept if not freed.
  • No hard reference is kept on DLL mod instances. If a non-static mod does not statically reference itself, it may be gc'ed by .Net.
  • Mods with multiple DLLs, will trigger DisarmMod on all DLLs if any DLL has been loaded.
  • Disarmed mods will be delisted from the "mod_list" api, but can still be queried with mod_info.
  • If a mod is disarmed in a phase during which it would otherwise be loaded, the mod would be skipped.
  • If a mod is rearmed in a phase during which it would otherwise be loaded, it would be loaded only once, out of order and by the api call.
  • Some apparently simple changes, like setting a weapon's damage, cannot be simply undone if multiple mods modified the same stats. Reverting to the previous value may undo or invalidate other mod's changes. There is no way to detect it, and it may be wise to not support DisarmMod.
  • DisarmMod was called UnloadMod during development, but changed at the last moment to avoid giving the wrong impression.
  • There is no way to detect / listen that another mod is disarmed, short of patching Modnix. For example, if you run scripts through any Scripting Library mod, your variables will be preserved.

Technical Details

SplashMod is triggered at the end of first call of Cinemachine.CinemachineBrain.OnEnable.

Init, Initialize, and MainMod are triggered at the end of the first call of PhoenixPoint.Common.Game.MenuCrt, after the SplashMod phase. Modnix loads right before the call returns, while PPML 0.1 loads right after the call returns. PPML 0.2 and 0.3 triggers somewhere between SplashMod and MainMod, but are merged here for simplicity.

HomeMod/OnShow/OnHide, GameMod/OnShow/OnHide, GeoscapeMod/OnHide, and TacticalMod/OnShow/OnHide are triggered at the end of OnLevelStateChanged of the relevant class. Home phases use PhoenixPoint.Common.Levels.MenuLevelController, Geoscape phases use PhoenixPoint.Geoscape.Levels.GeoLevelController, and Tactical phases use PhoenixPoint.Tactical.Levels.TacticalLevelController.

  • "Mod" phases happen after the controller state switches from Uninitialized to anything, usually NotLoaded.
  • "OnShow" phases happen after the controller state switches from Loaded to Playing.
  • "OnHide" phases happen after the controller state switches from Playing to anything, usually Loaded.

GeoscapeOnShow (and its relevant GameOnShow) is special, because most geoscape initialisations happen on game timer, after OnLevelStateChanged. It is triggered when the loading tip is hidden, i.e. PhoenixPoint.Common.UI.LoadingTipsController.HideTip, but only for Geoscape.

Load Order

When mod loader initiates, the Mods folder is scanned for mods. Mod Sets (that are not are manually disabled) are expanded immediately.

Qualified dlls and their embedded mod_info will be parsed with Cecil, without loading their code into memory. This make sure disabled mods won't cause a conflict or crash.

Then the mods are filtered in this order:

  1. Sort - All mods are sorted by LoadIndex, then by normalised Id, then by path.
  2. Manual - Manually disabled mods are removed.
  3. Duplicates - Mods with same normalised Ids are removed, leaving only the latest version.
    1. Requires - Mods with unmet requirements are removed.
    2. Avoids - Mods that avoids other mods are checked. If there are any matches, the mod will be removed.
    3. Disables - Mods that disables other mods are processed.
    4. Validate - Mods that have no recognised contents are removed. (Since Modnix 3)

Requires > Avoids > Disables > Validate happen in a resolve loop. If a mod is removed during any step, the loop will restart after the step concludes. The steps will re-run in this order until no mods are removed, up to 30 loops.

Removed mods will not be re-added, even if the condition is invalidated later. This ensures that the resolve loop will not be infinite.

Mods that survive will be loaded when (and only when) the game reaches the phase they mod.