UI state
Status: experimental.
What it is
While the player plays, the game is often busy with something other than the world: a menu is open, the game is paused, a cutscene has taken the controls, or the inventory is locked. This API lets your mod ask about those states, so it does not react to a key press while the player is in a menu. It also tells you when the pause screen opens and closes.
When to use it
- Ignore your mod’s hotkey while a menu is open, the game is paused, or the keys are blocked.
- Hide your own on-screen display while the pause screen is open.
- React when the player pauses the game, dies and sees the death screen, or resumes.
Quick example
This mod writes a log line when the pause screen opens and closes. Its Update method runs every frame, so it also logs when a menu opens or closes.
using ObenseuerCore;
[ModInfo("example.uiwatch", "UI Watch", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public sealed class UiWatch : FrameworkMod
{
private bool menuWasOpen;
protected override void OnLoad()
{
UiState.PauseOpened += death => Log.Info(death ? "The death screen opened." : "The pause screen opened.");
UiState.PauseClosed += () => Log.Info("The pause screen closed.");
}
private void Update()
{
bool menuOpen = UiState.IsMenuOpen;
if (menuOpen == menuWasOpen) return;
menuWasOpen = menuOpen;
Log.Info(menuOpen ? "A menu opened." : "A menu closed.");
}
}
PauseOpened and PauseClosed are events: the framework calls your code each time they happen. Update is the method the game calls once for every frame. See Using events.
Common tasks
Only react to a key press when the player is really playing.
bool CanAct()
{
return !UiState.IsMenuOpen && !UiState.IsPaused && !UiState.IsInputBlocked;
}
Check whether the player can open the inventory right now.
bool inventoryLocked = UiState.InventoryDisabled;
Reference
This is the full list of what the API gives you. Text in brackets at the end of a note is extra detail for experienced modders, and you can skip it on a first read.
UiState menu, pause and input flags
Use these flags to check whether a menu is open, the game is paused, or input is blocked. PauseOpened tells you the pause screen opened, and its flag says whether the player died.
public static class UiState
{
public static bool IsMenuOpen { get; }
public static bool IsPaused { get; }
public static bool IsInputBlocked { get; }
public static bool CursorVisible { get; }
public static bool InventoryDisabled { get; }
public static bool IsTyping { get; }
public static bool PauseEventsAvailable { get; }
public static event Action<bool> PauseOpened;
public static event Action PauseClosed;
}
| Property | Type | What it means | |—|—|—| | IsMenuOpen | bool | true when a game menu is open. | | IsPaused | bool | true when the game is paused. | | IsInputBlocked | bool | true when the player’s keys are switched off. | | CursorVisible | bool | true when the game shows the mouse cursor. | | InventoryDisabled | bool | true when something has locked the inventory. | | PauseEventsAvailable | bool | false when the pause events could not be set up. |
- All flag members are read-only properties. Each returns
falsewhenGameController.instanceorGameUIController.instanceis missing (main menu, loading, no save). IsMenuOpen:GameUIController.IsMenuVisible. It is alsotruewhile the wait or sleep timer is on screen. It does not cover the pause menu, shops or dialogue. Those are separate flags.IsPaused:GameController.GameIsPaused. The pause menu, the saving screen and the load menu set it. Do not confuse it withGameTime.IsPaused, which readsTime.timeScale == 0: the pause menu and the saving screen set the time scale to 0 too, soGameTime.IsPausedis alsotruewhile either is open.IsInputBlocked:GameController.keysDisabled. It istruewhile aControlsDisabledcall with the defaultdisableKeysis active (cutscenes, dialogue, sleep and similar). An open game menu does not set it, becauseShowGameMenupassesdisableKeys: false. The framework’s own input lock also sets it.CursorVisible:GameController.cursorVisible. This is the game’s cursor flag. It does not include the controller-input and computer-screen cases thatUpdateCursorVisibilityadds.InventoryDisabled:GameController.inventoryDisabled. It staystruewhile any holder fromDisableInventoryhas not calledEnableInventory.IsTyping:truewhile a text field has focus, so a mod’s hotkey can stand down. It readsEventSystem.current.currentSelectedGameObjectand accepts aTMP_InputFieldwhoseisFocusedis true on that object or on one of its parents, so a selection that is an inner child of the field still counts; the framework’s own text fields instead compare the selection to the field object itself. It also covers the developer console throughDeveloperConsole.Instance.consoleOpenedandInputField.isFocused. AnyTMP_InputFieldthat Unity focuses sets that selection, whether the player clicked it or the game calledActivateInputFieldorSelect, which is how the in-game computer screens (including the framework’s own app text fields), the notes editor, the editable signs, the new-game and character name fields, the virtual keyboard, the developer console and the framework’s own Mods-menu text fields take the keyboard. It does not cover the WebUi module’s HTML fields, which take the keyboard themselves, a field that holds focus while the event system selection is neither the field nor one of its parents, or aUnityEngine.UI.InputField, which the game does not ship. It is a read: it never changes focus, and it is safe on the main menu and with no save.PauseEventsAvailableisinstalled && !CorePatcher.IsDisabled(owner). It isfalsewhen the pause patches could not be installed, and then the two events never fire.
| Event | Gives you | When it fires |
|---|---|---|
PauseOpened | bool | The pause screen opened. The value is true when it is the death screen. |
PauseClosed | Nothing. | The player left the pause screen and went back to the game. |
Things to know
- Every flag is read only, and each one is
falsewhen there is no game to ask. On the main menu, while a level is loading, or with no save loaded, all five flags arefalse. Reading them never changes anything and never throws an error. (Each returnsfalsewhenGameController.instanceorGameUIController.instanceis missing.) IsMenuOpenistruewhile a game menu is open, and also while the wait or sleep timer is on screen. It does not cover the pause menu, shops or dialogue. Those have their own states. (It readsGameUIController.IsMenuVisible, which also checksWaitingUI.waitingUIisVisible.)IsPausedistruewhile the pause menu, the saving screen or the load menu is open. Do not confuse it withGameTime.IsPaused. That one istruewhenever the game clock is stopped, which also happens while the pause menu or the saving screen is open. (IsPausedreadsGameController.GameIsPaused.GameTime.IsPausedreadsTime.timeScale == 0.)IsInputBlockedistruewhile the game has switched off the player’s keys. That is the case in cutscenes, dialogue, sleep and similar. An open game menu does not set it. A mod that blocks the player’s input with the framework sets it too. (It readsGameController.keysDisabled. AControlsDisabledcall with the defaultdisableKeyssets it, andShowGameMenupassesdisableKeys: false.)CursorVisibleis the game’s own cursor flag. It does not include the cases where the game also shows the cursor for a controller or a computer screen. (It readsGameController.cursorVisible, without whatUpdateCursorVisibilityadds.)InventoryDisabledstaystrueuntil everyone who locked the inventory has unlocked it. Several things can lock it at the same time. (It readsGameController.inventoryDisabled, which staystruewhile any holder ofDisableInventoryhas not calledEnableInventory.)PauseOpened(isDeathScreen)fires right after the pause screen opens. The value istruewhen the pause screen is the death screen shown after the player dies. (It is raised afterPauseMenu.PauseMenuOpenreturns. The argument isDeath.instance.dead.PauseMenu.PausemenuDeathcalls the same open path afterdeadis set.)PauseOpenedcan fire twice in a row. If the pause screen is already open when the player dies, the game opens the death screen without closing the pause screen first, so you get a secondPauseOpened(true). If your handler must act only once, keep your own flag. (The framework does not remove the repeat.PausemenuDeathruns the open path again with no close between.)PauseOpeneddoes not fire when the game fails to open the pause screen. On a scene with no game controller, such as the main menu, the game’s own opening code fails before the framework is told, and the framework does not catch that failure. (PauseMenuOpenthrows atGameController.instance.ChangeCursorStatuswhen there is noGameController, before the postfix runs.)PauseClosedfires when the player goes back to the game from the pause screen. It fires only if the pause screen was really open. (It is raised afterPauseMenu.PauseMenuClosereturns, and only whenPauseIsActive()was true before the call (read in the prefix). In the game code onlyPauseMenu.ResumecallsPauseMenuClose, andCardGameUIcallsResumeonly while the menu is open.)PauseCloseddoes not fire when the player quits to the main menu or to the desktop. Quitting to the menu loads a scene, andEvents.SceneLoadedfires for that instead. Quitting to the desktop ends the game. (PauseMenu.QuitMainMenuandPauseMenu.QuitDesktopdo not callPauseMenuClose.)PauseEventsAvailabletells you whether the two events work. It isfalsewhen the framework could not set them up, and then neither event ever fires. The five flags still work.- There are no events for the five flags, so check them yourself. The game changes them in many places, and the framework could not watch all of them. Read them when you need them, for example once per frame in
Updateor when a key is pressed. Each read is cheap. (ControlsDisabledandControlsEnabled(GameController) writekeysDisabledandcursorVisible.DisableInventoryandEnableInventorywriteinventoryDisabled.ShowGameMenuandHideGameMenu(GameUIController) change the menu state. One postfix would miss the others, and each read is a field read.)
Details
Target: Obenseuer 0.5.08 (buildid 25969064), Unity 6000.0.77f1 Mono, BepInEx 6 Unity.Mono (be.788, the tested build) or BepInEx 5 (5.4.23 or later). Namespace: ObenseuerCore. Files: src/Api/UiState.cs, src/Api/UiStatePatches.cs.
Everything not listed in this reference is internal and may change without notice. Items marked experimental may change too.
The framework changes some of the game’s methods when it starts, so it can tell your mod when the pause screen opens and closes. Those changes belong to one feature group, obenseuer.moddingframework.uistate, the name you will see in the log. The five flags only read the game and need no changes to it.
When you can call it
- Call everything from your mod’s normal code (
OnLoad, event handlers,Update), not from a background thread you started. The game and Unity only work safely on one thread, the main thread (see the glossary). - Your event handlers run inside the game’s own call, one at a time. If one of your handlers throws an error, the framework logs it once and still runs the other handlers.
- The framework does not hold back the pause events while a save is loading or being written. A handler that should ignore those moments can check
SaveData.Busy.
If something fails to set up
The framework changes some of the game’s methods when it starts. If a game update breaks one of them, the framework removes all the changes of the feature group that depends on it, turns the group off, and writes error lines and a warning to the group’s log file in BepInEx\logs. The rest of the framework keeps working.
- If the pause events can’t be set up, only
PauseOpenedandPauseClosedstop. The feature groupobenseuer.moddingframework.uistateturns off, error lines and a warning go to the log, andPauseEventsAvailableisfalse. The five flags keep working. (The game methods involved arePauseMenu.PauseMenuOpenandPauseMenu.PauseMenuClose. The framework’s own code in them never throws into the game: each prefix and postfix catches an error and logs it once.)