Tutorial:Component Context Objects (CCOs): Difference between revisions

From Total War Modding
Line 87: Line 87:
=== Directly Accessing CCOs Within Scripts ===
=== Directly Accessing CCOs Within Scripts ===


If you were to run a third test using the setup above, one where instead of clicking a new settlement, you selected no settlements, your new result would be a script error, and this illustrates the major drawback of using this method for accessing CCOs in scripts: It ''requires'' the UI component from which we are going to get our desired CCO to already exist. In our example, we are using the "settlement_list" component, but this component is only created when the player has selected a settlement. Sure, you could create a UI listener that is triggered by the player clicking on a settlement (running your CCO code only after that happens), and that may be what you want in some circumstances, but often you will find that you want access to the queries and commands contained within a certain CCO without waiting for connected UI elements to pop into existence first. You want a context-less CCO, a C-O, if you will. Thankfully, this, too, is possible -- at least for many of the most commonly useful CCOs.  
If you were to run a third test using the setup above, one where instead of clicking a new settlement, you selected no settlements, your new result would be a script error, and this illustrates the major drawback of using this method for accessing CCOs in scripts: It ''requires'' the UI component from which we are going to get our desired CCO to already exist. In our example, we are using the "settlement_list" component, but this component is only created when the player has selected a settlement. Sure, you could create a UI [[Lua:Commands, Interfaces & Listeners#Listeners_and_Events|listener]] that is triggered by the player clicking on a settlement (running your CCO code only after that happens), and that may be what you want in some circumstances, but often you will find that you want access to the queries and commands contained within a certain CCO without waiting for connected UI elements to pop into existence first. You want a context-less CCO, a C-O, if you will. Thankfully, this, too, is possible -- at least for many of the most commonly useful CCOs.  


==== The cco Function ====
==== The cco Function ====

Revision as of 13:28, 9 July 2023

Welcome to the wonderful world of modding with CCOs. This guide seeks to consolidate and expand upon information that has already been provided by community heavyweights Groove Wizard (in the UI section of this wiki) and DrunkFlamingo (in the Eldritch Knowledge channel on Da Modding Den Discord), dropping everything we know about these mysterious interfaces in one place. This guide will start with foundational topics like "what are these things?" and "why should I care?," moving through the basics and onto some semi-advanced usage examples.

It should be noted that while CCOs were evidently developed by CA primarily as a tool for building user interfaces, this guide will focus more on their applications outside of that primary role, in particular how they can be a useful asset for script-based mods.

Prerequisites

Before going any further with this guide, you really need to have a basic working knowledge of Lua scripting in Total War. If you're completely new to the world of scripting, I highly recommend beginning with Groove Wizard's excellent Lua tutorial series first. Once you're confident in the basics, come on back.

Although it isn't absolutely necessary, strictly speaking, I also highly recommend getting yourself a Lua console to try out the examples in this guide. This will allow you to execute Lua code while the game is running, which is much, much more efficient for testing purposes than continuously changing lines in an external script and restarting the game each time. Conveniently enough, Groove Wizard has also made one of these for us (where would we be without him?), and this is the console that I will be using in my examples.

Part 1: What is a CCO?

What, you don't know? It's in the title, silly. CCOs are "Component Context Objects." There. Section done.

Just kidding. I also had no idea what that was supposed to mean when I started working with these things. So let's unpack it. First, "Component" in this case refers specifically to a UI component. If you have ever delved into the world of UI scripting, you may already be familiar with this concept. If not, just think of everything you can see in the Total War user interface -- the minimap in the corner, the unit panel, the building browser, etc. -- as a component. Many (although not all) of these UI components are part of the "context system," which provides objects (context objects!) containing game information that is meant to be displayed (or at least contained) in some way within the UI component. In the words of CA, from their documentation on the Context Viewer tool (which we will be using frequently later in the guide):

The TW UI (user interface) framework is primarily driven by what we call the ‘context system’.  We expose game objects that we want to represent in the UI as contexts (which we refer to cco for short, you will notice the cco prefix at the start of all context type names).
So a context is basically a type of object that exists in the game.  Some simple examples:
- A battle unit is exposed as a CcoBattleUnit
- A campaign character is exposed as a CcoCampaignCharacter
- A campaign settlement is exposed as a CcoCampaignSettlement
- A special ability is exposed as a CcoUnitAbility
Each context has an interface that we can call both queries on (to get information) as well as commands on (to change game state). We combine these with different behaviours in our UI system to basically build the UI.

Essentially, you can think of CCOs as potentially handy interface points between the UI and the game. They provide a way to extract game info from a UI element or send commands to the game via a UI element. Because the UI state is always changing (when opening new panels, etc.), the interface points that are available are variable (you might even say they are contextual in nature). That said, you can also access many (though certainly not all) CCOs directly, regardless of UI state, and this is generally how CCOs can really shine in script mods -- but more on that later.

TODO: Add a subsection here broadly going over the Cco categories (CcoCampaign, CcoRecord, etc.)

Why Use CCOs?

So CCOs are objects embedded into UI components, but I'm not interested in UI modding, so why should I care? In short, despite being designed as part of the UI system, CCOs have scripting uses outside of the UI. There is game information exposed through CCOs that is not available through the model hierarchy, and there are some script actions you can take via CCO that don't have an equivalent within the episodic scripting interface. You may also find that there comes a time in your modding journey where you need to pull game information from the current UI state, and CCOs are practically purpose-build for this task (example: getting a character CQI from an event feed message). For more specific use cases, you can skip ahead to Part 4 of this guide, but then please, do come back. We're just getting started.

Part 2: CCOs in Context Viewer

Context Viewer is a wonderous tool bequeathed to us by CA that is essential for working with CCOs. If you aren't already familiar with it, stop here and take a moment to read the linked guide on this Wiki, which goes over how to turn it on and get it running.

Got Context Viewer up and running in your game? Great. Let's dive into some CCOs. Context Viewer's most useful feature is that it shows you exactly which CCOs are attached to which UI elements (themselves represented as CCOs here, a specific subtype of CCO called CcoComponent), and it will then show you which commands you can run from these CCOs. All CCO commands do one of two things: Either they extract information from the game and return it (this is what most of them do), or they send some instruction to the game, altering its state. These are referred to as “Queries” and "Commands" in Context Viewer, respectively.

Using Context Viewer, you can mess around with all of these options. Queries will automatically show you what they would return (unless additional arguments are required, in which case you would need to provide them in the context expression test field at the bottom), and commands can be clicked to induce the effect immediately (again, unless additional arguments are required). This is great for rapid experimentation.

Navigating to the settlement_list UI component

As an example, open any campaign, then open Context Viewer, then select any settlement anywhere on the map. I will be using my current Nurgle campaign, but any settlement in any campaign will work just as well.

The screenshot to the right shows how to use Context Viewer to inspect a particular UI element for possible CCO options contained within it. In this case, that element is the settlement_list component, which is a child component of settlement_panel, the main component for all of the settlement info UI elements.

With the settlement_list component selected in the component tree, you will see that the main panel now shows all of the options that are contained within this component. There are various queries and commands that can be called on the UI component itself (CcoComponent), and one such query -- the one we really care about for this example -- is "ContextsList." This lists all of the other contexts (i.e., other CCOs) attached to this component. You will see that there are two: One CcoCampaignProvince, which is a CCO containing queries and commands for the whole province, and one CcoCampaignSettlement, which is a CCO containing queries and commands for the currently selected settlement. Clicking on either option in ContextsList will open the query and command options for that CCO, which you are encouraged to explore.

It should be pointed out that there can never be more than one CCO of a given type attached to a given component at any one time. In this example, the CcoCampaignSettlement attached to the settlement_list UI component is for Dragon Fang Mount, but this result is contextual; it depends on the settlement that is currently selected. Click on a different settlement (or use a different UI component when we get to script), and this result will change.

The important point about all this is that all of the options we just explored, as well as anything else you find in Context Viewer, can be made available to you in script, as we will see in the next section. Think of Context Viewer as your guide for what is possible with CCOs in scripting.

Part 3: Utilizing CCOs Within Scripts

Of course, you’re not here just to play around with Context Viewer. Obviously, for any of this to be useful, you need to be able to utilize it within your mod scripts. There are a few ways of doing this, the most reliable of which is through UIComponent script objects. Using this method, you will be able to access any CCO you can find through Context Viewer.

Accessing CCOs via UIComponent Script Objects

General UI scripting is not the focus of this guide (you can find more on that here), but for our purposes here there are a few things you need to know:

  1. A "UIComponent script object" is another script interface designed specifically for script-based manipulation of UI elements themselves. It is separate from but related to CCOs in that both interfaces deal with the UI.
  2. The simplest way to access any UIComponent is through the find_uicomponent function, which allows you to input a search path through the UI hierarchy (identical to the component tree seen in Context Viewer, which you should use as your guide) and get back the UIComponent (or "UIC" for short) for the UI element that you want.
  3. Once you have your UIC, you can run its GetContextObject method to get the particular CCO you want to work with.

For example, let's return to the settlement_list component we were examining in Context Viewer in the previous section, but this time let's access those options via script. First, we will need to get the UIC for settlement_list:

local uic = find_uicomponent("settlement_list")

As you recall from our example with Context Viewer, this component has two other CCOs attached to it, one CcoCampaignSettlement for the currently selected settlement, and one CcoCampaignProvince for the province in which this settlement exists. Because there is only ever one of a given type of CCO connected to a component at any one time, we can retrieve the settlement CCO script interface simply:

local settlement_cco = uic:GetContextObject("CcoCampaignSettlement")

Testing CCO scripting with Lua console
Result of our test
Same code, new context, different result

Now we have our settlement CCO retrieved as a CCO script interface, which has been assigned to the local variable "settlement_cco." You may wonder why we didn't simply use "cco" since this is a local variable; the answer is that CA has already assigned "cco" as an important global function that we will touch on later in this section, so don't use "cco" as your variable name for CCO script interfaces.

All CCO script interfaces contain the method "Call", which allows you to run any query or command that you saw in your experimentation with Context Viewer previously. For example, let's run both a query and a command using our Lua console so that we can test it "live."

The code we will use is as follows:

local uic = find_uicomponent("settlement_list")
local settlement_cco = uic:GetContextObject("CcoCampaignSettlement")
local name = settlement_cco:Call("Name")
settlement_cco:Call("ZoomTo")

This will first find our UIC. Then, it will get the CcoCampaignSettlement CCO attached to it and save it as a CCO script interface. Finally, through our CCO interface, this code calls the query "Name" and the command "ZoomTo," both of which were shown as available options for this CCO in Context Viewer. The effect of "ZoomTo" can be seen immediately, but in order to use the string that is returned from the "Name" query, we will need to save it to a variable and then print it, which for this particular console can be accomplished using console_print(name). The screenshots to the right demonstrate the whole process and result.

Remember what the second "C" in CCO stands for? That's right: Context. To get a sense of why the context system can be so useful with UI systems, let's try a quick follow-up test. Without changing a single line of code in our Lua console, we're going to click on a different settlement and then run it again.

Notice what happened? The code didn't change, but the context did, so the result is now completely different. This is one of the ways in which CCOs can be especially useful, particularly if you do intend to work on UI scripting.

Directly Accessing CCOs Within Scripts

If you were to run a third test using the setup above, one where instead of clicking a new settlement, you selected no settlements, your new result would be a script error, and this illustrates the major drawback of using this method for accessing CCOs in scripts: It requires the UI component from which we are going to get our desired CCO to already exist. In our example, we are using the "settlement_list" component, but this component is only created when the player has selected a settlement. Sure, you could create a UI listener that is triggered by the player clicking on a settlement (running your CCO code only after that happens), and that may be what you want in some circumstances, but often you will find that you want access to the queries and commands contained within a certain CCO without waiting for connected UI elements to pop into existence first. You want a context-less CCO, a C-O, if you will. Thankfully, this, too, is possible -- at least for many of the most commonly useful CCOs.

The cco Function

Remember when I said that "cco" should not be used as a variable name for your CCO script interfaces? This subsection will explain why. As it turns out, cco(TypeID,ObjectID) is a global function that can be called from anywhere within Total War script. It has two arguments: "TypeID," which is your CCO type (e.g., CcoCampaignUnit), and "ObjectID," which is the specific identifier for that particular CCO (either a number or a string, depending on the CCO type). We never needed an ObjectID in our previous examples because we could rely on UI context; providing the CCO type for that UI component was enough information, but now that we're ignoring the UI, we need additional identity information. But where can we find this information? In Context Viewer, of course! Where else?

TypeID and ObjectID for any CCO provided by Context Viewer
Directly accessing a unit CCO to get its rank level

As you may have already noticed, Context Viewer provides the TypeID and ObjectID of any CCO that you mouse over in its main window. Using this information in conjunction with the cco function, we can directly retrieve a CCO script interface without needing to go through any UI components first. As an example, let's directly access the CcoCampaignUnit interface for this unit of Forsaken in the screenshot to the left. Here's how:

local unit_cco = cco("CcoCampaignUnit",32821)

That's it! Just as before, you can now use the "Call" method on this CCO script interface to run any query or command available for that CCO. The difference now is that because we accessed this CCO directly, the current state of the UI doesn't make a difference; we can use this combination of TypeId and ObjectID to access this CCO from anywhere at any time.

As a quick example, let's use our Lua console to directly access the CcoCampaignUnit CCO for this unit of Forsaken and send a query to get its current rank (the query for this is "ExperienceLevel," as shown in Context Viewer). Now that we don't need to worry about UICs, the code is dead simple:

local unit_cco = cco("CcoCampaignUnit",32821)
local unit_rank = unit_cco:Call("ExperienceLevel")

console_print(unit_rank)

Remember, this is just an example. You can use any of the queries or commands that you find in Context Viewer. There are some some limitations, however.

Direct Access Limitations

At this point, you may be wondering, "Why even bother with the UIComponent method? This cco function is so much more convenient!" Well, there are two reasons. First and foremost, the CCO function does not work for all CCOs. In fact, it only works for about 45 of them, although these include some of the most useful. Below is the full list of CCOs that have direct script access support (AFAIK):

  • CcoCampaignCharacter
  • CcoCampaignFaction
  • CcoCampaignRoot
  • CcoCampaignSettlement**
  • CcoCampaignTechnology
  • CcoCampaignUnit
  • CcoAncillaryRecord
  • CcoArmoryItemSetRecord
  • CcoBattlePurchasableGroupRecord
  • CcoBattlePurchasableItemsJunctionRecord
  • CcoBattlePurchasableItemsUiCategoryRecord
  • CcoBattleRecord
  • CcoBuildingLevelRecord
  • CcoCampaignGroupRecord
  • CcoCampaignMapPlayableAreaRecord
  • CcoCampaignPayloadRecord
  • CcoCampaignPayloadResourceTransactionRecord
  • CcoCampaignPayloadUnitComponentRecord
  • CcoCultureRecord
  • CcoFactionRecord
  • CcoMainUnitRecord
  • CcoMultiplayerCampaignSettingRecord
  • CcoOwnershipProductRecord
  • CcoRitualChainRecord
  • CcoRitualPayloadRecord
  • CcoSubcultureRecord
  • CcoTimeOfLegendsCampaignRecord
  • CcoUiMainThemeOptionRecord
  • CcoUnitPortholeCameraSettingRecord
  • CcoUnitSetToMpUnitCapRecord
  • CcoUnitsCustomBattleTypeCategoryRecord
  • CcoUnitsCustomBattleTypeRecord
  • CcoEffectBundle
  • CcoScriptObject
  • CcoCustomBattleMap
  • CcoFrontendRoot
  • CcoCampaignBasePlague
  • CcoCampaignPlagueIngredient
  • CcoCampaignBuildingSlot
  • CcoCampaignFactionInteraction
  • CcoCampaignPendingActionNotificationQueue
  • CcoComponent
  • CcoBattleRoot
  • CcoBattleSelection
  • CcoBattleUnit

If you attempt to use the cco function with a CCO that is not contained on the list above, it will fail. The only way to access these other CCOs is through the context system, i.e., via UI components.

It is also notable that the ObjectID requirement for the cco function is not 100% accurate. You may have noticed that there are asterisks next to CcoCampaignSettlement above. This is because the listed ObjectID for CcoCampaignSettlement does not work with the cco function. CcoCampaignSettlement interfaces can be accessed directly, but you need to replace the ObjectID found in Context Viewer with the settlement's CQI on the campaign map. Why is this? I have no idea; it was probably a mistake. Are there others like it? Not that I've found in my personal testing, but I have not gone through them all, so maybe. In any case, just be aware that the cco function was manually programmed to expect a particular data format for a particular set of CCO types, and there is always a chance that it won't match up precisely. You will never need to worry about this when accessing CCOs through the context system.

The second limitation to direct access of CCOs is that by accessing them directly, you are setting aside the contextual aspect of their design that can make them so useful in the UI. This isn't a problem if you just want information from a unit record, for example, but if you are intending to use CCOs in the UI scripting realm, you will find that the contextual relationship with UI components is one of their best features.

Part 4: Practical Use Cases for CCOs

SECTION NOT FINISHED, ONLY OUTLINE/NOTES BELOW

  • The above is neat and all, but where does this stuff really get useful?
  • One of the primary use cases of CCOs is getting information from the game database “on the fly” within your scripts, which is not otherwise possible. The context system contains a ton of (although by no means all of the) DB information in the form of Cco___Record objects, which are kind of like sub-objects to the main CcoCampaign____ objects. Again, you can find what is available here via Context Viewer, and it will generally follow logically from a UI element representing whatever it is you’re seeking. There is also a list of all available database table records on the left panel of Context Viewer (“context menu”), allowing for direct lookups.
    • Example: Finding out a unit’s class through its unit record
  • CCOs are also useful for getting campaign information that isn’t easily accessible – or even possibly accessible – using standard model hierarchy queries. Again, all of the possibilities will be contained within Context Viewer. One example that is currently used in CBFM is getting information regarding “initiatives,” which covers things like Ogre Big Names (character initiatives) or Chaos Gifts for Warriors of Chaos factions (faction initiatives). You can use this information to give additional bonuses to a WoC faction that has a certain Chaos Gift active, for example.
    • Example: Getting a list of active Chaos Gifts for a WoC faction
  • Most CCO commands (not queries) accomplish things that could just as easily be done through episodic scripting, which makes them rather redundant to the modder. However, there are exceptions. Some examples: forcibly disbanding a unit (via CcoCampaignUnit), automatically making a dilemma choice (via CcoCampaignEventDilemma).
    • Example: Disbanding a unit

Part 5: CCO Shortcuts and Tricks

SECTION NOT FINISHED, ONLY OUTLINE/NOTES BELOW

  • Using common context commands to quickly get the information (or send the command) you want.
    • You will note that ObjectIDs for many of the common database record CCOs tend to match their DB key entries (e.g., all of the ObjectIDs for CcoMainUnitRecord objects match their corresponding unit key from the DB), which makes looking them up directly particularly easy.
    • ObjectIDs are not 100% reliable! The ObjectID does not work for direct access of CcoCampaignSettlement CCOs, for example. The cco function requires a CQI in this case instead, which was probably a programming error, but it is what it is.
    • common.get_context_value is really the only function you need; common.call_context_command is exactly the same except it never returns anything
    • These commands are no different than running the cco function followed by the Call method; this is just a shortcut.
  • Using the CCO documentation that was included with your game so that you don’t need to hunt through Context Viewer all the time.
    • I saved this for last because the information contained in this documentation is immense and risks being overwhelming if not understood within the context of the context system.
    • Two important sections:
      • CcoContextList
      • Global functions, which can be used anywhere and even called via common commands without arguments.
        • Example: RootComponent
  • Interacting with Cco lists via script (indexing, etc.)
    • Advanced example: Determining how many units of a certain tier are contained within a particular army