Basic XML modding
Contents
Brief Explanation Of XML For Non-Coders
XML (Extensible Markup Language) used to create ships, structures, tweak AI difficulty and more.
XML syntax requires all tags to be closed (unlike HTML)
all tags look something like this <tagName: stuff here if need be>
.
with every single tag you create or 'open' it must then be closed. tags can be closed in one of two ways. either like this: <tag1 />
or like this .<tag1> </tag1>
. this allows tags to be nested with tags like so:
<tag1> <tag2/> <tag1>
.
There is one tag that is required in XML modding at the start yet does not follow this rule and is one of two exceptions to this rule (the other being commenting). that tag is
<?xml version="1.0" encoding="utf-8"?>
that tag is used to tell the XML reader (parser) that this xml version is version 1.0 and the character set used (utf-8)
The most common mistake when XML modding will probably be forgetting to close a tag somewhere in your mod and there is very little to tell you where the offending tag is. (if you are XML modding and a big error message shows up it's probably due to forgetting to close a tag). (also please note there are some weird things with mod loader currently and partial records (overwriting) through XML mods so take care when using partial records).
Where To Put Your Mod (And An Example)
for creating a new starting fleet create an XML file. the name doesn't matter just make sure it's an XML file. and put it in: AI War 2\GameData\Configuration\FleetDesignTemplate or put it in a new XML mod folder and then FleetDesignTampate like: AI War 2\XMLMods\anyNameYouWant\FleetDesignTemplate
now in your new XML file for creating a new starting fleet you need to put in these tags in this fashion
<?xml version="1.0" encoding="utf-8"?> <root> </root>
These two tags will always be within any XML file used for general AIW2 modding. Within tags the XML parser can read variables used for various reason. for starting fleets we'll need another tag place within the root tag. this tag will contain variables within it. the tag will follow a pattern which will be:
<fleet_design name="InternalName" display_name="Name on starting menu" description="Description within selection menu" design_logic="InitialPlayerFleet" weight="100" include_full_cap_of_each_type="true" append_each_ship_description_to_main_description="true" don't touch the last four variables variables (please). btw weight is used more for the non- initial player fleets. (it determines it's likehood to be chosen) > </fleet_design>
within these two tags you will put another tag (or a few). the tag will be
<ship_membership name="Entityname" ship_cap_group="Centerpiece" weight="100" min="1" max="1"/> the 'variables' are the Name (please note this is an entity name and I will tell you how to obtain this name later). the weight can be left at 100 and the min and max should be the same. ship_cap_group should be left as is and is ONLY used for the fleet centerpiece (that needs a cap of one only).
the entity name can be found within AI War 2\GameData\Configuration\GameEntity (within the XML files). open any file you want (but preferably KDL_Ships_FleetShips) for finding entities to add to the squad. the entityname 9refered to as name within the XML will be found in
<entity name="VWing" The entity name. visuals="assets/_finalgamemeshes/fleetships/fighter/fighterold.prefab" icon_name="Official/Fighter" voice_group="Fighter" category="Ship" is_strikecraft="true" size_scale="0.9" visuals_scale_multiplier="6" collision_priority="100" display_name="V-Wing" description="Inexpensive and short ranged, adept at shielding allies and chasing down fleeing hostiles." starting_mark_level="Mark1" tech_upgrades_that_benefit_me="Generalist,Light" cost_for_ai_to_purchase="28" hull_points="2700" shield_points="1300" speed="AboveAverage1" metal_cost="1800" energy_consumption="400" armor_mm="50" albedo="0.3" engine_gx="8" mass_tx="0.21" ship_or_structure_explosion_sfx="ShipSmall_Explosion" ship_or_structure_explosion_if_on_other_planet_sfx="ShipLostOnOtherPlanet_Explosion" exp_to_grant_on_death="20" priority_as_ai_target="NormalFleetship" priority_as_frd_target="NormalFleetship" priority_to_protect="Expendable" > </entity>
Thus the your fleet will look like this:
<fleet_design name="ClassicStartingFleet" display_name="Classic Fleet" description="This classic mix isn't good at any one particular thing, but is well-rounded against any foe. The forcefields just make things even better." design_logic="InitialPlayerFleet" weight="100" include_full_cap_of_each_type="true" append_each_ship_description_to_main_description="true" > <ship_membership name="TransportFlagship_Starter" ship_cap_group="Centerpiece" weight="100" min="1" max="1"/> <ship_membership name="VWing" ship_cap_group="Strike" weight="100" min="40" max="40"/> <ship_membership name="FusionBomber" ship_cap_group="Strike" weight="100" min="40" max="40"/> <ship_membership name="ConcussionCorvette" ship_cap_group="Strike" weight="100" min="40" max="40"/> <ship_membership name="ForcefieldFrigate" ship_cap_group="Frigate" weight="100" min="1" max="1"/> </fleet_design>
yes this is a base game fleet. (each fleet_membership tag represents a single unit's cap and type for the fleet). your final file should look like this:
<?xml version="1.0" encoding="utf-8"?> <root> <fleet_design name="ClassicStartingFleet" display_name="Classic Fleet" description="This classic mix isn't good at any one particular thing, but is well-rounded against any foe. The forcefields just make things even better." design_logic="InitialPlayerFleet" weight="100" include_full_cap_of_each_type="true" append_each_ship_description_to_main_description="true" > <ship_membership name="TransportFlagship_Starter" ship_cap_group="Centerpiece" weight="100" min="1" max="1"/> <ship_membership name="VWing" ship_cap_group="Strike" weight="100" min="40" max="40"/> <ship_membership name="FusionBomber" ship_cap_group="Strike" weight="100" min="40" max="40"/> <ship_membership name="ConcussionCorvette" ship_cap_group="Strike" weight="100" min="40" max="40"/> <ship_membership name="ForcefieldFrigate" ship_cap_group="Frigate" weight="100" min="1" max="1"/> </fleet_design> </root>
again please note this is a base game fleet.
Modding Existing Ships Or Similar
If you want to edit an existing ship, difficulty level, or whatever else, then you want to make a new xml file of your own inside your mod folder, matching the structure of the existing xml folder of whatever you want to mod. Note that your filename doesn't have to have any bearing on the original filename.
You now have two options on how to proceed:
Alter The Existing Item (is_partial_record)
Here's an example of how our second expansion actually "mods" the central ExternalConstants by having a file with these contents:
<?xml version="1.0" encoding="utf-8"?> <root is_partial_record="true" custom_int_zenithtrader_budgetpersecondforotherfactions="100" custom_FInt_zenithtrader_aibudgetmultiplierlow="0.5" custom_FInt_zenithtrader_aibudgetmultipliermedium="1.0" custom_FInt_zenithtrader_aibudgetmultiplierhigh="1.3" custom_int_zenithtrader_structurecost="270000" custom_bool_zenithtrader_spawnonplayerhomeplanet="false" > </root>
Note that basically is_partial_record="true" on any xml node (ship, settings, whatever) will let you start adding either new nodes (as in this expansion example), or overwriting prior entries (change the name or difficulty of a ship, make a ship faster, etc).
Copy The Existing Item And Make Changes (copy_from)
Here's an example from within the main base game itself:
<entity name="AstroTrainTankStyleHigh" cannot_be_stacked="true" copy_from="AstroTrainTankStyle" tags="AstroTrain,AstroTrainHigh,ShowsOnNormalDisplayMode" hull_points="3500000" shield_points="2500000" speed="BelowAverage2" > <system name="FusionBomb" display_name="Demolisher Fusion Bomb" category="Weapon" firing_timing="OnlyInRange" damage_per_shot="15000" range="Normal5" shot_speed="Slow" rate_of_fire="High" shots_per_salvo="20" fires_salvos_sequentially="false" shot_type_data="FusionBomb" base_percent_damage_bypasses_personal_shields="1" only_targets_static_units="true" > </system> </entity>
Essentially there are a lot of cases where we want to take some entity, in this case AstroTrainTankStyle, and we want to then make some changes to make a new variant (in this case called AstroTrainTankStyleHigh). In this case we added some guns, changed around hull points, shield, and speed, but otherwise left all the basic things from the underlying entity.
It's good practice to leave all of the fields in place except the ones you actually want to change, so that your variant will continue to evolve with the base unit if it gets some changes unrelated to the overrrides you have set up?
But hey, what about that "tags" field? That's being spelled out here, and identical to the original. That brings us to...
Attributes Available On Every Node
First of all, please note that there are some "minor child nodes" that give data to their parent nodes. These are things like metal_flows on entities, and things like that. All of those "minor child nodes" do NOT support these attributes and are not parsed as major nodes. Normally a node like this has a 1:1 correlation to an ArcenDynamicTableRow entry in an ArcenDynamicTable. If that's not the case, then it isn't supporting all those things.
The one strange sub-node that is both a child sub node but ALSO a major node is systems on entities.
- name
(string)
name="BobsShip"
, required- This is always mapped to InternalName in the game.
- This often should basically be just basic ASCII characters and underscores and punctuation. Maybe avoid spaces? This is not meant to ever be seen by a player.
- For the few types of tables where their internal names are serialized "by index" for more-efficient later reading, we use our "condensed string" format, which will throw errors if you use diacritic marks or other extended unicode letters.
- This is the most key field on absolutely every node, and is considered a unique primary key (aka, two nodes of the same type from two different mods will error if they are named the same, so try to make your mod names a bit unique).
- For systems, these do not need to be unique. Rather, this is the one place in the codebase where it combines the name of the parent with the name of the child in order to create a unique name.
- If you give the name "None" (case sensitive), then this will fill the special NoneRow field on the table, which is used in some cases where players or coders/modders want to assign something to be blank.
- display_name
(string)
, optional, default blankdisplay_name="Bob's Ship"
- This is used to show to the player, if that's relevant. For some back-end types of nodes, players never see those in general (like AI budgets or something). But for others, like the name of player ships, this is where you'd write out what you want them to see. You can use fuller unicode here if you want, but bear in mind our fonts often can't display extended unicode characters and so those would just be blanks.
- is_default
(bool)
, optional, default falseis_default="true"
- If this is present and true, then this is considered the default row for the table to use if it can't find a value for some reason.
- This is very very rarely relevant. If no row is marked as default, then the code has a null row. This is okay almost universally.
- is_hidden
(bool)
, optional, default falseis_hidden="true"
- If this is present and true, then this is a hint to the UI to not show this row. For instance in things like dropdown lists for AI types, or whatever else.
- The trick here is that each individual UI, or part of the game that otherwise is relevant, needs to interpret this on its own. By default it does nothing, but consumers of this data are expected to handle hidden rows if they care about it.
- In some cases this variable is used quite a bit by the game, but in other cases we just don't care about the concept of being hidden. What is a hidden ship, for instance? It isn't used. But for certain build menus, those need to exist but also be hidden from the UI, and we do use that there.
- ready
(bool)
, optional, default trueready="false"
- If this is present and false, then the rest of this node will entirely be ignored. This is useful for some xml you were working on and wanted to include in your file, but not actually have anyone else see yet.
- is_partial_record
(bool)
, optional, default falseis_partial_record="true"
- If this is present and true, then this is considered only an addendum to the original node that was specified.
- copy_from
(string)
, optionalcopy_from="BobsShipBase"
- This should be pointing to the InternalName of another node of the same type of node that this is on.
- This copies over everything from the original, and then whatever attributes you actually specify get added on top of that (or replace their values). Aka this is how to make a variant.
- exclude_children_from_copy
(string list)
, optionalexclude_children_from_copy="metal_flow,system"
- This has its own section below, but is only relevant when copy_from is used.
- sort_order
(int)
, optional, default -1sort_order="100"
- The Rows list on the ArcenDynamicTable will be sorted automatically by anything that is greater than or equal zero, ascending (0, 1, 2, 3) AFTER anything that is less than zero (-3, -2, -1).
How Fields Act During copy_from Or is_partial_record
In general, single-value fields (like damage_per_shot) can either be required or optional.
For a system, damage_per_shot is actually required IF category="Weapon", but is an error if the category is anything else.
But even more than that, fields are very very rarely required if is_partial_record="true". In these cases, we pretty much assume that "if it was there before, and you didn't say anything about it, it will stay the same."
That is, generally speaking, also true for records where copy_from="something".
There's a list of exceptions below, most of them lists of items in one attribute, but what can you do if you don't like the way a field is behaving and want it to work differently?
Changing How Lists Are Read
- The game supports a few new general-purpose attribute variants:
- Essentially, if there is an attribute that is a list, and it is called bacon for whatever reason (this could be a mod or a main game attribute, after all), you can append these things after bacon to change how the xml is read in for that specific xml node.
- So if you have bacon="5,6,6", and normally that is read in as Uniqueness Required in the code, then you're powerless as a modder to get 5,6,6 as your results, instead only getting 5,6.
- But NOW you can add in bacon_uniqueness in order to override that to the value you want, as one example. This works with any attribute that is a list type, whether that is defined in the core game or in code you added for a mod.
- The valid new attribute variants are:
- [attribute]_uniqueness, with the values: Unenforced and Required. These overwrite whatever was passed into the FillList method in code.
- [attribute]_if_present, with the values: ReplaceExistingList and AddToExistingList. These overwrite whatever was passed into the FillList method in code.
- [attribute]_clear_before_reading, with the values: Never, Always, and IfNotPartialRecord. These overwrite whatever was passed into the FillList method in code.
exclude_children_from_copy
Normally when you do copy_from, it copies any child nodes that might be in there. Aka, if you are copying a GameEntity, it copies all of the system entries, all of the metal_flow entries, and a bunch of others. These are child nodes, not attributes.
If you want to have it not copy, for instance, metal_flow entries, then on your copy_from=whatever" entry you can add a exclude_children_from_copy="metal_flow". If you also want systems to be excluded, you can add exclude_children_from_copy="metal_flow,system".
Exceptions To The Rule
Normally lists are read in as: IfPresent.ReplaceExistingList, Uniqueness.Required, ClearBeforeReading.Never
Here's a list of exceptions, which we believe is comprehensive:
- all nodes
- exclude_children_from_copy
- IfPresent.ReplaceExistingList, Uniqueness.Required, ClearBeforeReading.Always
- exclude_children_from_copy
- SpecialFaction
- team_center_colors_beyond_the_first
- IfPresent.AddToExistingList, Uniqueness.Unenforced, ClearBeforeReading.Never
- team_border_colors_beyond_the_first
- IfPresent.AddToExistingList, Uniqueness.Unenforced, ClearBeforeReading.Never
- custom_field
- arbitrary_options
- IfPresent.AddToExistingList, Uniqueness.Required, ClearBeforeReading.Never
- arbitrary_options
- team_center_colors_beyond_the_first
- GameEntity
- lod_distance_overrides
- IfPresent.ReplaceExistingList, Uniqueness.Unenforced, ClearBeforeReading.Never
- tags
- IfPresent.ReplaceExistingList, Uniqueness.Required, ClearBeforeReading.IfNotPartialRecord
- lod_distance_overrides
- AIWar2GalaxySetting
- arbitrary_options
- IfPresent.AddToExistingList, Uniqueness.Required, ClearBeforeReading.Never
- arbitrary_options
- Balance_MarkLevel
- most_everything_squad_cap_multiplier_list
- IfPresent.ReplaceExistingList, Uniqueness.Unenforced, ClearBeforeReading.Never
- frigate_squad_cap_multiplier_list
- IfPresent.ReplaceExistingList, Uniqueness.Unenforced, ClearBeforeReading.Never
- most_everything_squad_cap_multiplier_list
- ExternalConstants
- engine_stun_multipliers_by_stun_seconds
- IfPresent.ReplaceExistingList, Uniqueness.Unenforced, ClearBeforeReading.Never
- engine_stun_multipliers_by_stun_seconds
- TechUpgrade
- science_cost_per_time_unlocked
- IfPresent.ReplaceExistingList, Uniqueness.Unenforced, ClearBeforeReading.Never
- science_cost_per_time_unlocked