UI:Main Page

From Total War Modding
Revision as of 13:53, 30 March 2022 by DrunkFlamingo (talk | contribs) (More CCO usage information)

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 have children, and all but 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. 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 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 two 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, 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.

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.

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, a string identifying the type of Cco to retrieve, and an ID, which is either a string or a number, for the specific instance of that CCO 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)


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

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