LootTableAPI

Tracking Issue: #8

Unprivates LootTables and adds convenience functions for common OPTC loot table operations.

Refresher on loot tables

Consider the simplified struct definitions taken from X2TacticalGameRulesetDataStructures:

struct native LootTableEntry
{
    var int     Chance;
    var int     RollGroup;
    var int     MinCount, MaxCount;

    // Modifier on the chance, multiplicatively applied for each existing Item of type TemplateName acquired:
    // TotalChance = Chance * ChanceModPerExistingItem ^ NumExistingItems
    var float   ChanceModPerExistingItem;

    //  NOTE: these two are mutually exclusive, so only one should ever be filled in for a given entry
    var name    TemplateName;
    var name    TableRef;
};

struct native LootTable
{
    var name    TableName;
    var array<LootTableEntry> Loots;
};

When a unit template says "hey, I drop this kind of loot", it references a given LootTable by name. For the common random loot, a random unit is designated as a loot carrier at the start of the mission and its loot is rolled. The game then looks at the Loots entries and for every distinct RollGroup the game rolls a number in the half-open interval [0; 100[ (largest possible number is 99) to choose the item from that RollGroup.

The game does this by iterating through the LootTableEntry entries with the current RollGroup and subtracting its Chance (after applying ChanceModPerExistingItem) from the current chance. If this causes the current chance to go below 0, this entry has been chosen and the game moves on to the next RollGroup.

This has a number of non-intuitive consequences:

  1. Within the same RollGroups, the chances are not independent; the game may choose 0 or 1 entries from that group.
  2. If the sum of chances in a RollGroup is x < 100, there is a 100 - x percent chance that no entry is chosen and this RollGroup doesn't roll anything.
  3. If the sum of chances is x = 100, then every entry will have a percent chance to be chosen identical to the Chance of the entry.
  4. If the sum of chances is x > 100, then the latter entries will actually have a lower chance. Consider entries in the same roll group with chances 90, 20, 10: The first entry will be chosen 9/10 times, the second 1/10 times, and the third never!

We will discuss later what this means for mods.

When an entry has been chosen, the game generates between MinCount and MaxCount of the item listed in TemplateName, if it is non-empty -- otherwise, the game invokes the aforedescribed algorithm recursively with the table referenced by name in TableRef.

New APIs

First and foremost, you need a loot table manager to work with. If you are in a place before templates are validated (template creation, OnPostTemplatesCreated), the loot table manager doesn't exist yet and you need to operate on the ClassDefaultObject; otherwise just request the loot table manager with the singleton accessor:

// OPTC or template creation
LootManager = X2LootTableManager(class'XComEngine'.static.FindClassDefaultObject("XComGame.X2LootTableManager"));
// Otherwise
LootManager = class'X2LootTableManager'.static.GetLootTableManager();

The OPTC context is going to be the most common one, so the below documentation assumes that it's going to be used.

LootTables

This feature unprivates the otherwise config-only LootTables array. You can pretty much change anything there. If your code runs after OnPostTemplatesCreated returns, ensure to call InitLootTables if you add/remove/move around tables as a whole (the game maintains a name->index lookup for efficiency).

RecalculateLootTableChanceStatic

  • public static function RecalculateLootTableChanceStatic(name TableName, bool bEquallyDistributed = false);

A non-static variant for the runtime context also exists: RecalculateLootTableChance

Changes the chances for every entry in the table referred to by TableName so that the chances of every RollGroup add up to 100. If bEquallyDistributed is true, all chances within a roll group will be equal, otherwise the proportions between the chances are maintained.

This is a useful function for when you just want to add some items to existing roll groups of existing tables. Your items will naturally be placed at the end of the array, so according to consequence 4, your items might never be rolled. Calling this function makes sure your items will have a proportional chance to be rolled.

Warning: Some of the latter functions recalculate chances for a given roll group implicitly unless opted out of.

Warning: Some RollGroups have chance sums much lower than 100 so that only sometimes an item from that group drops. Carelessly calling RecalculateLootTableChance or forgetting to opt out of recalculating may cause showers of rare loot!

AddEntryStatic/RemoveEntryStatic

  • public static function AddEntryStatic(name TableName, LootTableEntry TableEntry, optional bool bRecalculateChances = true);
  • public static function RemoveEntryStatic(name TableName, LootTableEntry TableEntry, optional bool bRecalculateChances = true);

Non-static variants for the runtime context also exist: AddEntry, Remove

Adds the entry to the given loot table, or removes it. Unless bRecalculateChances is set to false, also recalculates the chances for the entry's RollGroup so that they sum up to 100.

AddLootTableStatic/RemoveLootTableStatic

Actually, I have no idea why they exist and they probably don't do what you want them to. Don't use them. If you do, also call InitLootTables on the ClassDefaultObject.

Example:

// Adds M2 and M3 to ADVENT mid- and end-game loot, respectively
static event OnPostTemplatesCreated()
{
    local LootTableEntry M2Entry, M3Entry;

    M2Entry.Chance = 20;
    M2Entry.MinCount = 1;
    M2Entry.MaxCount = 1;
    M2Entry.TemplateName = 'AdventGremlinM2';
    // RollGroup 1 is 100% a random Weapon Upgrade. This turns it into 20% Gremlin, 80% upgrade.
    M2Entry.RollGroup = 1;

    M3Entry = M2Entry;
    M3Entry.TemplateName = 'AdventGremlinM3';

    class'X2LootTableManager'.static.AddEntryStatic('ADVENTMidTimedLoot', M2Entry, true);
    class'X2LootTableManager'.static.AddEntryStatic('ADVENTLateTimedLoot', M3Entry, true);
}

More example:

Musashis RPG Overhaul uses this feature. Feel free to peruse its source code!

Source code references