UI:Main Page: Difference between revisions

From Total War Modding
Line 208: Line 208:
This section describes a basic pattern for implementing a new UI Component using Lua. This is far from the only way to do things, but it should provide you with a decent template for how to begin building your own UI features.   
This section describes a basic pattern for implementing a new UI Component using Lua. This is far from the only way to do things, but it should provide you with a decent template for how to begin building your own UI features.   


=== Basic Button Pattern by Drunk Flamingo ===
The pattern has four parts:  
The pattern has four parts:  


Line 218: Line 219:
In the case of images, text displays, or other visual elements, only the first three elements are required. The final two elements are specific to adding buttons to the UI.
In the case of images, text displays, or other visual elements, only the first three elements are required. The final two elements are specific to adding buttons to the UI.


=== Get or Create ===
==== Get or Create ====
As you might have guessed, this is a function that returns your new UI Component. The function first attempts to 'get' a component which already exists, and then creates the component if it doesn't.
As you might have guessed, this is a function that returns your new UI Component. The function first attempts to 'get' a component which already exists, and then creates the component if it doesn't.


Line 237: Line 238:
         end
         end
     end</nowiki>
     end</nowiki>
Note that the only thing being done to this UI element is creating it, and setting up aspects of it which don't change: in this case, the image. As a general rule, your 'get or create' function should never include references to gameplay values or bother with setting the dynamic aspects of the element.


=== Update Listeners ===
==== Populate ====
This function's job is to apply any dynamic aspects of your UI component based on the game state. Common examples of things that happen in the populate phase are:


=== Click Listener ===
* Locking or unlocking buttons based on whether the player can afford some cost associated with pressing them.
* Showing or hiding elements based on what region/character the player has selected.
* Building and setting custom tooltips with information from the game.


=== Multiplayer Safe Trigger Listener ===
Essentially: it should make the element look correct and display the correct information based on what the context is.
 
A simple populate function is as follows:
 
    <nowiki>function populate_my_button(MyButton)
        local character = cm:get_character_by_cqi(cm:get_campaign_ui_manager():get_char_selected_cqi())
        local ssm = cm:model():shared_states_manager()
        local script_state_key = "already_pressed_my_button"
        if ssm:get_state_as_bool_value(character, script_state_key) or character:faction():treasury() < 1000 then
            MyButton:SetState("inactive")
            MyButton:SetTooltipText("Go Away!", true)
        else
            MyButton:SetState("active")
            MyButton:SetTooltipText("Click me!", true)
        end
    end</nowiki>
 
 
==== Update Listeners ====
 
==== Click Listener ====
 
==== Multiplayer Safe Trigger Listener ====
[[Category:Unfinished]]
[[Category:Unfinished]]

Revision as of 15:34, 30 March 2022

So, you’ve come to this crazy place. You want to learn about UI modding. Congrats, and good luck.

This section is going to cover a lot – I’d like to share about as much UI modding tips as I have – and because of that, it’s likely things will go back and forth. Primarily, this is a subject based in Lua, and most of the actual UI creation will take place in Lua scripts. Lua knowledge is expected. I’m not going to make this a copy of the Lua chapters, but with UI. If you don’t know what I’m talking about, it should be in the Lua chapters. If it is not, tell me, and I will add it.

There will also be assumptions that you understand what various game objects are – like buildings, effects, localisation – and there will be assumptions that you know how to do them and use them. If you do not – find out, then come back.

The final large assumption this tutorial bears in mind is that you have a goal, a specific thing you want to create. I’m going to mostly be doing lightly guiding, and I’ll be explaining one general concept at a time. We’ll start from the very basics, and will eventually rabbit-hole into a bunch of fun specific tasks – like creating custom tooltips, generating UI from data. If a chapter doesn’t cover whatever your specific needs are – I would read it. The progression is setup in mind for someone trying to go from “create a button” to “create fully custom UI panels and mess with vanilla ones as well”.

Note that this is heavily geared towards WH3, but almost all of the information here will carry over to Three Kingdoms. I’ll try to make clear any differences, if I can recall any!

And without further adieu, let’s get started.

Recommended Setup and Documentation

UI modding requires frequent testing, and as such can be very time consuming. There are, however, things you can do to make it easier.

Many of these things are other mods, such as console tools, which may or may not be available in a given Total War title.

Logging

You should always have script logging turned on when working with Lua, and the UI is no exception.

(Link the debug mods here)

Lua Consoles

Some nerds made some lua console thing you should use it (Vandy fix this).

The UI Context Viewer

Any references on this page to the context viewer refer to this tool, which is available in Warhammer 3. You can read more about it here.

Documentation

It would be difficult (and pointless) for us to reflect all of the available information about available functions and methods for the UI here. Furthermore, the details of what is and is not available will depend on the specific game you are working with.

You should be making active use of documentation in addition to the information on this page. If you need to know what arguments to give to a method, what return value to expect, or what methods are even available to call, documentation is where you will find the answers. In general, most documentation is linked to on this page, maintained by Vandy.

Warhammer 3

You can find the CA Script Documentation for Warhammer 3 hosted here. This documentation contains pages for UIComponents, CampaignUI, and all of CA's scripted utility functions.

Documentation for Component Context Objects can be found locally in the data.pack, in the /documentation/ folder. However, it is also hosted here for convienience.

Relevant Interfaces Types

The interface (or userdata, I'm going to use interface) types that are necesssary for UI modding using Lua are: UIComponents, CampaignUI, and the cco script interface.

UIComponents

UIComponents represent pieces of the UI. They include: text; images, buttons; hud elements, and pretty much everything else you can see or click on. We typically refer to them as UIC, or just components, for shorthand.

Hierarchy

All UIC may have children, and all UIC (except one) of them has a parent. The one that doesn't have any parents is the UI Root, and can be accessed as a variable at any time using core:get_ui_root().

This setup creates a 'tree' or 'hierarchy' of components branching out from the root. If you're modding Warhammer 3, you can see this tree by opening the context viewer and pressing "Component Tree" at the top. The hierarchy typically moves from larger pieces and containers to smaller individual elements.

For instance, my Lord's unit card in the units panel is represented by a UIC named LandUnit0. This UIC It has a child named card_image_holder, which itself has lots of children, such as experience. Each component in this setup plays a different role: the first acts as a container for the other elements and displays a tooltip; the second displays the actual unit card png; and the third represents the experience icon on the unit.

We can also follow this tree backwards. LandUnit0 has a parent named units, which acts as a list and is contained by main_units_panel, which itself is contained within the units_panel, which is a child of the UI root. We refer to this chain of component names as the "path" to the component.

Following the hierarchy is the main way to get access to a UI Component. For instance, if we wanted to define a variable in our script for LandUnit0, we could do this:

local units_panel = UIComponent(core:get_ui_root():Find("units_panel"))

local main_units_panel = UIComponent(units_panel:Find("main_units_panel"))

and so forth until you got to the element you wanted, but that is extremely cubersome, so CA defines a function to do it for you: find_uicomponent. You will call this function a lot when you are UI modding.

To use it, we just provide a parent and the path to the element we want, in this case:

local LandUnit0 = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "units", "LandUnit0").

On the rare occasion we need to, we can also define the parent of any element using

local units = UIComponent(LandUnit0:Parent())

Addresses and the UIComponent() function

In the above function calls, we didn't define our variables as the return values of Find() and Parent(), but rather passed those return values to UIComponent.

That is because those functions don't return the UIComponent that is a child or parent of the current one; instead, they return an address for it. There are three uses for addresses:

  1. You can pass the address to UIComponent() and it will return the UIC which the address points to.
  2. You can use the address with certain other UIC Methods that ask for one, such as Divorce and Adopt
  3. You can pass the address to tostring() and it will give you a string that can be used to refer to that specific UIC. This is useful when you want to distinguish between two UIC with the same name.

One of the most common mistakes when starting out with UI Modding is forgetting to use UIComponent to convert addresses into UIC's before using them. Make sure to pay attention to this distinction.

States

All UI Components have a state, which changes how they appear. For instance, our unit card from the previous example has active hover inactive locked queued raise reinforcement routing selected selected_hover taking_damage taking_damage_selected unknown wavering wavering_selected

That's a lot of states, but it does a good job showing what they're for: they let a single element change how it looks, which of its children are visible, what image it is displaying, and what text and tooltip it shows, and pretty much anything else.

That makes them very useful when you're trying to change the UI. For instance, if I wanted to make it impossible to disband, upgrade, or merge one of your units, I could achieve that by simply calling

local LandUnit5 = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "units", "LandUnit5")

LandUnit5:SetState("inactive")

Creating Elements

UIComponents can be created through two methods: CreateComponent and CopyComponent.

Note that these commands return an address, so make sure to pass this address to UIComponent() in order to get the script object for your new UI component before trying to call methods on it.

AddScriptEventReporter (Warhammer 3)

UIComponents fire certain Lua events by default when they are interacted with. For example, most UI elements will notify the script when they are clicked using the ComponentLClickUp event.

However, there are more detailed events which are not fired by most UIComponents by default in the most recent titles. Examples of this include ComponentMouseOn and ComponentMouseOff, which help modders to detect when elements are hovered.

You can use the AddScriptEventReporter() method on any UIComponent to cause it to begin firing all events. For some elements, this is already enabled by default.

In Warhammer 2 and previous games, all events are fired by all UIComponents without needing to call this command - this is less complicated, but it also means that there is more of a performance impact for using those events.

CampaignUI

The CampaignUI object is a singleton that holds a bunch of useful functions for working with the UI. As the name suggests, it is only available in campaign. The exact details of the CampaignUI object can be found in CA's script documentation, which can be used to check what methods are available in the specific game you are working with.

Multiplayer Compatibility Using TriggerCampaignScriptEvent

One of the most important methods available to the CampaignUI object is TriggerCampaignScriptEvent. This command is important because it is necessary to implement button clicks or other UI actions which modify the game state. We will talk about it here in the context of pressing a button.

This method needs two arguments: a faction command queue index, and a string. The faction command queue index is used to identify which faction pressed the button, while the string is used to transmit any necessary information about what button was pressed and what needs to be done in response.

When called, the game will fire a "UITrigger" event, which can be listened for. This event provides a context with the methods context:faction_cqi(), which returns the CQI passed into the command, and context:trigger(), which returns the string passed into the command.

Here is a simple implementation to serve as an example:

    --listen for the click 
    core:add_listener(
        "MyComponentLClickUpListener",
        "ComponentLClickUp",
        function(context)
            return context.string == "my_button_name"
        end,
        function(context)
            out("Player clicked my Button! This only happens on one computer!")
            local character = cm:get_character_by_cqi(cm:get_campaign_ui_manager():get_char_selected_cqi())
            local trigger_string = "press_my_button:"..character:character_subtype_key()
            CampaignUI.TriggerCampaignScriptEvent(cm:get_local_faction(true):command_queue_index(), trigger_string)
        end,
        true)
    
    core:add_listener(
        "MyUITriggerListener",
        "UITrigger",
        function(context)
            return string.find(context:trigger(), "press_my_button:")
        end,
        function(context)
            local faction = cm:model():faction_for_command_queue_index(context:faction_cqi());
            local subtype_key = string.gsub(context:trigger(), "press_my_button:", "")
            out("Our button was pressed by faction "..faction:name().." with a character subtype "..subtype_key.." passed as additional information")
            out("This happens on both computers!")
        end,
        true)
    
    


In this example, we are taking two things which are local to our computer (who our currently selected character is, the fact that a button was just pressed) and transmitting them through a script event so that they are available to all the computers in the game.

Component Context Objects

The third and final type relevant to UI Modding. We call these Ccos for short. They are objects created by CA to expose game objects to the UI, so that it can be used to populate it. They also often contain commands which are used by the UI to affect the game object.

For instance, every unit in your army has a CcoCampaignUnit attached to its unit card, and each of those Cco has a method IconPath() which returns the unit card of that unit for displaying on the UI. They also contain a method Disband which is used by a button to disband the unit.

We refer to CcoCampaignUnit as the type of Cco.

The cco() Function

The cco() function takes two arguments: the first is the type of Cco we want to retrieve; the second is a string or number uniquely identifying which Cco of that type we want to retrieve.


For example, if we wanted to retrieve the CCO object corresponding to the currently selected character, we could do the following:

   local character_cqi = cm:get_campaign_ui_manager():get_char_selected_cqi()
    local characterContext = cco("CcoCampaignCharacter", cqi)

Here CcoCampaignCharacter is the type of Cco, and the CQI identifies which specific character we want.

For gameplay objects that have a CQI, such as factions, settlements, characters, and forces, the CQI can be used to retrieve their associated Cco.

In cases where you want a Cco for a game object that does not have a CQI, you will need to use another method to find the Context Id you are looking for.

Retrieving Cco Id's from UIComponents

UIComponents have a method GetContextObjectId. which can be used to retrieve the unique identifier of a particular Cco attached to that UIComponent.

This method takes a single argument: a string identifying the type of Cco to retrieve. Elements may have many different Cco's attached to them, but they will only have one of a particular type of Cco.

For instance, a unit card typically has a CcoCampaignUnit, referring to the unit it represents, as well as a CcoCampaignCharacter corresponding to the general character of the force which contains that unit.

If we wanted to define a variable as one of those contexts, we could do so as follows:

   --assume unitCard is a valid UIComponent representing a card on the units panel
    local campaignUnitContextId = unitCard:GetContextObjectId("CcoCampaignUnit")
    local campaignUnitContext = cco("CcoCampaignUnit", campaignUnitContextId)

Using Ccos

Once a CCO is defined, it has a single method which can be used: Call. This method takes a string called a "context expression".

This allows you to use Cco commands and queries from the Lua script. Continuing our example from above, we could retrieve the path to the unit card of our unit by doing:

campaignUnitContext:Call("IconPath")

or disband that same unit using

campaignUnitContext:Call("Disband")

Context expressions can also be chained together. For instance, to get the main_units_tables key associated with our unit, we could do it the long way:

   local mainUnitRecordContext =  campaignUnitContext:Call("UnitRecordContext")
    local main_unit_key = mainUnitRecordContext:Call("Key")

or we could save time by chaining them together as follows:

   local main_unit_key = campaignUnitContext:Call("UnitRecordContext.Key")

You can learn more about context expressions on their page.

Implementation Patterns

This section describes a basic pattern for implementing a new UI Component using Lua. This is far from the only way to do things, but it should provide you with a decent template for how to begin building your own UI features.

Basic Button Pattern by Drunk Flamingo

The pattern has four parts:

  1. The Get or Create function.
  2. The Populate function.
  3. The Update Listeners
  4. The Click Listener
  5. The Multiplayer Safe Trigger Listener

In the case of images, text displays, or other visual elements, only the first three elements are required. The final two elements are specific to adding buttons to the UI.

Get or Create

As you might have guessed, this is a function that returns your new UI Component. The function first attempts to 'get' a component which already exists, and then creates the component if it doesn't.

Why do we do it that way? Because UIComponents are often destroyed as the player navigates the UI and causes it to refresh. That means that every time we want to reference our UIC, we also want to make sure it doesn't exist, and presumably we also want to make it exist if it doesn't. Rather than messy code that manually checks this every time, we formalize it into a single function.

A simple get or create function is as follows:

   function get_or_create_my_button()
        local ui_button_parent = find_uicomponent(core:get_ui_root(), "character_details_panel", "character_context_parent", "skill_pts_holder", "skill_pts")
        local button_name = "my_button_name"
        local existing_button = find_uicomponent(ui_button_parent, button_name)
        if is_uicomponent(existing_button) then
            return existing_button
        else
            local new_button = UIComponent(ui_button_parent:CreateComponent(button_name, "ui/templates/round_small_button"))
            new_button:SetImagePath("ui/skins/default/icon_swap.png")
            return new_button
        end
    end

Note that the only thing being done to this UI element is creating it, and setting up aspects of it which don't change: in this case, the image. As a general rule, your 'get or create' function should never include references to gameplay values or bother with setting the dynamic aspects of the element.

Populate

This function's job is to apply any dynamic aspects of your UI component based on the game state. Common examples of things that happen in the populate phase are:

  • Locking or unlocking buttons based on whether the player can afford some cost associated with pressing them.
  • Showing or hiding elements based on what region/character the player has selected.
  • Building and setting custom tooltips with information from the game.

Essentially: it should make the element look correct and display the correct information based on what the context is.

A simple populate function is as follows:

   function populate_my_button(MyButton)
        local character = cm:get_character_by_cqi(cm:get_campaign_ui_manager():get_char_selected_cqi())
        local ssm = cm:model():shared_states_manager()
        local script_state_key = "already_pressed_my_button"
        if ssm:get_state_as_bool_value(character, script_state_key) or character:faction():treasury() < 1000 then
            MyButton:SetState("inactive")
            MyButton:SetTooltipText("Go Away!", true)
        else
            MyButton:SetState("active")
            MyButton:SetTooltipText("Click me!", true)
        end
    end


Update Listeners

Click Listener

Multiplayer Safe Trigger Listener