An improved XCOM 2 mod build system. The following (in no praticular order) are its features/improvements over the default one:
ModShaderCache
and invoking the shader precompiler only when the mod content files have changedinclude
s and without messing with your SrcOrig
)Foreword: the build system was designed to be flexible in how you want to set it up. This section describes the most common/basic setup that should work for 95% of mods out there. If you want to customize it, read the next section
First, create a .scripts
folder in the root of your mod project (next to the .XCOM_sln
file) - from now on referred
to as [modRoot]
. The next step depends on whether you are using git or not. Git is preferable but the build system
will work just fine without it.
Open a command line prompt (cmd or powershell, does not matter) in the [modRoot]
. Ensure that
your working tree is clean and run the following command:
git subtree add --prefix .scripts/X2ModBuildCommon https://github.com/X2CommunityCore/X2ModBuildCommon v1.2.1 --squash
Download the source code of this repository from the latest release on the Releases page.
Unzip it and place so that build_common.ps1
resides at [modRoot]\.scripts\X2ModBuildCommon\build_common.ps1
.
BuildCache
The build system will create a [modRoot]\BuildCache
folder which is used for various file-based operations (such
as recompiling the ModShaderCache
only when mod's content has changed). This folder is fully managed by the build
system and normally you should never open it. It is also safe to delete at any time (e.g. if you want to
force a full rebuild).
As such, this folder is not meant to be shared with other developers working on the project or stored in backups/previous versions (e.g. when using a VCS) - this can lead to incorrect behaviour.
If you are using git, you should add it (BuildCache/
) to your .gitignore
Create [modRoot]\.scripts\build.ps1
with the following content:
Param(
[string] $srcDirectory, # the path that contains your mod's .XCOM_sln
[string] $sdkPath, # the path to your SDK installation ending in "XCOM 2 War of the Chosen SDK"
[string] $gamePath, # the path to your XCOM 2 installation ending in "XCOM2-WaroftheChosen"
[string] $config # build configuration
)
$ScriptDirectory = Split-Path $MyInvocation.MyCommand.Path
$common = Join-Path -Path $ScriptDirectory "X2ModBuildCommon\build_common.ps1"
Write-Host "Sourcing $common"
. ($common)
$builder = [BuildProject]::new("YourProjectName", $srcDirectory, $sdkPath, $gamePath)
switch ($config)
{
"debug" {
$builder.EnableDebug()
}
"default" {
# Nothing special
}
"" { ThrowFailure "Missing build configuration" }
default { ThrowFailure "Unknown build configuration $config" }
}
$builder.InvokeBuild()
Replace YourProjectName
with the mod project name (e.g. the name of your .XCOM_sln
file without the extension).
If you're transitioning an existing mod to X2ModBuildCommon, this advice might come too late, but we recommend that
the project name contain only ASCII alphabetic characters, numbers and underscores (matching the regular expression ^[A-Za-z][A-Za-z0-9_]*$
).
The ModBuddy project generator lets you create projects with a large variety of characters that will break the ModBuddy
build already (like brackets and dashes), but spaces and semicolons are allowed and work fine with the Firaxis ModBuddy plugin.
X2ModBuildCommon
will do its best to support project names with spaces, but it's historically been a common source of bugs
and you may run into fewer of them if you keep your mod name simple.
At this point your mod is actually ready for building but invoking the powershell script with all the arguments each time manually is not convinient. Instead, we would like it to be invoked automatically when we press the build button in our IDE
Close Modbuddy (or at least the solution) if you have it open. Open your .x2proj
(in something like notepad++) and find the follwing line:
<Import Project="$(MSBuildLocalExtensionPath)\XCOM2.targets" />
Replace it with following:
<PropertyGroup>
<SolutionRoot>$(MSBuildProjectDirectory)\..\</SolutionRoot>
<ScriptsDir>$(SolutionRoot).scripts\</ScriptsDir>
<BuildCommonRoot>$(ScriptsDir)X2ModBuildCommon\</BuildCommonRoot>
</PropertyGroup>
<Import Project="$(BuildCommonRoot)XCOM2.targets" />
Note that the build tool does not care about most of the .x2proj
file and will
copy and compile files not referenced by the project file without issuing warnings.
Consider using a tool like Xymanek/X2ProjectGenerator
to automatically ensure the file list in ModBuddy accurately lists the files part of the project.
FIXME(#1): Rename variables to remove HL references?
First, you need to tell Visual Studio code where to find the game and SDK (similar to the first-time ModBuddy setup). To do that, open the "Settings (JSON)" file by using the "Ctrl+Shift+P" shortcut and running "Preferences: Open Settings (JSON)" or by clicking "File->Preferences->Settings" and clicking the "Open Settings (JSON)" button on the tab bar. Add the following two entries, adjusting paths as necessary.
"xcom.highlander.sdkroot": "d:\\Steam\\SteamApps\\common\\XCOM 2 War of the Chosen SDK",
"xcom.highlander.gameroot": "d:\\Steam\\SteamApps\\common\\XCOM 2\\XCom2-WarOfTheChosen"
VS Code may tell you that the configuration settings are unknown. This is acceptable and can be ignored.
Next up, you have to tell VS code about your build tasks. Create a folder .vscode
next to the .scripts
folder,
and within it create a tasks.json
file with the following content (replacing MY_MOD_NAME
with the mod project
name in the "Clean" task):
{
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"type": "shell",
"command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\build.ps1' -srcDirectory '${workspaceRoot}' -sdkPath '${config:xcom.highlander.sdkroot}' -gamePath '${config:xcom.highlander.gameroot}' -config 'default'",
"group": "build",
"problemMatcher": []
},
{
"label": "Build debug",
"type": "shell",
"command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\build.ps1' -srcDirectory '${workspaceRoot}' -sdkPath '${config:xcom.highlander.sdkroot}' -gamePath '${config:xcom.highlander.gameroot}' -config 'debug'",
"group": "build",
"problemMatcher": []
},
{
"label": "Clean",
"type": "shell",
"command": "powershell.exe –NonInteractive –ExecutionPolicy Unrestricted -file '${workspaceRoot}\\.scripts\\clean.ps1' -modName 'MY_MOD_NAME' -srcDirectory '${workspaceRoot}' -sdkPath '${config:xcom.highlander.sdkroot}' -gamePath '${config:xcom.highlander.gameroot}'",
"group": "build",
"problemMatcher": []
},
{
"label": "Full rebuild",
"dependsOrder": "sequence",
"dependsOn": ["Clean", "Build"]
}
]
}
Note that the -config 'debug'
or -config 'default'
build configurations correspond to
the build configurations in the build.ps1
entry point created earlier. You can easily add
existing build tasks with custom configurations by modifying build.ps1
and configuring the
$builder
(see just below!)
FIXME(microsoft/vscode#24865): Add problem matchers when they can be shared between tasks.
You can now successfully build your mod from your IDE using X2ModBuildCommon. Keep reading on to find about what you can configure.
The build system is desinged to be version-pinned against your mod - you can continue using the old version as long as it suits your needs, even if a new one is released. If you would like to get the new features/improvements/bugfixes of the new version, the update procedure is simple.
If you don't use git, simply download the new version and overwrite the old files inside the X2ModBuildCommon
folder.
If you use git, run the same command as before, replacing add
with pull
:
git subtree pull --prefix .scripts/X2ModBuildCommon https://github.com/X2CommunityCore/X2ModBuildCommon v1.2.1 --squash
All the following examples are modifications that could be made to your build.ps1
.
FIXME:
ThrowFailure
vsFailureMessage
?
Throw a failure. Example usage:
switch ($config) {
# ...
"" { ThrowFailure "Missing build configuration" }
}
Override the workshop ID from the x2proj file. Example usage:
# make sure beta builds are never uploaded to the stable workshop page
if ($config -eq "stable") {
$builder.SetWorkshopID(1234567890)
}
else {
$builder.SetWorkshopID(6789012345)
}
Pass the -final_release
flag to the compiler for base-game script packages and the Highlander cooker.
Can only be used for Highlander-style mods that modify native packages. Example usage:
switch ($config) {
# ...
"final_release" {
$builder.EnableFinalRelease()
}
"stable" {
$builder.EnableFinalRelease()
}
}
Pass the -debug
flag to all script compiler invocations. Incompatible with EnableFinalRelease
, and will skip
Highlander cooking process (accordingly you have to use -noseekfreeloading
when launching the game). Example usage:
switch ($config) {
# ...
"debug" {
$builder.EnableDebug()
}
}
Add a callback to be executed after all script sources have been added to Src
but before the compiler is run.
Example usage:
# Checks if a certain automatically generated file actually compiles, but only with the "compiletest" configuration
if ($compiletest) {
$builder.AddPreMakeHook({
Write-Host "Including CHL_Event_Compiletest"
# n.b. this copies from the `target` directory where it is generated into, see tasks.json
Copy-Item "..\target\CHL_Event_Compiletest.uc" "$sdkPath\Development\Src\X2WOTCCommunityHighlander\Classes\" -Force -WarningAction SilentlyContinue
})
}
The Highlander also uses it to embed the current git commit hash in some source files.
Add dependencies' source files to Src
. This removes the step where mods whose sources you want to have available
have to be copied to SrcOrig
. Example usage (from Covert Infiltration):
$builder.IncludeSrc("$srcDirectory\X2WOTCCommunityHighlander\X2WOTCCommunityHighlander\Src")
$builder.IncludeSrc("$srcDirectory\X2WOTCCommunityHighlander\Components\DLC2CommunityHighlander\DLC2CommunityHighlander\Src")
$builder.IncludeSrc("$srcDirectory\SquadSelectAnyTime\SquadSelectAtAnyTime\Src")
Deletes certain built mods from SDK/XComGame/Mods
. Usually necessary for dependencies since their script compiler configuration
files can cause the script compiler to choke. Covert Infiltration does this:
$builder.AddToClean("SquadSelectAtAnyTime")
You can provide a "content options" file that will determine some additional content-related steps. This file should be checked
in to your VSC (e.g. tracked by git) and must reside next to your .x2proj
(note that the file will not be included in the
final built mod). If using Modbuddy, you can add the file to the project for easier editing.
Assuming the file is named ContentOptions.json
:
$builder.SetContentOptionsJsonFilename("ContentOptions.json")
Four options are available: missingUncooked
, sfStandalone
, sfMaps
, sfCollectionMaps
. Omitting an option (or the file entirely)
is treated the same as setting it to an empty array
In case your mod depends on some assets that were not shipped in a seek free package, you can automatically include it with your mod. Example from Covert Infiltration:
{
"missingUncooked": [
"CIN_TroopTransport.upk",
"PCP_Archetypes_XPACK.upk"
]
}
IMPORTANT: you need to be on the full_content
branch of the SDK for this to work.
The rest of the options are for the mod assets cooking. Because it is such a complex process, the package and map configuration is described in a separate file. See Asset Cooking for details.
This isn't a configuration option, but mods can create an extra_globals.uci
file in the
Src
folder to have the build tool append its contents to Globals.uci
. This allows mods
to use custom macros.
Moreover, the extra_globals.uci
files of any dependencies added via IncludeSrc
will be merged into Globals.uci
too. This allows dependency mods to safely use custom macros
without causing compilation problems for dependent mods.
Any files in the Localization
folder will have its encoding rewritten from UTF-8 to UTF-16. This allows tracking
localization files in the git-compatible UTF-8 text encoding even though the game only supports ASCII and UTF-16.
You can customize your .x2proj
using the following properties:
Property | Default value | Notes
-------- | ------------- | -----
SolutionRoot
| None (required) | The path to folder which houses your .XCOM_sln
file
ScriptsDir
| None | Required if you don't set BuildEntryPs1
. Ignored otherwise
BuildCommonRoot
| None (required) | The path to folder which houses the InvokePowershellBuild.cs
file
BuildEntryFileName
| build.ps1
| Required if you don't set BuildEntryPs1
. Ignored otherwise
BuildEntryPs1
| $(ScriptsDir)$(BuildEntryFileName)
|
BuildEntryConfig
| default
or debug
| Will be passed to your build entrypoint. Default is derived from $(Configuration)
(build configuration dropdown)
Hint: if you want to add other build configurations, you can let the default
and debug
ones be handeled
by the provided XCOM2.targets
. Example from CHL:
<PropertyGroup>
<SolutionRoot>$(MSBuildProjectDirectory)\..\</SolutionRoot>
<ScriptsDir>$(SolutionRoot).scripts\</ScriptsDir>
<BuildCommonRoot>$(ScriptsDir)X2ModBuildCommon\</BuildCommonRoot>
<!-- Default and debug are handeled by the .targets automatically -->
<BuildEntryConfig Condition=" '$(Configuration)' == 'Final release' ">final_release</BuildEntryConfig>
<BuildEntryConfig Condition=" '$(Configuration)' == 'Compiletest' ">compiletest</BuildEntryConfig>
<BuildEntryConfig Condition=" '$(Configuration)' == 'Workshop stable version' ">stable</BuildEntryConfig>
</PropertyGroup>
Note that you can always check the issue tracker: https://github.com/X2CommunityCore/X2ModBuildCommon/issues?q=is%3Aissue+is%3Aopen+label%3Abug
If you delete content files (e.g. moved to a different mod or just completely removed) the current caching logic (e.g. for the
shader cache) might not recognize it. As such, it's recommended that you simply delete the BuildCache
folder in such cases.