Extending UI

From Baldur's Gate 3 Modding
(Redirected from UI: Extending UI)

This guide provides an overview on how to mod the UI by extending UI states.

Introduction

When extending a UI state, you can:

  • Add additional UI panels to existing states
  • Add hooks to existing or new UI states
  • Adjust state properties

ⓘ Copying a state property into your extended state will make your version of the property the final version. If the original state needs a fix and changes this property, your mod will override it when it gets loaded on top, nullifying the fix on the original.

Examples

Adding Hooks

The following example adds hooks for UI states to the moddable GameRoot state.

Original:

<!-- Original -->
<ls:State Name = "GameRoot" Layout = "Common" Owner = "All" IsModdable="True">
    <ls:State.Events>
        <ls:StateEvent Name = "GE.OnStateLoadingStart">
            <ls:StateEvent.Actions>
                <ls:SetSubstate Name="Loading"/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
    </ls:State.Events>
</ls:State>

Modded:

<!-- Mod -->
<ls:State Name = "GameRoot" ModType="Extend">
    <ls:State.Events>
        <ls:StateEvent Name = "GE.OnStateMainMenu">
            <ls:StateEvent.Actions>
                <ls:SetSubstate Name="Eula"/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
        <ls:StateEvent Name = "GE.OnStateRunning">
            <ls:StateEvent.Actions>
                <ls:SetSubstate Name="Running"/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
        <ls:StateEvent Name = "GE.OnStateLobby">
            <ls:StateEvent.Actions>
                <ls:SetSubstate Name="Lobby"/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
        <ls:StateEvent Name = "GE.OnBusyShow">
            <ls:StateEvent.Actions>
                <ls:PushState Name="Busy"/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
        <ls:StateEvent Name = "GE.OnSaveShow">
            <ls:StateEvent.Actions>
                <ls:PushState Name="Saving"/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
        <ls:StateEvent Name = "GE.OnMovieShow">
            <ls:StateEvent.Actions>
                <ls:PushState Name="Movie"/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
        <ls:StateEvent Name = "GE.EditorMode">
            <ls:StateEvent.Actions>
                <ls:RemoveAllSubstates/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
    </ls:State.Events>
</ls:State>

Adding a Panel

The following example adds one panel to the PlayerHUD state.

Original:

<!-- Original -->
<ls:State Name = "PlayerHUD" Layout = "Player" Owner = "Player" IsModdable=True>
    <ls:State.Widgets>
        <ls:StateWidget Filename="/MainUI;component/Pages/Overlay.xaml" Layer="Notifications"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/HudIndicator.xaml" Layer="HUD" IgnoreHitTest="True"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/OverheadInfo.xaml" Layer="HUD" IgnoreHitTest="True"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/HotBar.xaml" Layer="HUDTop"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/Minimap.xaml" Layer="HUD"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/TargetInfo.xaml" Layer="HUD" IgnoreHitTest="True"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/PlayerPortraits.xaml" Layer="PopupPanels"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/TurnModeInfo.xaml" Layer="HUD"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/CombatLog.xaml" Layer="HUD"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/CombatantsOverlay.xaml" Layer="HUD"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/CursorText.xaml" Layer="PopupPanels"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/DragAndDropPreview.xaml" Layer="DragAndDrop" IgnoreHitTest="True"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/PassiveRoll.xaml" Layer="PopupPanels"/>
        <ls:StateWidget Filename="/MainUI;component/Pages/AlwaysOnTopOverlay.xaml" Layer="PopupPanels"/>
    </ls:State.Widgets>
    <ls:State.InitialSubstates>
        <ls:InitialSubstate Name="PlayerPortraits" Metadata="InHUD"/>
    </ls:State.InitialSubstates>
    <ls:State.Events>
        <ls:StateEvent Name = "OpenSelectionFlyOut">
            <ls:StateEvent.Actions>
                <ls:AddSubstate Name="SelectionFlyOut"/>
            </ls:StateEvent.Actions>
        </ls:StateEvent>
    </ls:State.Events>
</ls:State>

Modded:

<!-- Mod -->
<ls:State Name = "PlayerHUD" ModType="Extend">
    <ls:State.Widgets>
        <ls:StateWidget Filename="TestModPanel.xaml" Layer="HUD"/>
    </ls:State.Widgets>
</ls:State>

Tutorial

For a simple example, we’re going to add a button to the MainMenu that will open a new panel. This panel on the MainMenu could then be used as a sort of hub to link your extension panels into.

For the initial mod setup, please follow the steps outlined in Getting Started: Creating a New Mod.

We've provided an empty UI mod pack [TODO] for you to use in this tutorial. Unzip it into the main folder of the mod you just created. The resulting structure should look something like this:

The folder structure of the mod pack.

For this example, we’ll just address the keyboard and mouse version of the UI. Open the Keyboard.xaml inside <yourmod>/GUI/StateMachines/Keyboard.xaml with a text editor like Notepad++.

Most of the UI states found inside our main UI are moddable, so we’ll include the MainMenu state in our Keyboard statemachine, inside the ls:StateMachine.States section.

We’ll also add the Extend ModType to it to let the system know that we are going to extend it (by adding our own panel).

 <ls:State Name = "MainMenu" ModType="Extend">
</ls:State>

Next, we’ll need to decide on a name for our new panel. Let’s go with TestModHub. Add it to the state widgets of the PlayerHUD state.

<ls:State Name = "MainMenu" ModType="Extend">
  <ls:State.Widgets>
    <ls:StateWidget Filename="TestModHub.xaml" Layer="HUD"/>
  </ls:State.Widgets>
</ls:State>

Our UI is visually organised into layers. Above, for our TestModHub panel, we select the HUD layer. This is our lowest visual layer. There is only one layer below it: the Default layer, which is set if you don’t select a layer.

Now, we can create a new .xaml file inside the Pages folder. The Pages in this folder are the actual UIs that will show up in-game. The package of files in this guide includes a PageBlueprint.xaml file that you can copy to use as the base for your new file.

<yourmod>/GUI/Pages/TestModHub.xaml

Open your newly created file.

  1. Change the x:Name property to the UI name. In this case, TestModPanel.
  2. Add the <StackPanel>. For test purposes, we’ll go with this:
<StackPanel Orientation="Vertical" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,50,50,0" Background="Magenta" MinWidth="100" MinHeight="100">
        
    </StackPanel>

Your full file should now look like this:

<ls:UIWidget x:Name="TestModPanel"
             xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:ls="clr-namespace:ls;assembly=Code"
             xmlns:System="clr-namespace:System;assembly=mscorlib"
             xmlns:noesis="clr-namespace:NoesisGUIExtensions;assembly=Noesis.GUI.Extensions"
             xmlns:b="https://schemas.microsoft.com/xaml/behaviors"
             xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance {x:Type ls:DCWidget}, IsDesignTimeCreatable=True}">

    <StackPanel Orientation="Vertical" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,50,50,0" Background="Magenta" MinWidth="100" MinHeight="100">
        
    </StackPanel>

</ls:UIWidget>

Save your work, and let’s test it out. There is no way to load the Main Menu in the Editor, so you’ll have to test it locally in the actual game.

Before:

The original Main Menu.

After:

The Main Menu with the new panel (top right).

Now, let’s add two buttons into the StackPanel.

One will be a placebo button. This is just to show how using a StackPanel set up with Orientation="Vertical" will arrange the buttons underneath each other, and to show that the StackPanel scales to its content.

The other button will be functional. We’ll give it a Command and a CommandParameter.

For the ModPanel to perform an action, it needs a Command. We bind the CustomEvent command to it. This command is used to send events to our StateMachine. We add the name of the event into the CommandParameter.

<ls:LSButton x:Name="ModPanel" Style="{StaticResource BrownButtonStyle}" Content="Open Mod Panel" Command="{Binding CustomEvent}" CommandParameter="ToggleModPanel"/>
<ls:LSButton x:Name="Placebo" Style="{StaticResource BrownButtonStyle}" Content="Placebo"/>
The Main Menu with the two new buttons.

We’ll need to add our new custom event handling to the StateMachine, opening another UI panel.

For this, go back to the Keyboard.xaml StateMachine and add the Events section to the extended State. Inside this Events section, we will add our custom event handling. The Events section can contain multiple events.

<ls:State.Events>
    <ls:StateEvent Name = "ToggleModPanel">
        <ls:StateEvent.Actions>
            <ls:SetSubstate Name="ModPanelState"/>
        </ls:StateEvent.Actions>
    </ls:StateEvent>
</ls:State.Events>

Our event handling will have one action: setting the SubState of the MainMenu State. Using the SetSubState action, a State can branch into several SubStates, or it can set a single SubState.

After adding the handling, we need to create the State that needs to be set as the SubState. We’ll add the HideStatesBelow property to this State to showcase that this property gives you the ability to hide all the previous states. The Widget that this State will open is the TestModPanel.xaml, which we will create as follows:

<ls:State Name = "ModPanelState" HideStatesBelow = "True">
    <ls:State.Widgets>
        <ls:StateWidget Filename="TestModPanel.xaml" Layer="Panels"/>
    </ls:State.Widgets>
</ls:State>

Now, we’ll create the Widget that is used in our newly added state.

This time we’ll create a Grid instead of a StackPanel. This is a different type of container that holds UIElements. Without these containers, the UIWidget itself can only hold one UIElement.

We’ll also add a CloseButton which we will position at the top right of the new UI panel, and we’ll add a TextBlock in the centre of our Panel.

Make a new Page: <yourmod>/GUI/Pages/TestModPanel.xaml

<ls:UIWidget x:Name="TestModPanel"
             xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:ls="clr-namespace:ls;assembly=Code"
             xmlns:System="clr-namespace:System;assembly=mscorlib"
             xmlns:noesis="clr-namespace:NoesisGUIExtensions;assembly=Noesis.GUI.Extensions"
             xmlns:b="https://schemas.microsoft.com/xaml/behaviors"
             xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance {x:Type ls:DCWidget}, IsDesignTimeCreatable=True}">

    <Grid HorizontalAlignment="Center" VerticalAlignment="Center" Background="#55000000" MinWidth="500" MinHeight="500">

        <ls:LSButton x:Name="CloseButton" Style="{StaticResource CloseButton}" Margin="0,20,20,0" Command="{Binding CustomEvent}" CommandParameter="ClosePanel" HorizontalAlignment="Right" VerticalAlignment="Top" />
        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Gather Your Party"/>
    
    </Grid>

</ls:UIWidget>
The new widget.

If you test the changes thus far, you’ll notice that pressing this CloseButton doesn’t close your UI panel. To get it working correctly, you’ll need to add the custom event handling to your ModPanelState, this time with the RemoveState action.

<ls:State.Events>
    <ls:StateEvent Name = "ClosePanel">
        <ls:StateEvent.Actions>
            <ls:RemoveState/>
        </ls:StateEvent.Actions>
    </ls:StateEvent>
</ls:State.Events>

With that, this very basic UI mod should work.