UI:Main Page
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
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:
- You can pass the address to
UIComponent()
and it will return the UIC which the address points to. - You can use the address with certain other UIC Methods that ask for one, such as
Divorce
andAdopt
- 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: 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