XCOM 2
Not enough ratings
How to create a custom map + mission for XCOM2
By Beat
   
Award
Favorite
Favorited
Unfavorite
Introduction: Making sure we're all on the same page
First we need to make sure that our tools are set up properly. Follow these instructions to set up your sdk. Click Here. You'll also want to make sure you have the community highlander mod installed as well as the dev tools. Instructions for installing the dev tools can be found in the subreddit linked above. Note that this guide is using mods built with the WOTC sdk for reference, although most of the information provided here will work the same regardless of the sdk you are using. Before we can begin making a custom mission + map, it is important that you read the level design document included in the sdk. You'll need to navigate to the folder where you downloaded the sdk and open the folder labeled documentation. In it, there will be a level design pdf that provides a nice introduction into the process of making maps, as well as how to define the correct definitions in the ini files. The file path will roughly be
/steam/steamapps/common/XCOM 2 War of the Chosen SDK/ Documentation/Content/XCOM2Mods_LevelDesign.pdf

Quick Note about the included templates in the sdk
The sdk has a few templates you can use as a basis for your project. Don't use them. The example mission template is good for educational purposes and to make sure that your setup is correct, but you won't be able to change the name of the mod, nor how it shows up in the mod launcher. Use the ModdedDefaultMod that was mentioned in the first time setup page on the subreddit wiki.
We've got the basics, now to build
Know what to focus on
The level design doc talked about plots, parcels, and pcps. If you're intention is to make a mission that falls in the categories of already existing game modes, then you do not need to worry about that aspect of the design process. However, if you want a mission type that does not exist in the game, then you must create your own plot and at least 1 parcel.
Helpful Links
Here are a few helpful links on map making / level design. Editor basics crash course
Watch a dude use the editor and go over concepts(long video)Level design info from a designer. It is not required to watch any of the videos to understand how to build parcels and plots, but they do help.Make sure that when you are designing your plot that you stay relatively low on the z-plane, but within the box, as tall structures will be viewed oddly due to not being able to scroll that high.
Making a mission that already exists
You simply need to make sure the
sType, Missionfamily, and MapNames
match the names defined in the
DefaultMissionDefs.ini or XCOMMissionDefs.ini in /steam/steamapps/common/XCOM 2 WOTC SDK/ XComGame/config


This guide details more how to make a truly custom mission so if you were only interested in the previous section then this guide can no longer aid you.
Adding a custom guerilla/council op
What needs to be done
In your config folder, you want to add 6 files:
XComMissions.ini, XComMissionDefs.ini, XComMissionSources.ini, and XComSchedules.ini
That has the header [XComGame.XComTacticalMissionManager]
and
XComParcels.ini, XComPlots.ini
That has the header [XComGame.XComParcelManager]

XComMissionDefs.ini

This file contains the the first endpoint in linking your mission and maps.
The two ini variables you need to fill out are
arrObjectiveSpawnInfo and arrMissions
An example of a file definition
;Mission Objective Spawn Info +arrObjectiveSpawnInfo=(sMissionType="DD_SurviveXTurns", bUseObjectiveLocation=True, \\ iMinObjectives=1, iMaxObjectives=1, iMinTilesBetweenObjectives=0, \\ iMinTilesFromObjectiveCenter=0, iMaxTilesFromObjectiveCenter=10000, \\ ArcToSpawn[0]=(ArchetypePath="XComInteractiveLevelActor'AdventChestA.Archetypes.ARC_IA_AdventChestObjective'"), \\ bCanSpawnOutsideObjectiveParcel=false, bReplaceSwapActor=true) ;Mission Data +arrMissions=(MissionName="DD_SurviveXTurns", sType="DD_SurviveXTurns", MaxSoldiers=10, \\ MissionFamily="Survive", \\ AliensAlerted=true, \\ MapNames[0]="DD_Obj_SurviveXTurns", \\ RequiredPlotObjectiveTags[0]="DD_SurviveXTurns", \\ RequiredParcelObjectiveTags[0]="DD_SurviveXTurns", \\ MissionObjectives[0]=(ObjectiveName="Survive", \\ SuccessLootTables[0]=(LootTableName="GiveSupplies"), \\ bIsTacticalObjective=true, bIsStrategyObjective=true), \\ MissionObjectives[1]=(ObjectiveName="Recover", \\ SuccessLootTables[0]=(LootTableName="GiveIntel"), \\ bIsTacticalObjective=true, bIsStrategyObjective=true), \\ MissionObjectives[2]=(ObjectiveName="Kill", \\ SuccessLootTables[0]=(LootTableName="GiveIntel"), \\ SuccessLootTables[1]=(LootTableName="GiveSupplies"), \\ bIsTacticalObjective=false, bIsStrategyObjective=false), \\ MissionObjectives[3]=(ObjectiveName="Grand", \\ SuccessLootTables[0]=(LootTableName="GiveSupplies"), \\ SuccessLootTables[1]=(LootTableName="GiveIntel"), \\ bIsTacticalObjective=false, bIsStrategyObjective=false), \\ MissionSchedules[0]="DD_D2_Survive", \\ CiviliansAlwaysVisible=true, \\ CiviliansAreAlienTargets=true, \\ MinCivilianCount=0, \\ ForcedSitreps[0]="LocationScout", \\ ForcedTacticalTags[0]="NoVolunteerArmy", \\ ForcedTacticalTags[1]="NoDoubleAgent", \\ DisallowUITimerSuspension=true \\ )

Make sure that the interactive actor you've chosen is valid for the OSP you placed on your parcel.

Reading you'll notice a few variables that the design doc does not mention,
ForcedSitreps, ForcedTacticalTags and DisallowUITimerSuspension
  • ForcedSitreps is self explanatory in that it guarantees that the indicated sitrep will be attached to your mission.
  • ForcedTacticalTags is not really explained anywhere that I could find. However, from the example above, it can prevent the free units from either the continent bonus or resistance directives from spawning, I'm sure there are other values that can be found in an ini file somewhere.
  • DisallowUITimerSuspension is how you tell the mission that if a chosen is to spawn, do not stop the timer. When a Chosen spawns in a mission, you'll notice that the timer is suspended. This variable controls that behavior.

Other unmentioned information
  • iMaxTilesFromObjectiveCenter determines how far away from the center of the map the objective can spawn. Set this value to 1000 to ensure that no matter where you place the OSP the objective will load just fine.
  • bIsTacticalObjective when set to true enables a behavior that on mission success, your bleeding out and unconscious units will be returned to you.
  • When you define a mission objective, you can set both 'bIsTacticalObjective' and 'bIsStrategicObjective' to false to make that objective optional.

XComMissionSources
This file is responsible for generating and giving the reward to the player.
;Mission Source/Reward Mapping +arrSourceRewardMissionTypes=(MissionSource="MissionSource_SurviveXTurns", RewardType="Reward_Intel", MissionFamily="Survive", XPackMissionSource=true)

There are five possible reward types
  1. Reward_Intel
  2. Reward_Supplies
  3. Reward_Soldier
  4. Reward_Engineer
  5. Reward_Scientist
Your mission can have all five but only 1 will be chosen to be given and each one requires another definition in the ini file.
If you want your mission to be a guerilla op, the MissionSource definition must be
MissionSource="MissionSource_GuerillaOp"
If you want it to be a council op then
MissionSource="MissionSource_Council"
XPackMissionSource is just a variable that boosts the probability of the mission being chosen if it has not been played as much as the other missions

XComSchedules
This file is responsible for defining the schedules that the mission uses.
An example definition
;A configuration File [XComGame.XComTacticalMissionManager] +MissionSchedules=(ScheduleID="DD_D2_Survive", \\ MinRequiredAlertLevel=1, MaxRequiredAlertLevel=999, \\ IdealXComSpawnDistance=16, \\ MinXComSpawnDistance=8, \\ XComSquadStartsConcealed=False, \\ )

You'll want to put MaxRequiredAlertLevel to 999 to ensure that the mission will spawn no matter how far into the game they are. A lower value will make it so that at one point the player will no longer be able to play your mission. The spawn distance variables are how you can attempt to finesse the system by having your units spawn close to the objective given you have placed an XCOM group spawn at the indicated distance from the objective. Its not 100%, but more likely than not you can have XCOM spawn that close to the objective.

A quick note about spawning
XCOM is very rigid in this process as there is really only two ways to spawn enemies or allies during a mission, randomly or through the use of a TargetPoint. TargetPoints are an asset that you can place on a parcel and then while setting up the kismet instructions, point to as the location of where to spawn the desired enemy/ally units. You cannot use this to spawn xcom as that is determined beforehand. Furthermore, if your parcel is not selected, kismet will spawn them at the center of the map.The other approach is randomly. You can instruct kismet to call reinforcements and then fill out the tileoffset field to a value. What this does is make it so that kismet will spawn the reinforcements somewhere between origin (0, 0, 0) and the offset you indicated. If you attempt to call reinforcements on a valid vector coordinate, kismet won't acknowledge it as valid and spawn the units randomly.

XComMissions.ini
;Missions.ini append for Bomb Disposal example mission [XComGame.XComTacticalMissionManager] +arrMissionTypeAliases=(KeyMissionType="Survive")
Pretty simple file, this is all you need and you should be good to go.

XComPlots.ini
Here is where you define the plot for your mission.
You want all values in the ObjectiveTags array to match what you defined in the RequiredPlotObjectiveTags array in XComMissionDefs. If one has more than the other, it will not be selected. If you want it to be selected for a guerilla op , you must include GuerillaOps as an entry in the array.Council ops do not require a tag.

XComParcels.ini
This is where you define the parcels for your missions. Same rules that apply to XComPlots apply here.

Unfortunately, no matter how many different parcels you put in, The game will only choose the parcel that matches the plot size you marked as the objective parcel and is the chosen parcel for the objective to be placed.
Localization for the guerilla/council op
All you need is one file in the Localization folder called XComGame.int

[EX_BombDisposal X2MissionTemplate] DisplayName="Diffuse Alien Bomb" Briefing="Diffuse Alien Bomb" PostMissionType="Diffuse Bomb" BriefingImage="img:///UILibrary_Common.Xcom_default" ObjectiveTextPools[0]="Diffuse Alien Bomb" ObjectiveTextPools[1]="Hold for further orders" PreMissionNarratives[0]="X2NarrativeMoments.TACTICAL.Recover.Central_Guerilla_Ops_Recover_Item_01" [MissionTimers] TimerBombDisposalTitle="Disarm Alien Bomb" TimerBombDisposalSubtitle="Turns until detonation" [DefaultEX_BombDisposal X2MissionNarrativeTemplate] ObjectiveTextPools[0]="Disarm the Alien Bomb" ObjectiveTextPools[1]="Escape the City" ObjectiveTextPools[2]="Neutralize all enemy targets" ObjectiveTextPools[3]="Hold position until skyranger arrives" ;Dialogue proxies ObjectiveTextPools[4]="CENTRAL" ObjectiveTextPools[5]="Menace 1-5, the bomb is located in a nearby building. Move quickly or a lot of people are going to end up dead - We don't have much time. Disarm the charge and then standby for further orders." ObjectiveTextPools[6]="We have eyes on the target. Remove the Elerium charge from the bomb to disarm it."" ObjectiveTextPools[7]="We've confirmed that the bomb has been disabled." ObjectiveTextPools[8]="Time is running out. Don't get pinned down. Disarm that bomb, no matter the cost!" ObjectiveTextPools[9]="You're out of time! Get to that bomb now or you're dead." ObjectiveTextPools[10]="The priming charge just locked in. Firebrand get out of there right now!" ObjectiveTextPools[11]="Eliminate any remaining hostiles in the vicinity. Once the battlespace is clear Firebrand will approach for extraction." ObjectiveTextPools[12]="We're not detecting any additional hostiles in the area. Move quickly and disarm that bomb before it's too late." Bomb Active ObjectiveTextPools[13]="Menace 1-5, excellent work. We saved a lot of people today and preserved the resistance's reputation in the process." ObjectiveTextPools[14]="We've failed to disarm the bomb. This is a major setback for the resistance, Commander" ObjectiveTextPools[15]="Watch where you're shoo---" [EX_BombDisposal_EleriumCharge X2QuestItemTemplate] FriendlyName="Elerium Charge" FriendlyNamePlural="Elerium Charges" LootTooltip="An incredibly powerful explosive charge made from high density Elerium paste."

If your guerilla op does not use a quest item, you can safely remove the last part.
Adding a true custom mission
The first thing we need to do before we can launch the TQL is make sure that our definitions in the unreal script files are correct. If they aren't, the mission will not show up as an option in the TQL.

The 3 .uc files to go through for this process are
X2MissionSet_YourProjectName, X2Item_YourProjectName, X2MissionNarrative_YourProjectName

There are 3 things we need to verify
  • In these files, you want to make sure that the MissionType property is given the same value as the sType in the .ini files.
  • in X2MissionSet, the parameter passed to AddMissionTemplate matches the value as the sType in the ini files
  • in X2MissionNarrative, `CREATE_X2MISSIONNARRATIVE_TEMPLATE(Template, parameter); where parameter's value is the same as the sType

If all the definitions are set up correctly, You should be able to launch the TQL and select your custom plot and mission.

Using unreal kismet
This is a umap file that indicates the mission behavior. This is how mission failure/success is determined as well as an onslaught of other things. Your mission must have this in order to function. It must be included in the MapNames array.

Our goals for this section
What you want to accomplish here is making sure that your mission behaves how it should. If you can accomplish this, you won't have to worry about this aspect when trying to get the mission to pop up in the game.

Getting the mission to proc
The way that this guide will approach this situation is by taking advantage of the resistance ring.
We'll create a covert action that will activate the mission for the player. This accomplishes two things:
  1. We can ensure that the mission shows up as there is now a process to guarantee it to show up, as opposed to randomly like with guerilla ops
  2. We allow the player to choose if they want to play this mission, as opposed to forcing them. Giving the player the choice makes it so that your mission isn't an annoyance since they get to say whether or not they want to do it.

The next section will have the .uc files needed to make this possible with missing parts that you will need to fill in.
Scripting
Any and all scripting files should be contained in the Src/ProjectName/Classes folder.
This folder will house all files related to kismet and under the hood functionality.

Creating a custom sequence action
While unlikely, it is possible that you may need to create a custom sequence action to be used in kismet. To do so, you're file should look like this
// This is an Unreal Script class SeqAct_Name extends SequenceAction; // declare variables here event Activated() { } defaultproperties { ObjName="Name"; ObjCategory="Unit"; bCallHandler=false; bConvertedForReplaySystem=true; bCanBeUsedForGameplaySequence=true; InputLinks.Empty; }

ObjName is what name of the option to select in the editor will look like and ObjCategory is where the option will be housed. You can look at how other sequence actions are written and defined in the game files to see what you need to do to modify this file to accomplish your goal.
All sequence actions begin with SeqAct so you can search the folder containing the sdk or the game itself for 'SeqAct' and a myriad of files will be pulled for you to select.
Kismet
Kismet is what drives the behavior of the mission. You can open the scripting maps of the mods used as reference for a rough idea of how to accomplish certain tasks, as well as use the example mission scripting map.
X2StrategyElement_MissionSource_Name.uc
Make sure to stay consistent with naming here. The names here will be referenced by their corresponding ini files that they depend on.
Name is whatever you want it to be here
[h5]X2StrategyElement_MissionSource_Name[/h5]
// This is an Unreal Script class X2StrategyElement_MissionSource_Name extends X2StrategyElement_DefaultMissionSources config(GameData); static function array<X2DataTemplate> CreateTemplates() { local array<X2DataTemplate> MissionSources; MissionSources.AddItem(CreateNameTemplate()); return MissionSources; } // //--------------------------------------------------------------------------------------- static function X2DataTemplate CreateNameTemplate() { local X2MissionSourceTemplate Template; `CREATE_X2TEMPLATE(class'X2MissionSourceTemplate', Template, 'MissionSource_Name'); Template.bIncreasesForceLevel = true; Template.bDisconnectRegionOnFail = false; Template.OnSuccessFn = NameOnSuccess; Template.OnFailureFn = NameOnFailure; Template.GetMissionDifficultyFn = GetMissionDifficultyFromTemplate; Template.OverworldMeshPath = "StaticMesh'UI_3D.Overwold_Final.RescueOps'"; // Template.MissionImage = "img:///UILibrary_XPACK_StrategyImages.CovertOp_Reduce_Avatar_Project_Progress"; Template.MissionImage = "img:///UILibrary_XPACK_StrategyImages.CovertOp_Facility_Lead"; Template.MissionPopupFn = NamePopup; Template.WasMissionSuccessfulFn = OneStrategyObjectiveCompleted; Template.GetMissionRegionFn = GetCalendarMissionRegion; Template.GetMissionDifficultyFn = GetMissionDifficultyFromMonth; return Template; } static function NameOnSuccess(XComGameState NewGameState, XComGameState_MissionSite MissionState) { local array<int> ExcludeIndices; ExcludeIndices = GetNameExcludeRewards(MissionState); GiveRewards(NewGameState, MissionState, ExcludeIndices); MissionState.RemoveEntity(NewGameState); class'XComGameState_HeadquartersResistance'.static.RecordResistanceActivity(NewGameState, 'ResAct_Mission_Name_Success'); `XEVENTMGR.TriggerEvent('NameComplete', , , NewGameState); } static function NameOnFailure(XComGameState NewGameState, XComGameState_MissionSite MissionState) { MissionState.RemoveEntity(NewGameState); class'XComGameState_HeadquartersResistance'.static.RecordResistanceActivity(NewGameState, 'ResAct_Mission_Name_Failed'); `XEVENTMGR.TriggerEvent(NameFailed', , , NewGameState); } static function NameOnExpire(XComGameState NewGameState, XComGameState_MissionSite MissionState) { class'XComGameState_HeadquartersResistance'.static.RecordResistanceActivity(NewGameState, 'ResAct_Mission_Name_Failed'); } static function NamePopup(optional XComGameState_MissionSite MissionState) { class'X2Helpers_Mission_Name'.static.ShowMissionNamePopup(MissionState); }

A quick note about the CreateNameTemplate function
you'll notice that in the CreateNameTemplate() function that certain values are given file paths as values. These can be found in the content browser of the editor you used to build your maps. You'll see these images later when selecting the covert action.

How to give certain rewards if conditions are met
The below function is used in the above template. How it works is that it is building an array of ints that the game uses as reference to remove rewards.You can check if objectives created in the ini file are marked complete during execution of the mission via kismet. If they are not, you can withhold the reward from the player.
As you can see, the function goes through the array of mission objectives. The array has two properties we care about: ObjectiveName and bCompleted.
ObjectiveaName is what you labeled the objective in the ini file and its index is what you specified in the ini file as well.
bCompleted is a boolean that indicates whether it was completed or not.
The generated rewards are determined in another file. The rewards are stored in an array and this function removes the specified indexes from that array.
The way this function is currently written, any objective that is not completed will have an entry from the rewards array removed.
Add this function to your file if you want to have the ability to exclude rewards.
static function array<int> GetNameExcludeRewards(XComGameState_MissionSite MissionState) { local XComGameStateHistory History; local XComGameState_BattleData BattleData; local array<int> ExcludeIndices; local int idx; History = `XCOMHISTORY; BattleData = XComGameState_BattleData(History.GetSingleGameStateObjectForClass(class'XComGameState_BattleData')); for (idx = 0; idx < BattleData.MapData.ActiveMission.MissionObjectives.Length; idx++) { if(BattleData.MapData.ActiveMission.MissionObjectives[idx].ObjectiveName && !BattleData.MapData.ActiveMission.MissionObjectives[idx].bCompleted) { ExcludeIndices.AddItem(idx); } } return ExcludeIndices; }
X2Helpers_Mission_Name.uc
This script is responsible for helping out with the UI aspect of the mission on the strategy layer. Replace Name with your mission name as be consistent.

// This is an Unreal Script class X2Helpers_Mission_Name extends Object config(GameCore); static function BuildUIAlert_Mission_Name( out DynamicPropertySet PropertySet, Name AlertName, delegate<X2StrategyGameRulesetDataStructures.AlertCallback> CallbackFunction, Name EventToTrigger, string SoundToPlay, bool bImmediateDisplay) { class'X2StrategyGameRulesetDataStructures'.static.BuildDynamicPropertySet(PropertySet, 'UIAlert_Mission_Name', AlertName, CallbackFunction, bImmediateDisplay, true, true, false); class'X2StrategyGameRulesetDataStructures'.static.AddDynamicNameProperty(PropertySet, 'EventToTrigger', EventToTrigger); class'X2StrategyGameRulesetDataStructures'.static.AddDynamicStringProperty(PropertySet, 'SoundToPlay', SoundToPlay); } static function ShowMissionNamePopup(XComGameState_MissionSite MissionState, optional bool bInstant = false) { local XComHQPresentationLayer Pres; local XComGameState_ResistanceFaction FactionState; local DynamicPropertySet PropertySet; Pres = `HQPRES; FactionState = MissionState.GetResistanceFaction(); BuildUIAlert_Mission_Survive(PropertySet, 'eAlert_NameMissionAvailable', Pres.RescueSoldierAlertCB, 'OnRescueSoldierPopup', FactionState.GetFanfareEvent(), true); class'X2StrategyGameRulesetDataStructures'.static.AddDynamicIntProperty(PropertySet, 'MissionRef', MissionState.ObjectID); class'X2StrategyGameRulesetDataStructures'.static.AddDynamicIntProperty(PropertySet, 'FactionRef', FactionState.ObjectID); class'X2StrategyGameRulesetDataStructures'.static.AddDynamicBoolProperty(PropertySet, 'bInstantInterp', bInstant); }
X2StrategyElement_CovertActions_Name.uc and its ini
This file is responsible for dealing with the ini values defined in your XComGameData.ini as well as localization. Replace Name with your mission name and be consistent.

When the user is in the resistance ring and your mission shows up as an option, this file is partly responsible for what they will see.

class X2StrategyElement_CovertActions_Name extends X2StrategyElement_DefaultCovertActions config(GameData); struct CovertActionSlotStruct { var name StaffSlotTemplateName; var int MinRank; var bool bOptional; var bool bRandomClass; var bool bFactionClass; var bool bChanceFame; var bool bReduceRisk; structdefaultproperties { StaffSlotTemplateName=none; MinRank=0; bOptional=false; bRandomClass=false; bFactionClass=false; bChanceFame=false; bReduceRisk=false; } }; struct OptionalCostStruct { var name ResourceTemplateName; var int Quantity; structdefaultproperties { ResourceTemplateName=none; Quantity=0; } }; var config array<CovertActionSlotStruct> StaffSlots; var config array<OptionalCostStruct> OptionalCosts; var config array<name> Risks; var config array<name> SoldierSlotFilter; var config array<name> StaffSlotFilter; static function array<X2DataTemplate> CreateTemplates() { local array<X2DataTemplate> CovertActions; CovertActions.AddItem(CreateNameMissionTemplate()); return CovertActions; } //--------------------------------------------------------------------------------------- // Survive X Turns //--------------------------------------------------------------------------------------- static function X2DataTemplate CreateNameMissionTemplate() { local X2CovertActionTemplate Template; local int i; `CREATE_X2TEMPLATE(class'X2CovertActionTemplate', Template, 'CovertAction_Name'); Template.ChooseLocationFn = ChooseRandomRivalChosenRegion; Template.OverworldMeshPath = "UI_3D.Overwold_Final.CovertAction"; Template.bMultiplesAllowed = false; Template.Narratives.AddItem('CovertActionNarrative_Name_Skirmishers'); Template.Narratives.AddItem('CovertActionNarrative_Name_Reapers'); Template.Narratives.AddItem('CovertActionNarrative_Name_Templars'); for (i = 0; i < default.StaffSlots.Length; i++) { if ((default.SoldierSlotFilter.Find(default.StaffSlots.StaffSlotTemplateName)) != INDEX_NONE)
{
if (default.StaffSlots.bOptional)
{
Template.Slots.AddItem(CreateDefaultOptionalSlot(default.StaffSlots.StaffSlotTemplateName,
default.StaffSlots.MinRank,
default.StaffSlots.bFactionClass));
}
else
{
Template.Slots.AddItem(CreateDefaultSoldierSlot(default.StaffSlots.StaffSlotTemplateName,
default.StaffSlots.MinRank,
default.StaffSlots.bRandomClass,
default.StaffSlots.bFactionClass,
default.StaffSlots.bChanceFame,
default.StaffSlots.bReduceRisk));
}
}
else if (default.StaffSlotFilter.Find(default.StaffSlots.StaffSlotTemplateName) != INDEX_NONE)
{
Template.Slots.AddItem(CreateDefaultStaffSlot(default.StaffSlots.StaffSlotTemplateName));
}
}

for (i = 0; i < default.OptionalCosts.Length; i++)
Template.OptionalCosts.AddItem(CreateOptionalCostSlot(default.OptionalCosts.ResourceTemplateName, default.OptionalCosts.Quantity));

//Now add risks
for (i = 0; i < default.Risks.Length; i++)
Template.Risks.AddItem(default.Risks);

Template.Rewards.AddItem('Reward_Mission_Name'); // used in the rewards ini file

return Template;
}


//
// IMPORTANT HELPER FUNCTIONS
// ---------------------------------------------------------------------------
private static function CovertActionSlot CreateDefaultSoldierSlot(name SlotName, optional int iMinRank, optional bool bRandomClass, optional bool bFactionClass, optional bool bFameChance, optional bool bReduceRisk)
{
local CovertActionSlot SoldierSlot;

SoldierSlot.StaffSlot = SlotName;
SoldierSlot.Rewards.AddItem('Reward_StatBoostHP');
SoldierSlot.Rewards.AddItem('Reward_StatBoostAim');
SoldierSlot.Rewards.AddItem('Reward_StatBoostMobility');
SoldierSlot.Rewards.AddItem('Reward_StatBoostDodge');
SoldierSlot.Rewards.AddItem('Reward_StatBoostWill');
SoldierSlot.Rewards.AddItem('Reward_StatBoostHacking');
SoldierSlot.Rewards.AddItem('Reward_RankUp');
SoldierSlot.iMinRank = iMinRank;
SoldierSlot.bChanceFame = bFameChance;
SoldierSlot.bRandomClass = bRandomClass;
SoldierSlot.bFactionClass = bFactionClass;
SoldierSlot.bReduceRisk = bReduceRisk;

if (SlotName == 'CovertActionRookieStaffSlot')
{
SoldierSlot.bChanceFame = false;
}

return SoldierSlot;
}

private static function CovertActionSlot CreateDefaultStaffSlot(name SlotName)
{
local CovertActionSlot StaffSlot;

// Same as Soldier Slot, but no rewards
StaffSlot.StaffSlot = SlotName;
StaffSlot.bReduceRisk = false;

return StaffSlot;
}

private static function CovertActionSlot CreateDefaultOptionalSlot(name SlotName, optional int iMinRank, optional bool bFactionClass)
{
local CovertActionSlot OptionalSlot;

OptionalSlot.StaffSlot = SlotName;
OptionalSlot.bChanceFame = false;
OptionalSlot.bReduceRisk = true;
OptionalSlot.iMinRank = iMinRank;
OptionalSlot.bFactionClass = bFactionClass;

return OptionalSlot;
}

private static function StrategyCostReward CreateOptionalCostSlot(name ResourceName, int Quantity)
{
local StrategyCostReward ActionCost;
local ArtifactCost Resources;

Resources.ItemTemplateName = ResourceName;
Resources.Quantity = Quantity;
ActionCost.Cost.ResourceCosts.AddItem(Resources);
ActionCost.Reward = 'Reward_DecreaseRisk';

return ActionCost;
}[/code]

here is the corresponding info you will need to add in XComGameData.ini
Be sure to change Name to your mission name.

[Name.X2StrategyElement_CovertActions_Name] ; Warning: If you activate bRandomClass, make sure you update the following: ; ; RandomSoldierClasses under [XComGame.XComGameState_CovertAction] in XComGameBoard.ini ; ; to support your custom classes or character, or you won't be able to particpate. ; ; StaffSlots adds and modifies the properties of the staff slots of the Covert Action. ; ; StaffSlotTemplateName = The template name of the slot to use. You can find the list in the filters section below. ; NOTE: If the slot is a staff slot (Scientist or Engineer), then no additional parameters are considered for evaluation. ; ; MinRank = the minimum rank for a soldier to participate in the Covert Action. ; bRandomClass = if true, chooses a random class from RandomSoldierClasses. See above for more details. ; bFactionClass = if true, chooses a random faction class. ; bChanceFame = if true, rolls for a chance of a famous soldier requirement. Not sure if used. ; bReduceRisk = if true, then this slot will reduce the risks associated with the CA. ; bOptional = if true, then the staff in that slot will be optional and not required to start the CA ; NOTE: If bOptional is true, then only bFactionClass and MinRank are considered for evaluation. ; +StaffSlots = ( StaffSlotTemplateName = CovertActionSoldierStaffSlot, MinRank = 4, bOptional = false, bRandomClass = false, bFactionClass = false, bChanceFame = false, bReduceRisk = false) +StaffSlots = ( StaffSlotTemplateName = CovertActionSoldierStaffSlot, MinRank = 3, bOptional = false, bRandomClass = false, bFactionClass = false, bChanceFame = false, bReduceRisk = false) +StaffSlots = ( StaffSlotTemplateName = CovertActionSoldierStaffSlot, MinRank = 3, bOptional = true, bRandomClass = false, bFactionClass = false, bChanceFame = false, bReduceRisk = false) ;+StaffSlots = ( StaffSlotTemplateName = CovertActionScientistStaffSlot) ; ; OptionalCosts adds new optional resource costs to reduce the risks of the Covert Action ; ResourceTemplateName = the name of the template of the resource to consume once launched ; Quantity = the number of this particular resource to use ; +OptionalCosts = (ResourceTemplateName = Intel, Quantity = 35) ;+OptionalCosts = (ResourceTemplateName = Suppl
UIMission_Name.uc
Change name to match your mission name.

You'll notice that the UI file is extending the RescueSoldier UIMission. As a result, the landmark used on the map as well as the voiceline that is played are dependent on this. You can attempt to extend another mission UI and see how that affects your mission, however, be sure to deal with the `RescueSoldierPopup` that has been used in other files as it is linked to this mission UI.
class UIMission_Name extends UIMission_RescueSoldier; simulated function InitScreen(XComPlayerController InitController, UIMovie InitMovie, optional name InitName) { super.InitScreen(InitController, InitMovie, InitName); FindMission('MissionSource_Name'); BuildScreen(); } simulated function Name GetLibraryID() { return 'XPACK_Alert_MissionBlades'; } simulated function BuildMissionPanel() { local XComGameState_ResistanceFaction FactionState; FactionState = GetMission().GetResistanceFaction(); LibraryPanel.MC.BeginFunctionOp("UpdateMissionInfoBlade"); LibraryPanel.MC.QueueString(m_strImageGreeble); LibraryPanel.MC.QueueString(""); LibraryPanel.MC.QueueString(FactionState.GetFactionTitle()); LibraryPanel.MC.QueueString(FactionState.GetFactionName()); LibraryPanel.MC.QueueString(GetMissionImage()); LibraryPanel.MC.QueueString(GetOpName()); LibraryPanel.MC.QueueString(m_strMissionObjective); LibraryPanel.MC.QueueString(GetObjectiveString()); LibraryPanel.MC.QueueString(m_strReward); LibraryPanel.MC.EndOp(); LibraryPanel.MC.BeginFunctionOp("UpdateMissionReward"); LibraryPanel.MC.QueueNumber(0); LibraryPanel.MC.QueueString(GetRewardString()); LibraryPanel.MC.QueueString(""); // Rank Icon LibraryPanel.MC.QueueString(""); // Class Icon LibraryPanel.MC.EndOp(); SetFactionIcon(FactionState.GetFactionIcon()); Button1.OnClickedDelegate = OnLaunchClicked; Button2.OnClickedDelegate = OnCancelClicked; Button3.Hide(); ConfirmButton.Hide(); } //-------------- EVENT HANDLING -------------------------------------------------------- //-------------- GAME DATA HOOKUP -------------------------------------------------------- simulated function bool CanTakeMission() { return true; } simulated function EUIState GetLabelColor() { return eUIState_Normal; } //============================================================================== defaultproperties { InputState = eInputState_Consume; Package = "/ package/gfxXPACK_Alerts/XPACK_Alerts"; }
X2StrategyElement_MissionSite_Name.uc
This is a simpler file as it simply for updating the UI when the mission from the covert action is complete. Be sure that UIMission_Name matches the file name that you used in the UIMission_Name.uc


// This is an Unreal Script class XComGameState_MissionSite_Name extends XComGameState_MissionSite; // copied from RM's Genji Redux and Iridar's Duke Nukem mod // Copy ahoy! function MissionSelected() { local XComHQPresentationLayer Pres; local UIMission_Name kScreen; Pres = `HQPRES; // Show the lost towers mission if (!Pres.ScreenStack.GetCurrentScreen().IsA('UIMission_Name')) { kScreen = Pres.Spawn(class'UIMission_Name'); kScreen.MissionRef = GetReference(); Pres.ScreenStack.Push(kScreen); } if (`GAME.GetGeoscape().IsScanning()) { Pres.StrategyMap2D.ToggleScan(); } } function string GetUIButtonIcon() { // 2d nuke logo at the bottom of the screen in the points-of-interest list // return "img:///UILibrary_StrategyImages.X2StrategyMap.MissionIcon_Advent"; // return "img:///UILibrary_StrategyImages.X2StrategyMap.MissionIcon_Retaliation" return "img:///UILibrary_XPack_Common.MissionIcon_ResOps"; }
X2StrategyElement_Rewards_Name.uc and its ini
This file determines rewards based on values declared in the XComGameData.ini
At the top level you will see variables that reference values defined in the ini.
You will also see the use of the function
`SYNC_RAND_STATIC(100)
which is essentially generating a random number with a max value of 100.
As the script is written, if the value generated is less than the value defined in the ini, give that specific reward. You can modify this file or the ini to increase the probability of a certain reward.

// This is an Unreal Script class X2StrategyElement_Rewards_Mission extends X2StrategyElement_DefaultRewards dependson(X2RewardTemplate) config(GameData); var config int MinPrisonBreakDuration; var config int MaxPrisonBreakDuration; var config int RequiredForceLevel; var config int ChanceForRandomRookie; var config int ChanceForRandomFactionSoldier; var config int ChanceForRandomScientist; var config int ChanceForRandomEngineer; var config int ChanceForReward; var config bool bEnableRewardChanceForStandardMission; static function array<X2DataTemplate> CreateTemplates() { local array<X2DataTemplate> Rewards; //Missions Rewards.AddItem(CreateNameMissionRewardTemplate()); return Rewards; } static function X2DataTemplate CreateNameMissionRewardTemplate() { local X2RewardTemplate Template; `CREATE_X2TEMPLATE(class'X2RewardTemplate', Template, 'Reward_Mission_Name'); Template.GiveRewardFn = GiveRescueSoldierReward; Template.GetRewardStringFn = GetMissionRewardString; Template.RewardPopupFn = MissionRewardPopup; Template.IsRewardAvailableFn = IsNameMissionAvailable; return Template; } // // -------------------------------------------------- static function bool IsNameMissionAvailable(optional XComGameState NewGameState, optional StateObjectReference AuxRef) { local XComGameStateHistory History; local XComGameState_CovertAction ActionState; local XComGameState_HeadquartersAlien AlienHQ; local XComGameState_CampaignSettings CampaignSettings; local XComGameState_MissionSite MissionState; History = `XCOMHISTORY; AlienHQ = XComGameState_HeadquartersAlien(History.GetSingleGameStateObjectForClass(class'XComGameState_HeadquartersAlien')); //Disable this reward until Mox is rescued to prevent duplication bugs. CampaignSettings = XComGameState_CampaignSettings(History.GetSingleGameStateObjectForClass(class'XComGameState_CampaignSettings')); if ( CampaignSettings.bXPackNarrativeEnabled && !class'XComGameState_HeadquartersXCom'.static.IsObjectiveCompleted('XP0_M4_RescueMoxComplete') ) { `log("["$ default.class $ "::" $ GetFuncName() $ "] XPACK Narrative is enabled and the player hasn't rescued Mox, not spawning", true, 'Mission Name'); return false; } if(AlienHQ.GetForceLevel() < default.RequiredForceLevel) { return false; } //Disable spawning this reward if an existing mission of this type already exists foreach History.IterateByClassType(class'XComGameState_MissionSite', MissionState) { if (MissionState.Source == 'MissionSource_Name') { `log("["$ default.class $ "::" $ GetFuncName() $ "] Mission State " $ MissionState.ObjectID $ " exists: " $ MissionState.Source, true, 'Mission_Name'); return false; } } // Only one can exist at one time foreach History.IterateByClassType(class'XComGameState_CovertAction', ActionState) { if(ActionState.GetMyTemplateName() == 'CovertAction_NameMission' && (ActionState.bStarted)) //this is dumb but we have to account for this { `log("["$ default.class $ "::" $ GetFuncName() $ "] Covert Action: " $ ActionState.GetMyTemplateName() $ " already exists. bStarted: " $ ActionState.bStarted, true, 'Mission_Name'); return false; } } `log("["$ default.class $ "::" $ GetFuncName() $ "] Successfully cleared all conditions!", true, 'Mission_Name'); return true; } static function GiveRescueSoldierReward(XComGameState NewGameState, XComGameState_Reward RewardState, optional StateObjectReference AuxRef, optional bool bOrder = false, optional int OrderHours = -1) { local XComGameState_MissionSite MissionState; local XComGameState_WorldRegion RegionState; local XComGameState_Reward MissionRewardState; local XComGameState_CovertAction ActionState; local X2RewardTemplate RewardTemplate; local X2StrategyElementTemplateManager StratMgr; local X2MissionSourceTemplate MissionSource; local array<XComGameState_Reward> MissionRewards; local array<XComGameState_WorldRegion> ContactRegions; local XComGameStateHistory History; local XComGameState_HeadquartersResistance ResHQ; local float MissionDuration; local int i, index; History = `XCOMHISTORY; StratMgr = class'X2StrategyElementTemplateManager'.static.GetStrategyElementTemplateManager(); ResHQ = class'UIUtilities_Strategy'.static.GetResistanceHQ(); ActionState = XComGameState_CovertAction(`XCOMHISTORY.GetGameStateForObjectID(AuxRef.ObjectID)); RegionState = ActionState.GetWorldRegion(); if(RegionState == none) { foreach `XCOMHistory.IterateByClassType(class'XComGameState_WorldRegion', RegionState) { ContactRegions.AddItem(RegionState); } RegionState = ContactRegions[`SYNC_RAND_STATIC(ContactRegions.Length)]; } //Reset Mission Rewards MissionRewards.Length = 0; //Set the first reward as the intel reward, because it's Objective 0 RewardTemplate = X2RewardTemplate(StratMgr.FindStrategyElementTemplate('Reward_Intel')); MissionRewardState = RewardTemplate.CreateInstanceFromTemplate(NewGameState); MissionRewardState.GenerateReward(NewGameState, ResHQ.GetMissionResourceRewardScalar(RewardState), RegionState.GetReference()); // Give Intel MissionRewards.AddItem(MissionRewardState); while (MissionRewards.Length < 7) { if (`SYNC_RAND_STATIC(100) < default.ChanceForRandomHighRankSoldier) { RewardTemplate = X2RewardTemplate(StratMgr.FindStrategyElementTemplate('Reward_Soldier')); MissionRewardState = RewardTemplate.CreateInstanceFromTemplate(NewGameState); MissionRewardState.GenerateReward(NewGameState, ResHQ.GetMissionResourceRewardScalar(RewardState), RegionState.GetReference()); // Give High Ranking Soldier MissionRewards.AddItem(MissionRewardState); } else if (`SYNC_RAND_STATIC(100) < default.ChanceForRandomFactionSoldier) { RewardTemplate = X2RewardTemplate(StratMgr.FindStrategyElementTemplate('Reward_ExtraFactionSoldier')); MissionRewardState = RewardTemplate.CreateInstanceFromTemplate(NewGameState); MissionRewardState.GenerateReward(NewGameState, ResHQ.GetMissionResourceRewardScalar(RewardState), RegionState.GetReference()); // Give Faction Soldier MissionRewards.AddItem(MissionRewardState); } else if (`SYNC_RAND_STATIC(100) < default.ChanceForRandomScientist) { RewardTemplate = X2RewardTemplate(StratMgr.FindStrategyElementTemplate('Reward_Scientist')); MissionRewardState = RewardTemplate.CreateInstanceFromTemplate(NewGameState); MissionRewardState.GenerateReward(NewGameState, ResHQ.GetMissionResourceRewardScalar(RewardState), RegionState.GetReference()); // Give Scientist MissionRewards.AddItem(MissionRewardState); } else if (`SYNC_RAND_STATIC(100) < default.ChanceForRandomEngineer) { RewardTemplate = X2RewardTemplate(StratMgr.FindStrategyElementTemplate('Reward_Engineer')); MissionRewardState = RewardTemplate.CreateInstanceFromTemplate(NewGameState); MissionRewardState.Generate


The ini file here is where you can mess with values to achieve the behavior you want.
[Name.X2StrategyElement_Rewards_Name] ; Strategic Settings for Mission Pop-up and Generation ; 3600 seconds * (24 * Num of Hours) ; 17 Days MinPrisonBreakDuration = 75 ; 23 Days MaxPrisonBreakDuration = 150 RequiredForceLevel = 5 ChanceForRandomHighRankSoldier = 25 ChanceForRandomFactionSoldier = 35 ChanceForRandomScientist = 40 ChanceForRandomEngineer = 40 ChanceForRandomRookie = 35
Localization for the true custom mission
Before we begin writing to ini files, we need to make sure that our directory structure is correct
click here to make sure that your directory looks like this, with SurviveXTurns being replaced by your Mission name

Localization/Strategy/Covert Actions/XComGame.int

This file is partly responsible for the text the player will see when selecting the covert action in the resistance ring when it appears as an option.


  • ActionObjective is what the covert action name will be on the side panel when looking for an action to take, similar to recruit engineer or recruit scientist.
  • ActionName is what the player will see at the center of their screen below the mission image.
  • ActionPreNarrative is what will be under ActionName.
  • ActionPostNarrative is what will be shown when the covert action is completed.
  • <XGParam:StrValue1/!ChosenName/> is a shorthand to get the relevant chosen name and use it where you see fit.

The other headers defined are what was defined in the X2StrategyElement_CovertActions_Name.uc file beforehand.

Template.Narratives.AddItem('CovertActionNarrative_Name_Skirmishers'); Template.Narratives.AddItem('CovertActionNarrative_Name_Reapers'); Template.Narratives.AddItem('CovertActionNarrative_Name_Templars');


;A configuration File [CovertAction_Name X2CovertActionTemplate] ActionObjective="Find the Advent Spy" [CovertActionNarrative_Name_Skirmishers X2CovertActionNarrativeTemplate] ActionName="Follow Lead on Disturbing Information" ActionPreNarrative="My kind still hold great knowledge of <XGParam:StrValue1/!ChosenName/>'s facilities and operations. Together, I am confident we will find the Advent operative." ActionPostNarrative="The Advent operative was successful in relaying the position of the Resistance cell. We must move quickly to save it." [CovertActionNarrative_Name_Reapers X2CovertActionNarrativeTemplate] ActionName="Follow Lead on Disturbing Information" ActionPreNarrative="My people were born to hunt, to track and pursue our prey undetected. Send a few of your soldiers to help and I'm sure we'll have no problem figuring out where <XGParam:StrValue1/!ChosenName/> has sent their operative" ActionPostNarrative="We found 'em, just like I expected, but they sent word to their boss and the cell is compromised. Now you just need to send in some of your heavies to pull off the evacuation." [CovertActionNarrative_Name_Templars X2CovertActionNarrativeTemplate] ActionName="Follow Lead on Disturbing Information" ActionPreNarrative="Blessed with the Earth's power, my followers have a gift for finding that which cannot be found. Together, we will find the Advent operative, no matter where <XGParam:StrValue1/!ChosenName/> has sent them." ActionPostNarrative="We have found the operative, but they exposed the position of the cell. You must move quickly to help evacutae the outpost."

Localization/Strategy/MissionName/

MissionName.int

Because we are extending UIMission_RescueSoldier, that must be the header file value

[UIMission_RescueSoldier] m_strFlavorText=Recent Covert Actions have uncovered that Advent has discovered the location of a resistance outpost. A team can go in and delay advent while firebrand helps with evacuation procedures. m_strRescueMission=Outpost Evacuation m_strImageGreeble=SECURE TRANSMISSION

XComGame.int
The first two headers are what were defined in the X2StrategyElement_MissionSource_Name.uc on a success and failure.

Name in X2MissionTemplate should match what the name of the mission is in the X2MissionSet file. What you write their is what will appear in firebrand right before you load into the mission.
The PreMissionNarratives array is what plays the voicelines from central. You can go into the content browser and attempt to find audio files that can work with your mission. Once you have found the audio file, you can right click and select the option that points to the narrative moment that uses the audio file.

Note: BriefingImage never appears to work, whether you use an image from the content browser, or create a upk file and add images to it and then reference them.

The last header is for what the council spokesman will say on a success or failure.

[ResAct_Mission_SurviveXTurns_Success X2ResistanceActivityTemplate] DisplayName="Bases Evacuated:" [ResAct_Mission_SurviveXTurns_Failed X2ResistanceActivityTemplate] DisplayName="Bases Annihilated:" [Name X2MissionTemplate] DisplayName="Help Evacuate the Compromised Resistance Outpost" Briefing="Defend The Area" PostMissionType="Survive" BriefingImage="img:///DD_SurviveXTurns.SurviveXTurnsBriefingImage" ObjectiveTextPools[0]="Defend" ObjectiveTextPools[1]="Secure Important Documents" PreMissionNarratives[0]="X2NarrativeMoments.TACTICAL.Unique.T_Unique_Missions_Terror_Attack_Central_04" [Reward_Mission_Name X2RewardTemplate] DisplayName="Mission that will give resources and personnel to XCOM" RewardDetails="Unlocks a Mission that gives many rewards." [MissionSource_Name X2MissionSourceTemplate] +MissionPinLabel="Evacuate" +BattleOpName="Operation K-1-0" MissionExpiredText="They were good people. What a shame." [Name X2MissionFlavorTextTemplate] FlavorText="MISSING FLAVOR TEXT" CouncilSpokesmanSuccessText="Word of the successful evacuation operation is already spreading. Your efforts today will no doubt bolster the resolve of Resistance forces across the globe, Commander." CouncilSpokesmanFailureText="It is unfortunate you were unable to secure the documents and delay the incursion, Commander. Our allies will no doubt question our methods after such a loss."

Localization/Tactical/MissionName/XComGame.int

This file is responsible for housing Narrative moments and dialogue popups that you may wish to play during the mission through the use of kismet, and Item template names that you want to add to the mission. Take note of the index in the array as you will reference that in kismet as the Line Index node for Display Mission Objective, and Objective Text Index and Title Objective Text index for show tutorial box.

To play voice lines, you must define the file path to the narrative moment in X2MissionNarrative in the array and use that index for Narrative index for Narrative Moment in kismet.

Quest Item is its own .uc script so if you have no need to use a quest item you can ignore.

;A configuration File [Name X2MissionNarrativeTemplate] ObjectiveTextPools[0]="Survive" ObjectiveTextPools[1]="Secure the documents" ObjectiveTextPools[2]="Hold Position until skyranger is finished" ObjectiveTextPools[3]="Evac with the documents" ObjectiveTextPools[4]="Evac XCom" ObjectiveTextPools[5]="BONUS: Kill Advent General" ;Dialogue proxies ObjectiveTextPools[6]="CENTRAL" ObjectiveTextPools[7]="Menace 1-5, I've just received word that important documents have been left behind. Secure those documents and hold this position until skyranger is done helping resistance personnel with evacuation procedures." ObjectiveTextPools[8]="Menace 1-5, you are taking heavy losses. Find a position and hunker down until evac is ready. The resistance personnel must be evacuated first." ObjectiveTextPools[9]="Menace 1-5, nicely done. Killing this general will deal a huge blow to Advent's operation in the region, giving this region hope in rebuilding the cell." ObjectiveTextPools[10]="FIREBRAND" ObjectiveTextPools[11]="Menace 1-5, this is Firebrand. Resistance personnel are out and I'm heading to your position. Deploy flares to begin evac procedures." [Name_ImportantDocuments X2QuestItemTemplate] FriendlyName="Important Documents" FriendlyNamePlural="Important Documents" LootTooltip="Important Information left behind by resistance personnel"
Other ini file values and definitions
From what we have done in this guide, the value in XComMissionSources.ini must change accordingly.

Change name to be what you used in your .uc files and if you choose to change RewardType to a different value, make sure to reflect that in the .uc reward script.

;A configuration File [XComGame.XComTacticalMissionManager] ;Mission Source/Reward Mapping +arrSourceRewardMissionTypes=(MissionSource="MissionSource_Name", RewardType="Reward_Intel", MissionFamily="Survive", XPackMissionSource=true)

Other ini files and values to add

XComLadder.ini

;A configuration File [XComGame.XcomGameState_LadderProgress] +AllowedMissionTypes="DD_SurviveXTurns"

Append this to XComGameData.ini
XComGameData.ini

;A configuration File [XComGame.X2StrategyElement_DefaultResistanceActivities] ;Good things +Activities="ResAct_Mission_Survive_Success" ; Bad things +Activities="ResAct_Mission_Survive_Failed" [ResAct_Mission_Name_Success X2ResistanceActivityTemplate] bAlwaysGood=true bMission=true [ResAct_Mission_Name_Failed X2ResistanceActivityTemplate] bAlwaysGood=false bMission=true

This ini file controls the images seen in the covert action ui in the resistance ring
Be sure to change name to the corresponding value you had put in the uc script and other locations
XComGameBoard.ini

;A configuration File [XComGame.X2StrategyElement_DefaultMissionFlavorText] +Missions=Name [Name X2MissionFlavorTextTemplate] +MissionSources="MissionSource_Name" +ObjectiveTypes="" ;put an objective value defined in missiondefs.ini here [XComGame.X2StrategyElement_DefaultCovertActionNarratives] +CovertActionNarratives="CovertActionNarrative_SurviveXTurns_Skirmishers" +CovertActionNarratives="CovertActionNarrative_SurviveXTurns_Reapers" +CovertActionNarratives="CovertActionNarrative_SurviveXTurns_Templars" [CovertActionNarrative_Name_Skirmishers X2CovertActionNarrativeTemplate] AssociatedFaction="Faction_Skirmishers" ActionImage = "img:///UILibrary_StrategyImages.X2StrategyMap.DarkEvent_Traitor" [CovertActionNarrative_Name_Reapers X2CovertActionNarrativeTemplate] AssociatedFaction="Faction_Reapers" ActionImage = "img:///UILibrary_StrategyImages.X2StrategyMap.DarkEvent_Traitor" [CovertActionNarrative_Name_Templars X2CovertActionNarrativeTemplate] AssociatedFaction="Faction_Templars" ActionImage = "img:///UILibrary_StrategyImages.X2StrategyMap.DarkEvent_Traitor" [CovertAction_Name X2CovertActionTemplate] MinActionHours[0]=120 MinActionHours[1]=120 MinActionHours[2]=120 MinActionHours[3]=192 MaxActionHours[0]=168 MaxActionHours[1]=168 MaxActionHours[2]=168 MaxActionHours[3]=288

Testing and Testing in-game
You can use the TQL to test that the mission and any voicelines, objectives, and popups work as expected. You can also use it to make sure that mission objectives are completed and marked correctly. Once you have confirmed that your mission behaves as expected, as well as tested any cases that could break the mission do not occur, you can upload to steam and test the covert action aspect of the mission.

You can use the two provided scripts below to force the covert action to appear. Remember the ini file has a default force level of set, so if you want it to appear earlier you must change the value to a lower number.

exec function SpawnCovertAction(name TemplateName, optional bool bForce = true, optional name FactionTemplateName = '') { local XComGameStateHistory History; local XComGameState NewGameState; local XComGameState_ResistanceFaction FactionState; local X2StrategyElementTemplateManager StratMgr; local X2CovertActionTemplate ActionTemplate; local array<name> ActionExclusionList; History = `XCOMHISTORY; StratMgr = class'X2StrategyElementTemplateManager'.static.GetStrategyElementTemplateManager(); ActionTemplate = X2CovertActionTemplate(StratMgr.FindStrategyElementTemplate(TemplateName)); if (ActionTemplate == none) { `REDSCREEN("Cannot execute SpawnCovertAction cheat - invalid template name"); return; } // Find first faction foreach History.IterateByClassType(class'XComGameState_ResistanceFaction', FactionState) { if (FactionTemplateName == '' || FactionState.GetMyTemplateName() == FactionTemplateName) { break; } } if (FactionState == none) { class'Helpers'.static.OutputMsg("Cannot execute SpawnCovertAction cheat - invalid faction template name"); return; } // Test if we can even use this covert action if (!bForce && !ActionTemplate.AreActionRewardsAvailable(FactionState, NewGameState)) { class'Helpers'.static.OutputMsg("Covert Action: " $ TemplateName $ " does not meet requirements!"); return; } NewGameState = class'XComGameStateContext_ChangeContainer'.static.CreateChangeState("CHEAT: CreateCovertAction" @ TemplateName); FactionState = XComGameState_ResistanceFaction(NewGameState.ModifyStateObject(FactionState.Class, FactionState.ObjectID)); FactionState.AddCovertAction(NewGameState, ActionTemplate, ActionExclusionList); `XCOMGAME.GameRuleset.SubmitGameState(NewGameState); } exec function ForceRefreshCovertActions() { local XComGameStateHistory History; local XComGameState NewGameState; local XComGameState_ResistanceFaction FactionState; local X2StrategyElementTemplateManager StratMgr; local X2CovertActionTemplate ActionTemplate; local array<name> ActionExclusionList; History = `XCOMHISTORY; NewGameState = class'XComGameStateContext_ChangeContainer'.static.CreateChangeState("CHEAT: ForceRefreshCovertActions"); // Iterate through each faction and refresh their Covert Action list foreach History.IterateByClassType(class'XComGameState_ResistanceFaction', FactionState) { FactionState = XComGameState_ResistanceFaction(NewGameState.ModifyStateObject(FactionState.Class, FactionState.ObjectID)); FactionState.CleanUpFactionCovertActions(NewGameState); FactionState.CreateGoldenPathActions(NewGameState); FactionState.GenerateCovertActions(NewGameState, ActionExclusionList); } `XCOMGAME.GameRuleset.SubmitGameState(NewGameState); class'Helpers'.static.OutputMsg("Covert Actions successfully regenerated!"); }
Conclusion
If you feel something in this guide is too ambiguous or not clear enough, feel free to leave a comment stating what the issue is and how to make it better and I will fix it.

Mods that were used for reference