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

From Total War Modding
Line 221: Line 221:
<br>
<br>
Any command you find in Context Viewer can be executed in much the same way. Once you find an entry point into the context system, be that from a character CCO, a faction CCO, or back to basics with a UI component, you can nearly always find a way to string a series of queries together that gets you to the command (or final query) you want.
Any command you find in Context Viewer can be executed in much the same way. Once you find an entry point into the context system, be that from a character CCO, a faction CCO, or back to basics with a UI component, you can nearly always find a way to string a series of queries together that gets you to the command (or final query) you want.
=== Applying CCOs to UI Components ===
I'm not really going to get into this in this guide, as this lies squarely within the realm of advanced [[UI:Main Page|UI modding]], but I will just mention that you can, if you wish, manually attach CCOs to UI Components. This can be done through the UIComponent interface, using the [https://chadvandy.github.io/tw_modding_resources/WH3/campaign/uicomponent.html#function:uicomponent:SetContextObject SetContextObject method]. You would want to do this if you were setting up your own UI elements and wanted to tie them into the context system. If aren't already knowledgeable with TW_UI and Context Callbacks, however, I would advise caution: You can cause all kinds of weird behavior with this command.


== Part 5: CCO Shortcuts and Tricks ==
== Part 5: CCO Shortcuts and Tricks ==

Revision as of 12:37, 10 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.

CCO Categories

Although there is nothing fundamental that differentiates one CCO from another -- they can all be used in essentially the same way -- CA has seen fit to label some CCOs in a manner that groups them into useful categories to indicate for what purpose they should be used. The important categories are as follows:

  • CcoCampaign_____: This group is for CCOs dealing with entities on the campaign map, e.g., armies, factions, settlements, agents, units, etc.
  • Cco_____Record: This group is for CCOs dealing with database information for various parts of the game. That's right, using CCOs, you can retrieve (some) database information via script.
  • CcoBattle_____: This group is for CCOs dealing with entities on the battle map, e.g., units, capture points, buildings, alliances, etc.
  • CcoComponent: This is not a group but one particular type of CCO, the one that corresponds directly to UI components. You can think of CcoComponent objects as the root objects for the entire context system; they're rather important.

Everything that isn't categorized into one of the above can be considered miscellaneous. They have various functions, but you won't necessarily get any hints in the name. Luckily, 90%+ of what you'll want to use CCOs for will probably come from the first two categories.

Why Use CCOs?

So CCOs are objects embedded into UI components, but you're not interested in UI modding, so why should you 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-built 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 options you can run from these CCOs. All CCO options do one of two things: Either they extract information from the game and return it, 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 this. 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 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

"That's all very interesting," I hear you pondering, "but how is this practically useful for me, the modder?" I'm glad you asked. In this section, I will go over just that: practical uses for CCOs in script mods.

Retrieving Game Database Information

The CCO_____Record category of CCO includes a vast array of information replicated from game database (DB) entries, a concept with which you must already be familiar if you've made it far enough in Total War modding that you clicked on a page about CCOs. By no means is the entire DB replicated in Record CCOs, but almost all of the most useful stuff is, and the CCO interface provides a method (the only method!) for a modder to retrieve this information from within a script at runtime.

Within the context system, you will typically find these Record CCOs attached to their relevant campaign entity CCOs. For example, when examining a CcoCampaignUnit, you will find that one of its included queries is "UnitRecordContext"; this query will return the CcoMainUnitRecord for this unit. Within this CCO interface, you will find queries that provide information from the DB, such as the unit's DB key, its upkeep cost, or its tier level. While it is true that some of this information could be obtained via the model hierarchy, some of it (like tier level) cannot. As always, a full list of available queries can be found in Context Viewer.

Because CcoMainUnitRecord is one of the CCOs listed above that is supported for direct access via script, you don't actually need to navigate through the context system to first find an applicable unit on the campaign map before accessing this record information. If you just need DB information for a particular unit type, you can use the cco function:

local unit_record_cco = cco("CcoMainUnitRecord","wh_main_emp_inf_swordsmen")
local unit_class = unit_record_cco:Call("ClassName")

The above code will return the unit class for bog-standard Empire Swordsmen, which in this case is "Melee Infantry." You may have noticed that the ObjectID that CcoMainUnitRecord accepts is a string -- not just any string, mind you, but an exact match for this unit's DB key in the main_units table. This is not an accident. For Record CCOs that support direct script access, the ObjectID typically matches the relevant key in the DB, allowing for quick and easy access to these CCOs without the need to look up the ObjectID in Context Viewer.

Retrieving Campaign Information

Campaign CCOs can be used as an alternative to the model hierarchy for retrieving dynamic information about an active campaign, and in some cases, it's the easier option. One example of this that is currently used as part of Community Bugfix Mod is getting information about "faction initiatives," which is the system that drives the "Gifts of Chaos" feature added with the Champions of Chaos DLC. This system extended the "character initiatives" system, which is what is used to track things like Ogre Big Names. Character initiatives, being older, have documented interfaces in the model hierarchy, but faction initiatives do not. Luckily, we can use CCOs instead:

local faction_cco = cco("CcoCampaignFaction","wh_main_chs_chaos") -- CcoCampaignFaction has direct access support, and the ObjectID matches the faction key

local active_undivided_gifts = faction_cco:Call("InitiativeSetList.FirstContext(InitiativeSetContext.Key == 'wh3_dlc20_faction_initiative_set_chs_undivided').ActiveInitiatives.Size")

The above code starts by directly accessing the faction CCO for Archaon's faction (OG Chaos), which is simple enough. It then calls a context expression from this CCO interface, which is essentially a series of queries, the result of which is the number of active Undivided initiatives (Gifts of Chaos) that Archaon is currently rolling with. Don't worry if the context expression part of this seems confusing; I will go into greater detail on these in the last section of this guide.

Using CCO Commands for Campaign Scripting

Generally speaking, when you want to make changes to the game (as opposed to queries from the game) via script, the episodic scripting interface is your best bet. It's straightforward and quite robust, certainly more robust than what is available through CCO commands. Yet there are a few situations where CCOs can do things that episodic scripting cannot, and when you're in such a situation, it's nice to have the added options.

Using CCOs to disband a unit, starting from a character
Forsaken has been forsaken!

One such instance is the scripted disbanding of a unit from an army. If you have the ObjectID of the unit you wish to disband, you can use the cco function to get its CcoCampaignUnit directly and simply call the "Disband" command from there. More often, you'll be starting from the lord leading the army, which is has a CcoCampaignCharacter we can access. For this example, I will disband the first unit of an army. The only information I need to start out with is the CQI of the lord, which is something we can easily get through the model hierarchy.

Because CcoCampaignCharacter is one of the direct access-enabled CCOs, we can get it using the cco function. Conveniently, the ObjectID for a CcoCampaignCharacter matches that character's CQI, so once we have that, we can just plug it in:

local character_cco = cco("CcoCampaignCharacter",9436)

To get from this Character CCO to the unit CCO that we want (the first unit in this lord's army that isn't the lord itself), we need to go through a few steps:

  1. We need to get from our Character CCO to its attached CcoCampaignMilitaryForce, which is the CCO for the army. Context Viewer shows us that this can be accomplished using the "MilitaryForceContext" query. (Side note: CcoCampaignMilitaryForce is not on the list of direct access CCOs, so we could not have started from here using the cco function).
  2. From the Military Force CCO, we need to get a list of units in the army. Context Viewer shows us that this can be accomplished using the "UnitList" query.
  3. From the UnitList, we want to select the second (first non-lord) unit, which is represented by [1] because context expressions use zero-based indexing (CCO lists will be discussed in more detail in the final section of this guide).
  4. Finally, having arrived at the Unit CCO (CcoCampaignUnit), we send the "Disband" CCO command, which immediately disbands the unit.

As a context expression, this is represented as such:

character_cco:Call("MilitaryForceContext.UnitList[1].Disband")


Any command you find in Context Viewer can be executed in much the same way. Once you find an entry point into the context system, be that from a character CCO, a faction CCO, or back to basics with a UI component, you can nearly always find a way to string a series of queries together that gets you to the command (or final query) you want.

Applying CCOs to UI Components

I'm not really going to get into this in this guide, as this lies squarely within the realm of advanced UI modding, but I will just mention that you can, if you wish, manually attach CCOs to UI Components. This can be done through the UIComponent interface, using the SetContextObject method. You would want to do this if you were setting up your own UI elements and wanted to tie them into the context system. If aren't already knowledgeable with TW_UI and Context Callbacks, however, I would advise caution: You can cause all kinds of weird behavior with this command.

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