This page might be out of date. If you are after more recent information and have questions, please don't hesitate to ask in the #phantom-modding channel on BYG Discord.
This document describes the changes to the modding support and adjacent systems of Phantom Brigade.
Changes:
colorHue
field now applies accent color to mod details and mod entry in the listurl
field is now available for primary website. Specifying a link will make the details pane display the clickable link button to the right of mod name.[url=www.mymodwebsite.com][u]My other mods[/u][/url]
. Use this if you need to display more than one link.modIndexLoad
and modIndexPreload
to ModLink to simplify debug logging. The former will contain the index of a given code mod among currently applied mods, while the latter will contain the index in the preload list, similar to what you see during preliminary loading logging or in the mod menu UI.public virtual void OnLoadStart ()
: override if if you are not interested in Harmony patching and would just like to run some code using existing API, e.g. game event utility subscriptionspublic virtual void OnLoadEnd ()
: override if you are after code that doesn't require a Harmony patcher reference but must run after tag mapping, console command and metadata registrationCustomMemoryValue
type not being type hinted, preventing mods from serializing custom pilot data without additional attributes.GameEventUtility.SubscribeToEventGeneral
with a System.Action
callback for general events.GameEventUtility.SubscribeToEventOnObject
with a System.Action<object>
callback for object specific events. You'll need to cast the received object to a specific type to make use of the objects in most contexts. Here are available events and suggested types to cast the received object
into:includesAssetBundles: true
in metadata.yaml.mods.asset-preview-prefab
command in mech customization screen to preview sideloaded prefabs. Specify mod name, then asset bundle name, then prefab name.ItemVisual
prefabs are registered under {assetBundle.name}/{prefab.name}
keys and are available for use in subsystem attachment collections, as seen on screenshot above.[ TODO ]
[ TODO ]
Changes:
localTransformOverride
value with position
and rotation
fields under the activation
field of a primary weapon subsystem to take advantage of this feature:centered: true
in the attachment config, under key
field. This can be useful for 3D models that are offset from their true pivot, such as front parts of weapons.cr.edit-system hardpointKey
(for example, cr.edit-system external_arm_upper
). The first command you need to enter. Selects a subsystem for editing. Grabs a unit and part from the mech customization screen, requires entering a specific hardpoint as an argument. The hardpointKey
argument is necessary because not every visual-embedding subsystem is accessible through the customization screen - a part might be the lowest possible level a player would be able to select. Running the command “pins” the subsystem for further editing, providing context to other console commands in this set.cr.edit-vis-key lookupKey visualKey
(for example, cr.edit-vis-key 01 wpn_vis_set03_back_02
). Adds an attachment with a given lookup key and visual model. If an attachment with a given lookup key already exists in the dictionary, replaces the visual model. Start with this command to register a new model or modify an existing attachment, then modify its placement. The visualKey
argument supports autocomplete - start typing to discover what's available!cr.edit-vis-remove lookupKey
(for example, cr.edit-vis-remove 01
). Remove an attachment with a specific key from a subsystem.cr.edit-vis-remove-all
. Remove all attachments from a subsystem.cr.edit-vis-remove-all-legacy-visuals
. Remove all legacy visuals from a subsystem ("visuals", not “attachments” in the subsystem config). Legacy visuals only define a name and can't be moved, centered, scaled or rotated, so most modded items with attachments might not require them. They might be useful to keep around in some rare cases, e.g. when making a version of an armor with attachments layered on top of legacy visuals.cr.edit-vis-centered lookupKey value
(for example cr.edit-vis-centered 01 false
). Toggles whether a given attachment is placed relative to true pivot (if false
) or relative to visual center (if true
). Attachments added via the console command above default to true
.cr.edit-vis-pos lookupKey position
(for example, cr.edit-vis-pos 01 (2, 0.5, -1.25)
). Sets 3D position of a given attachment.cr.edit-vis-pos lookupKey axis position
(for example, cr.edit-vis-pos 01 z -1.25
). Sets 1D position of a given attachment on a given axis. Useful to avoid retyping the full vector when you want to adjust an axis in isolation.cr.edit-vis-pos-offset lookupKey position
(for example, cr.edit-vis-pos-offset 01 (0.5, 0.5, 0)
). Offsets 3D position of a given attachment. Useful for progressively modifying a position without remembering previous values, e.g. shifting an attachment 20cm up and 10cm right without remembering what preceding values were.cr.edit-vis-pos-offset lookupKey axis position
(for example, cr.edit-vis-pos-offset 01 x 0.5
). Offsets 1D position of a given attachment on a given axis. Similar to the above, but when you only need an offset on an isolated axis.cr.edit-vis-pos-offset-local lookupKey axis position
(for example, cr.edit-vis-pos-offset-local 01 z 2.5
). Offsets 1D position of a given attachment on a given local axis. Very useful for attachments that aren't aligned along any major axes. Allows you to express operations such as “shift a diagonally attached panel outward along its normal” without having to guess 2-3 offsets on separate axes.cr.edit-vis-rot *
. A set of 4 commands similar to the 4 above, but for modifying attachment rotation (in Euler angles such as (-90, 180, 0)
).cr.edit-vis-scale *
. A set of 4 commands similar to the 4 above, but for modifying attachment scale.cr.edit-vis-match lookupKeyModified lookupKeySource
(for example, cr.edit-vis-match 02 01
). Matches position, rotation and scale of a first attachment to position, rotation and scale of a second attachment. Can be useful for pulling pieces together to begin local edits.cr.edit-vis-match-* lookupKeyModified lookupKeySource
(for example, cr.edit-vis-match-pos 02 01
). Matches a specific property (position, rotation, scale) between two attachments. Valid suffixes are -pos
, -rot
, -scale
.cr.edit-vis-copy lookupKeySource lookupKeyCopy
(for example, cr.edit-vis-copy 01_old 02_new
). Creates a new attachment matched to the old attachment in terms of visuals, centering, position, rotation and scale. Very useful as a starting point in complex compositions: copy a certain precisely positioned piece, start modifying its visual, then start offsetting its position locally etc.cr.edit-vis-mirror lookupKey axis
(for example, cr.edit-vis-mirror 01 x
). Mirrors position, rotation and scale of an attachment on a given axis. Experimental, might not work for anything but X axis.cr.edit-vis-mirror-local lookupKey axis
(for example, cr.edit-vis-mirror-local 01 x
). Mirrors rotation and scale of an attachment on a given axis while leaving position unmodified. Experimental, might not work for anything but X axis.cr.edit-vis-dump
. Dumps the description of the currently selected subsystem visuals to console and Windows clipboard. Final step of tweaking attachment visuals - from there, you can paste the generated text into a YAML file in your mod.cr.edit-vis-dump-key
(for example cr.edit-vis-dump-key 01
). Dumps the description of one specific attachment to console. Not intended to be used for pasting into YAML, intended for quick reference and prioritizes readability.cr.edit-vis-save
. Same as the above, but with an attempt to save the subsystem database to game data on top. This is not meant to be a way of implementing mods: we strongly recommend against direct game data modification, since it interferes with analytics, has issues with file permissions for some users and is rolled back by installation integrity checks. However, it can be useful in rare special cases where a modder might want to inspect a modified YAML file implementing the changes.cr.scenario-escape
- allows you to escape the combat encounter even if you're locked into it. stuns the enemy like a smoke charge, but without requiring one. a pattern I frequently get into is testing something in overworld and being attacked by a patrol before I remember to cheat in some smoke charges, which means I can't escape a fight. this solves it.ow.escalation-set
now works in briefing, refreshing a given scenario to an escalation value between 0 and 350. Note that the scenarios are not scaled by that exact 0-350 value and only by escalation level ("wanted" stars in the UI).ow.escalation-set-level
allows you to directly input a wanted level between 0-3, automatically setting precise 0-350 escalation value in the middle of desired range. Automatically resets scenario if in briefing.cm.teleport
- allows you to teleport a selected unit in combat to speed up testing of scenarios that require long movement, testing of attacks at specific distances etc.cr.inv-generate-part
- (might have been implemented right around 1.0 release, not sure if it's entirely new) allows you to verify how a given part is generated at provided 0-3 rating. It prints a full report, which is really handy for debugging part generation issues, confirming full spread of perks a part is eligible to roll etc. The game picks a random choice from all the possibilities for each hardpoint, so inspecting that is a great shortcut to inspecting 50+ generated parts manually.unitScalingThreatBased
(on by default). Threat rating is a value embedded into every overworld blueprint. It is increased by +20 for each escalation level in overworld. Threat rating based combat scaling system scans the generated combat scenario, calculates the threat rating of all encountered squads (unit groups), and activates if there is a decifit. It ranks up squads (from normal to veteran to elite, increasing equipment quality, level offset and unit count, depending on how a unit group is configured). When there are no more squads to rank up and a deficit remains, the system clones a random squad at grade 0. This system makes it easy to reuse the same scenario across entities with a different intended difficulty (e.g. base assault scenario will have different number and quality of units if played at an outpost with TR40 vs a base with TR100). However, this system might be undesirable for a tightly controlled experience with finely tuned squads and per-entity scenarios.unitScalingGradeBased
(off by default). Should not be enabled together with TR scaling. An alternative to TR scaling that simply scales squads (unit groups) in scenarios proportionally to escalation level. Escalation 0: normal squads; 1: 50/50 normal/veteran squads; 2: 50/50 veteran/elite squads; 3: 100% elite squads. More predictable, ensures the player encounters exactly the squads defined in a scenario config. Requires additional scenarios per entity or embedded units defined directly on entities if the intended experience is supposed to differ between two entities pointing to the same scenario (for example, camps, outposts and bases should directly declare their garrisons outside of assault scenario if this is to be enabled, otherwise fights at all 3 entities will look the same).back
. The new socket is not supported on any unit types except mechs.statMultipliers
dictionary on the custom
data block of unit resolvers in unit group configs. See custom_patrol_swarm.yaml
in UnitGroups database, where a large number of tanks is balanced by reducing their HP and barrier stats.cm.set-unit-stat-multiplier
console command. Enables quick testing of altered unit stats, both for creating custom
data blocks and for testing possible permanent equipment balance changes.generic_assault
scenario, modify base
, camp
and outpost
overworld entity configs to feature different embedded squads. See squad_invader_patrol_*
entities for examples.scenarioUnits
on Overworld/Blueprints configs.custom
data block of spawned units to movement actions, allowing reinforcements in custom scenarios to arrive over ground and not from the air.AgentBehaviorDestination
), new targeted function allowing its modification (CombatUnitDestinationChange
) and 2 new AI behaviors making use of it (DefensiveDestination
and DumbDestination
). Can be used to make AI units navigate to a specific point on the map based on global coords, map data or objectives, vs. circling player units as seen under standard behavior.major.minor
format (1.1
). Fixed min/max mod version being accepted in any other format than major.minor
or major.minor.patch
.Changes:
ModManager
class to make patching mod loading code easier. Per-mod operations are now split into a separate method, so it is possible to affect mod loading right from within the very same loading pass (in contrast to old implementation where patching the loading method couldn't have an effect on subsequent mods until next load). See the LoadMod
method separated from LoadMods
.*o7
taking place of actual values.!MyFunctionName
tags in configs and enable the deserialization process to connect a tag to your new class, you can now modify the tag mapping collection. The collection can be retrieved via UtilitiesYAML.GetTagMappings
and extended like so: tagMappings.Add ("!MyFunctionName", typeof(MyFunctionName))
.EntitasUtilities.InsertSystemInFeature<T>
method to make registering new Entitas systems easier (e.g. InsertSystemInFeature<EquipmentSystems> (new MySystem)
). Added `GameControllerState.GetSystems<T>` allowing fetching of a GameController state feature group, enabling the above and other similar usages. Added GameController.GetInstance
to enable the above.activation
/ timing
field.timeFrom
and timeTo
fields are 0-1 floats expressing normalized time projectiles are fired in. E.g. timeFrom
of 0.5 would mean a weapon only starts firing past 50% of the attack duration.exponent
value is used to raise normalized projectile firing time to a given power. Consider a weapon with 5 projectiles. By default, they would have normalized firing times of 0, 0.25, 0.5, 0.75 and 1. With exponent: 2
, the firing times would turn to 0, 0.125, 0.25, 0.56 and 1 - the weapon would burst more projectiles early, and start shooting more slowly towards the end of the action. With exponent: 0.5
, the firing times would turn to 0, 0.5, 0.7, 0.86, 1 - the weapon would start slow and speed up towards the end.item_info_rating_{?}
localization keys to override or provide new rating names. For example, modding item_info_rating_2
would modify the “Uncommon” label and modding item_info_rating_4
would register a new name for a rating above Rare.equipmentRatingSuffixes
dictionary under ui.yaml to override or provide new rating suffixes such as +++
.attachments
collection under subsystem configs, as depicted above. You can declare any number of attachments.visuals
collection is left in for backwards compatibility. Using it is equivalent to declaring an attachment at position 0,0,0 with rotation 0,0,0 and scale 1,1,1. The keys for the visuals and attachments are the same - please refer to the reference page.beam.impact
asset field similar to the impact
field that's available under projectile
data block.factionUsed
field from all pooled asset data blocks. It was redundant when presence of a value on keyEnemy
field already indicates the asset needs to vary by faction.*Enemy
field is left empty, the game assumes that assets of every faction should be colored using the value of base field. If base field is null as well, no color changes are applied.f
inside. It is used on effects that consist of arbitrary number of colors, typically particle based effects, where directly defining a color or two is not nearly enough information to drive the full content of an effect. The change from the hue offset is akin to dragging a hue slider in software like Photoshop, with the 360 degree hue color wheel compressed into 0-1 range. If an original effect consists of a red flames and orange sparks, hue offset with f: 0.1
will turn flames orange and sparks yellow (shift everything forward by 10% on the color wheel). f: 0.5
will push the colors further forward, e.g. turn red to sky blue.hueOffset
and hueOffsetEnemy
fields on the following asset data blocks:activation.local
(muzzle attached firing effect) and activation.root
(unit attached firing effect)projectile.impact
(hit effect on projectile collision) and projectile.deactivation
(effect on projectile expiring from distance or time)beam.impact
(hit effect on beam collision)Color
properties inside, colorFrom
and colorTo
. It is suited to VFX assets that are precisely controlled by a pair of colors. The pairs are available through colorOverride
and colorOverrideEnemy
fields on the following asset data blocks:projectile.body
(asset attached to projectiles)beam.body
(asset attached to beams)projectile.body.key: fx_projectile_standard_blue
, leave keyEnemy
emptyprojectile.body.colorOverride
to green and light greenprojectile.body.colorOverrideEnemy
to purple and light purpleactivation.local.key: fx_muzzle_mg_heavy_blue
, leave keyEnemy
emptyactivation.local.hueOffset.f: -0.2
(shift blue towards green) and hueOffsetEnemy.f: 0.3
(shift blue to purple)ISubsystemFunctionGeneral
.ISubsystemFunctionTargeted
.ISubsystemFunctionAction
.partEventsEnabled: true
in simulation.yamlfunctions
list in the Subsystems database configs. The field is a list to allow multiple hierarchy levels to declare their own functions: for example, a parent for a new weapon type might declare a special function that runs for every model in that type, and each model might add yet another function specific to its design.functions
list has 3 fields: general
, targeted
and action
, each of them a list of function invocations of a given type. See the first 3 bullet points for details on the role of the 3 types.general
, targeted
or action
lists has a contexts
list field, which specifies exact events a given set of functions can be invoked under. For instance, you might want a certain special effect to spawn on part destruction, but not on part activation, so you'll list on_part_destruction
under contexts
to achieve that. Full list:on_part_activation
- called per subsystem within a part that is referenced by an equipment action, when an action startson_part_destruction
- called per subsystem within a part that is being destroyedon_wpn_fired
- called per subsystem within a weapon part firing a projectile (the unit being targeted will be passed in as an optional argument)on_wpn_hit
- called per subsystem within a weapon part responsible for a projectile that just hit another unit (the unit being hit will be passed in as an optional argument)on_wpn_part_destruction
- called per subsystem within a weapon part responsible for destroying another unit (the unit being destroyed will be passed in as an optional argument)on_action_start
- called per every subsystem in the unit when any action beginson_action_end
- called per every subsystem in the unit when any action endsfunctions
fields in contexts such as scenario or overworld event steps: each entry is preceded by a tag allowing the serialization system to learn which type it is dealing with. The built-in examples are limited to a few classes such as SubsystemFunctionLog
, SubsystemSpawnEffect
and SubsystemRetargetProjectiles
, depicted below. These are provided as examples: the overall system is intended to be used with a code mod declaring additional freeform functions.targetMode
value 3
on stat data blocks in subsystems. As a refresher: 0
is non-targeted (value gets added to stat sum in an item), 1
is additive targeted (add multiple of non-modified stat of a targeted item to stat sum in that item), 2
is multiplicative targeted (multiply the final stat of a targeted item by a given amount). New mode 3
is a targeted override: the declared value will entirely replace the final value of a targeted item. This is a fairly niche targeting mode, mostly useful for cases like setting shot count to exactly 1 or 2 in an exotic weapon perk, overriding mass for special NPCs etc.minPerPartLimit, minPerPart, maxPerPartLimit, maxPerPar, minPerUnitLimit, minPerUnit, maxPerUnitLimit, maxPerUnit, levelUsed, levelIncrease, multiplierUsed, multiplier
.minInPart, minInUnit, maxInPart, maxInUnit, increasePerLevel, scale
minFromStat, maxFromStat
. The former is used in wpn_scatter_angle_moving
, like so: minFromStat: wpn_scatter_angle
.DataMultiLinker.GetDataList
method for iterating over databases without overhead of sorted dictionary iterators.res_concussion
values between 0 and 1 determine how much pilot damage is mitigated (e.g. unit total of 0.2 means 20% of pilot damage is skipped)res_heat
does the same for incoming heat from incendiary weapon hitsres_stagger
does the same for hits from destabilizing weaponsscenarioUnits
. It is a list, with each entry containing three fields: step
, tags
and unitGroups
.unitGroups
field is identical to the unit field under combat scenario steps or state reactions and allows defining squads (unit groups) that appear in combat, along with customizations and restrictions (such as minimum distance from the player). Embedded unit group type is not supported, but linked (directly referenced unit group DB keys) or filtered (tag filter for unit groups) are supported.tags
is a set of tags indicating where to use the defined units. most cases would use a single entry - start
under this field, indicating the units should appear on the field immediately, as part of the starting step of any combat scenario (all built-in scenarios are already tagged appropriately), but it's possible to define completely arbitrary tags and filter for them. New tags
field has been added to steps and state reaction data blocks to enable these checks.step
is a bool that determines whether a given unit description applies to scenario steps or scenario states. In most cases, where this feature would be used to inject units into the first step of a scenario, this would be set to true.ui.view-list
- lists all registered UI screens (views), printing GameObject path and exact type name. Type name is used fully or partially in the next 2 commands.ui.view-enter
- attempts to enter a view with a type name matching the provided filter. Partially entering type names is supported, e.g. ui.view-enter CIViewInternalUITools
and ui.view-enter uitools
will both work.ui.view-exit
- attempts to exit a view with a type name matching the provided filter.tex.print-groups
- prints all supported texture groups, folder paths, file requirements and loaded textures (if any). Can be constrained with optional group name filter argument, e.g. tex.print-groups overworld
will print only 2 overworld-related groups.tex.load
- re-loads the texture database. Can be useful for quickly testing images via direct editing of the StreamingAssets install folder before attempting to create a mod.ui.view-enter uitools
, as seen in the example above.string icon
fields in different configs which require entering a valid sprite name), confirming internal data such as sprite resolution, verifying whether modded sprites were correctly loaded, debugging issues with sprite metadata etc.UILabel
component (under CIViewLoader.ins
anchor objects), or use an existing UILabel
component (e.g. from one of CIView*.ins
objects). Ensure the same object has a Collider
component attached and is on the UI
layer (5).myLabel.RegisterURLCallback ("myCallbackKey", new UICallback (MyVoidMethod));
to register an alias and associated callback void method.myLabel.text
to insert the alias into text, allowing the registered callback to trigger on clicks. For example, myLabel.text = “This is static text. [url=myCallbackKey]This is clickable text.[/url]”
.CIButton
components, it is a very easy way to set up a basic interactive UI, especially when modding in a new UI view from code. UICallback
class supports many function signatures and fits most use cases needed in UIs. Check the CIViewInternalUITools
class (the UI debug view described above) for an example of using this: the nearest sprite name list on the left is a single label with per-line click callbacks.Fixes:
PhantomBrigade.Data
namespace not being moddable through the config editing system.
The following features are under investigation and might require some time to realize. We're hoping they can be delivered but can't commit to a specific window for the time being.