def save_mod(save_filename):
    """
    Save out a mod file.  This actually relies on an awful lot of global variables.
    Apologies.
    """
    mod = Mod(save_filename,
            'Arbitrary Partlocks: {}'.format(bal_last),
            'Apocalyptech',
            [
                'Auto-generated partlocks, chosen with an interactive CLI app.',
            ],
            lic=Mod.CC_BY_SA_40,
            )

    for cat_idx, cat_actions in enumerate(actions):
        cur_cat = categories[cat_idx]
        always_vals = set()
        never_vals = set()
        for action in cat_actions.values():
            if action.action == Action.ALWAYS:
                always_vals.add(action.list_idx)
            else:
                never_vals.add(action.list_idx)

        # Now, loop through each category and assign new weights
        if len(always_vals) > 0 or len(never_vals) > 0:
            if cat_idx == 0:

                # We could try to generalize, but whatever.  Just process anoints separately
                cur_idx = 0
                if cur_cat.cat.partlist:
                    cur_source = cur_cat.cat.partlist[0].anoint_source
                for num, part in enumerate(cur_cat.cat.partlist):

                    # Reset our array index if needed
                    if part.anoint_source != cur_source:
                        cur_idx = 0
                        cur_source = part.anoint_source

                    # If we have any "always" entries, we effectively assume everything else
                    # is a "never", so we process a bit differently
                    if len(always_vals) > 0:
                        if num not in always_vals:
                            never_vals.add(num)

                    # Now write out
                    if part.anoint_source != balance_name:
                        extra_txt = ' (affects anything using this anoint)'
                        attr_ref = 'GenericParts.Parts'
                    else:
                        extra_txt = ''
                        attr_ref = 'RuntimeGenericPartList.PartList'
                    if num in always_vals:
                        mod.comment('Anointments: Always choose {}{}'.format(part.short_name, extra_txt))
                        mod.reg_hotfix(Mod.PATCH, '',
                                part.anoint_source,
                                '{}[{}].Weight'.format(attr_ref, cur_idx),
                                BVCF(bvc=1))
                        mod.newline()
                    elif num in never_vals:
                        mod.comment('Anointments: Never choose {}{}'.format(part.short_name, extra_txt))
                        mod.reg_hotfix(Mod.PATCH, '',
                                part.anoint_source,
                                '{}[{}].Weight'.format(attr_ref, cur_idx),
                                BVCF(bvc=0))
                        mod.newline()

                    # Increment our index
                    cur_idx += 1

            else:

                # Make sure this is set to use weights
                if not cur_cat.cat.use_weight_with_mult:
                    mod.comment('Category {}: Enable weight-based part picking'.format(cat_idx))
                    mod.reg_hotfix(Mod.PATCH, '',
                            balance.partset_name,
                            'ActorPartLists.ActorPartLists[{}].bUseWeightWithMultiplePartSelection'.format(cat_idx-1),
                            'True')
                    mod.newline()

                # Regular parts
                # TODO: If we ever get GPartExpansion objects which do more than add expansions,
                # well have to support it here, too.
                cur_idx = cur_cat.start_part_idx
                for num, part in enumerate(cur_cat.cat.partlist):

                    # If we have any "always" entries, we effectively assume everything else
                    # is a "never", so we process a bit differently
                    if len(always_vals) > 0:
                        if num not in always_vals:
                            never_vals.add(num)

                    # Now write out
                    if num in always_vals:
                        mod.comment('Category {}: Always choose {}'.format(cat_idx, part.short_name))
                        mod.reg_hotfix(Mod.PATCH, '',
                                balance_name,
                                'RuntimePartList.AllParts[{}].Weight'.format(cur_idx),
                                BVCF(bvc=1))
                        mod.newline()
                    elif num in never_vals:
                        mod.comment('Category {}: Never choose {}'.format(cat_idx, part.short_name))
                        mod.reg_hotfix(Mod.PATCH, '',
                                balance_name,
                                'RuntimePartList.AllParts[{}].Weight'.format(cur_idx),
                                BVCF(bvc=0))
                        mod.newline()

                    # Increment index
                    cur_idx += 1

    print('')
    print(color_success + '='*80)
    mod.close()
    print(color_success + '='*80)
    print(color_reset)
    input('Hit Enter to Continue...')
    print('')
        balances.append(f'/Game/PatchDLC/Geranium/Customizations/RoomDeco/RoomDecoration_Geranium_{num}.InvBal_RoomDecoration_Geranium_{num}')
    for num in [1, 2, 3]:
        balances.append(f'/Game/PatchDLC/Geranium/Customizations/RoomDeco/RoomDecoration_Geranium_IO_{num}.RoomDecoration_Geranium_IO_{num}')
    balances.append('/Game/PatchDLC/Geranium/Customizations/RoomDeco/RoomDecoration_KeyToCity.InvBal_RoomDecoration_KeyToCity')
    mod.comment('Room Decorations')
    set_pool(mod, '/Game/Pickups/Customizations/_Design/ItemPools/PlayerRoomDeco/ItemPool_Customizations_RoomDeco_Loot.ItemPool_Customizations_RoomDeco_Loot', balances)
    mod.newline()
    num_decorations = len(balances)

    # Now set our weighting on the main customization-drop pool
    mod.header('Overall customization-type weighting')
    # Keep the "average" weight in this pool to the default 0.05, in case anything weird happens to the
    # pool later.
    target_total = 6*0.05
    total_weight = num_heads + num_skins + num_weap_skins + num_trinkets + num_echo_skins + num_decorations
    for idx, individual_weight in enumerate([
            num_heads,
            num_skins,
            num_weap_skins,
            num_trinkets,
            num_echo_skins,
            num_decorations,
            ]):
        weight = individual_weight/total_weight*target_total
        mod.reg_hotfix(Mod.PATCH, '',
                '/Game/GameData/Loot/ItemPools/ItemPool_SkinsAndMisc',
                'BalancedItems.BalancedItems[{}].Weight'.format(idx),
                BVCF(bvc=weight))

    mod.close()
예제 #3
0
    '/Game/PatchDLC/Hibiscus/GameData/Loot/EnemyPools/ItemPoolList_StandardEnemyGunsandGear_Hibiscus',
    'ItemPools.ItemPools[6].PoolProbability.BaseValueAttribute',
    Mod.get_full_cond(
        '/Game/GameData/Loot/ItemPools/Attributes/Att_Shields_DropOdds',
        'GbxAttributeData'))
mod.newline()

# DLC4 Fix
mod.comment('DLC4')
mod.reg_hotfix(
    Mod.CHAR, 'MatchAll',
    '/Game/PatchDLC/Alisma/GameData/Loot/EnemyPools/ItemPoolList_StandardEnemyGunsandGear_Alisma',
    'ItemPools.ItemPools[5]', """(
            ItemPool={},
            PoolProbability={},
            NumberOfTimesToSelectFromThisPool={}
        )""".format(
        Mod.get_full_cond(
            '/Game/GameData/Loot/ItemPools/Shields/ItemPool_Shields_All',
            'ItemPoolData'),
        BVCF(
            bva=
            '/Game/GameData/Loot/ItemPools/Attributes/Att_Shields_DropOddsWithMayhem_Total'
        ),
        BVCF(bvc=1),
    ))
mod.newline()

# And done.
mod.close()
예제 #4
0
            mod.reg_hotfix(
                Mod.CHAR, 'MatchAll',
                '/Game/PatchDLC/BloodyHarvest/Missions/Side/Seasonal/BloodyHarvest_Intro/{}'
                .format(pool),
                'ItemPools.ItemPools[{}].NumberOfTimesToSelectFromThisPool.BaseValueConstant'
                .format(idx), num_times)
    mod.newline()

    # Loot Ghost drops
    mod.comment(
        'Increase quantity of drops from Loot Ghosts, and make legendaries more likely'
    )
    mod.reg_hotfix(
        Mod.CHAR, 'MatchAll',
        '/Game/PatchDLC/BloodyHarvest/Enemies/Ghost/_Shared/LootPool/ItemPool_BH_LootGhost',
        'Quantity', BVCF(bvc=7))
    mod.reg_hotfix(
        Mod.CHAR, 'MatchAll',
        '/Game/PatchDLC/BloodyHarvest/Enemies/Ghost/_Shared/LootPool/ItemPool_BH_LootGhost',
        'BalancedItems.BalancedItems[4].Weight.BaseValueConstant', 0.032759)
    mod.newline()

    if False:
        # This is something related to twitch streaming integration.  I don't actually want
        # to include anything like that in mods, but figured I'd keep it here for reference.
        mod.reg_hotfix(
            Mod.LEVEL, heck,
            '/Game/GameData/Social/StreamingInteraction/Events/StreamingInteraction_Lootable_RedChest',
            'LootableFilterBalanceData.LootableFilterBalanceData[2]',
            mod.get_full_cond(
                '/Game/Lootables/_Design/Data/Eridian/LootDef_Eridian_RedChest'
예제 #5
0
    'only_atlas_grenades.bl3hotfix',
    'Only Atlas Grenades',
    'Apocalyptech',
    [
        "While doing a Zane runthrough, I only really wanted homing grenades,",
        "since I didn't have a lot of control over when exactly they were",
        "getting thrown out (or at least I didn't care enough to try and",
        "control for that).  So, I wanted only homing.  This does that!",
        "",
        "This does *not* affect legendary grenade drops, only the regular",
        "sort.",
    ],
    lic=Mod.CC_BY_SA_40,
    v='1.0.0',
    cats='gear-general, gear-grenade',
)

for pool_name in [
        'ItemPool_GrenadeMods_01_Common',
        'ItemPool_GrenadeMods_02_Uncommon',
        'ItemPool_GrenadeMods_03_Rare',
        'ItemPool_GrenadeMods_04_VeryRare',
]:
    for idx in range(1, 6):
        mod.reg_hotfix(
            Mod.PATCH, '',
            '/Game/GameData/Loot/ItemPools/GrenadeMods/{}'.format(pool_name),
            'BalancedItems.BalancedItems[{}].Weight'.format(idx), BVCF(bvc=0))

mod.close()
    ('/Game/GameData/Modifiers/DataTable_Mayhem_CoreMods_Easy', 2),
    ('/Game/GameData/Modifiers/DataTable_Mayhem_CoreMods_Medium', 5),
    ('/Game/GameData/Modifiers/DataTable_Mayhem_CoreMods_Hard', 9),
]:
    mod.table_hotfix(Mod.PATCH, '', table, 'ExpGain_CombatOnly', 'MinValue',
                     default_val * xp_scale)
    mod.table_hotfix(Mod.PATCH, '', table, 'ExpGain_CombatOnly', 'MaxValue',
                     default_val * xp_scale)
mod.newline()

# Anoint Chances
mod.comment(
    'Anoint drop chances -- These are currently identical to what the Farming')
mod.comment(
    'Frenzy event has set, apart from Mayhem 3+, which is set here to be')
mod.comment('guaranteed.  Possibly the Farming Frenzy rates are permanent?')
for (col, values) in [('PlaythroughOne', (2.3, 1.5, 1, 0)),
                      ('PlaythroughTwoAndBeyond', (2.3, 1.5, 1, 0))]:
    for row, value in zip([
            'NoneChance',
            'NoneChance_Mayhem_01',
            'NoneChance_Mayhem_02',
            'NoneChance_Mayhem_03',
    ], values):
        mod.table_hotfix(
            Mod.PATCH, '',
            '/Game/Gear/Weapons/_Shared/_Design/EndGameParts/DropWeight/DataTable_EndGame_DropChance',
            row, col, BVCF(bvc=value))

mod.close()
예제 #7
0
        lic=Mod.CC_BY_SA_40,
        )

art_bal_name = '/Game/Gear/Artifacts/_Design/BalanceDefs/InvBalD_Artifact_05_Legendary'
cat_idx = 1
terra_part = '/Game/Gear/Artifacts/_Design/PartSets/Abilities/_Legendary/Misc/PendantOfTerramorphous/Artifact_Part_Ability_PendantOfTerramorphous'

# Add the part
mod.comment('Set as a valid drop')
data = BL3Data()
art_bal = Balance.from_data(data, art_bal_name)
cat = art_bal.categories[cat_idx]
if len(cat) != 14:
    raise Exception('Expected to find a category with fourteen parts!')
cat.add_part_name(terra_part, 1)
art_bal.hotfix_balance_full(mod)
mod.newline()

# Now buff up its health regen rate -- the default is really anemic (presumably it
# was scrapped before they got around to looking at balancing it).  Default value
# is `0.001`.
mod.comment('Buff health regen')
mod.table_hotfix(Mod.PATCH, '',
        '/Game/Gear/Artifacts/_Design/Balance/Table_Artifact_Abilities2',
        'LEGENDARY_BloodOfTerramorphous_HealthRegen',
        'Multiplier',
        BVCF(bvc=0.03))
mod.newline()

mod.close()
예제 #8
0
rapidfire = Config(filename='rapidfire',
        label='Rapid-Fire',
        desc=[
            "Converts the Fabricator's primary firing mode to full auto single-gun",
            "firing, and the legendary mode to a full auto shotgun-like blast.  The",
            "quantity of gear dropped on the legendary mode has been buffed up a bit",
            "as well, which is mildly cheaty.",
            ],

        weap_name='Vladof Fabricator',
        card_ammo='Shoots [skill]guns[/skill] instead of bullets!',
        card_cost='Requires [skill]1[/skill] Eridium per gun.',

        itempool_reg=fab_default_reg,
        itempool_reg_qty=BVCF(bvc=1),
        firerate_reg=6,
        reg_cost=1,
        label_reg='Gun Hose',

        itempool_leg=fab_default_leg,
        # Buffing this a bit, so we get at least 2
        itempool_leg_qty=BVCF(ai='/Game/GameData/Loot/ItemPools/Init_RandomLootCount_SomeMinOne', bvs=2),
        firerate_leg=0.75,
        leg_cost=250,
        label_leg='Legendary Blast',
        )

###
### Eridium config
###
def zero_itempoollist(hf_mode, hf_target, poollist_name, index):
    global mod
    mod.reg_hotfix(hf_mode, hf_target, poollist_name,
                   'ItemPools.ItemPools[{}].PoolProbability'.format(index),
                   BVCF(bvc=0))
             toc) in enumerate(data[0]['RuntimePartList']['PartTypeTOC']):
            if toc['NumParts'] > 0 and toc['StartIndex'] >= 0:
                for part_idx in range(toc['StartIndex'],
                                      toc['StartIndex'] + toc['NumParts']):
                    part_idx_to_apl_idx[part_idx] = apl_idx

        # Now loop through parts
        apl_idxes_to_check = set()
        for (part_idx,
             part) in enumerate(data[0]['RuntimePartList']['AllParts']):
            if part['PartData'][1] == aug_name:
                do_start(mod, last_component)
                mod.reg_hotfix(
                    Mod.PATCH, '', obj_name,
                    'RuntimePartList.AllParts.AllParts[{}].Weight'.format(
                        part_idx), BVCF(bvc=0))
                apl_idxes_to_check.add(part_idx_to_apl_idx[part_idx])
        do_end(mod, last_component)

        # Check the PartSet for weighting, if we haven't already
        if len(apl_idxes_to_check) > 0:
            partset_name = data[0]['PartSetData'][1]
            partset_cat = '{} Weighting'.format(partset_name.split('/')[-1])
            if partset_name not in partset_seen:
                partset_seen.add(partset_name)
                partset_data = bl3data.get_data(partset_name)
                if len(partset_data) != 1:
                    raise Exception(
                        'PartSet with an unknown export count ({}): {}'.format(
                            len(partset_data), partset_name))
                for apl_idx in sorted(apl_idxes_to_check):
                    # If we have any "always" entries, we effectively assume everything else
                    # is a "never", so we process a bit differently
                    if len(always_vals) > 0:
                        if num not in always_vals:
                            never_vals.add(num)

                    # Now write out
                    if num in always_vals:
                        if partsource:
                            mod.comment(
                                'Anointments: Always choose {} (affects anything using this anoint)'
                                .format(part.short))
                            mod.reg_hotfix(
                                Mod.PATCH, '', partsource,
                                'GenericParts.Parts[{}].Weight'.format(
                                    part.idx), BVCF(bvc=1))
                            mod.newline()
                        else:
                            mod.comment('Anointments: Always choose {}'.format(
                                part.short))
                            mod.reg_hotfix(
                                Mod.PATCH, '', balance_name,
                                'RuntimeGenericPartList.PartList[{}].Weight'.
                                format(part.idx), BVCF(bvc=1))
                            mod.newline()
                    elif num in never_vals:
                        if partsource:
                            mod.comment(
                                'Anointments: Never choose {} (affects anything using this anoint)'
                                .format(part.short))
                            mod.reg_hotfix(
예제 #12
0
     72),

        # DLCs
    ('/Game/PatchDLC/Dandelion/Gear/CM/_D/PartSets/_U/SRN/InvBalD_CM_Siren_DLC1',
     72),
    ('/Game/PatchDLC/Hibiscus/Gear/ClassMods/_Design/SRN/InvBalD_CM_Siren_Hib',
     72),
    ('/Game/PatchDLC/Alisma/Gear/ClassMods/_Design/SRN/InvBalD_CM_Siren_Alisma',
     73),
]:

    # Set the `None` weight to zero
    mod.reg_hotfix(
        Mod.PATCH, '', bal_name,
        'RuntimePartList.AllParts.AllParts[{}].Weight'.format(part_idx),
        BVCF(bvc=0))

    # Find the category index and partset name to make sure that it's using the weight.
    bal_obj = data.get_data(bal_name)[0]
    partset_name = bal_obj['PartSetData'][1]
    cat_idx = 0
    for idx, toc in enumerate(bal_obj['RuntimePartList']['PartTypeTOC']):
        if toc['StartIndex'] > part_idx:
            break
        if toc['NumParts'] > 0:
            cat_idx = idx

    # Now do those updates
    mod.reg_hotfix(
        Mod.PATCH, '', partset_name,
        'ActorPartLists.ActorPartLists[{}].bUseWeightWithMultiplePartSelection'
            'MinValue',
            default_val*xp_scale)
    mod.table_hotfix(Mod.PATCH, '',
            table,
            'ExpGain_CombatOnly',
            'MaxValue',
            default_val*xp_scale)
mod.newline()

# Anoint Chances
mod.comment('Anoint drop chances -- These are currently identical to what the Farming')
mod.comment('Frenzy event has set, apart from Mayhem 3+, which is set here to be')
mod.comment('guaranteed.  Possibly the Farming Frenzy rates are permanent?')
for (col, values) in [
        ('PlaythroughOne', (2.3, 1.5, 1, 0)),
        ('PlaythroughTwoAndBeyond', (2.3, 1.5, 1, 0))
        ]:
    for row, value in zip([
            'NoneChance',
            'NoneChance_Mayhem_01',
            'NoneChance_Mayhem_02',
            'NoneChance_Mayhem_03',
            ], values):
        mod.table_hotfix(Mod.PATCH, '',
                '/Game/Gear/Weapons/_Shared/_Design/EndGameParts/DropWeight/DataTable_EndGame_DropChance',
                row,
                col,
                BVCF(bvc=value))

mod.close()
        'TechSlaughter_P',
        'Towers_P',
        'Watership_P',
        'WetlandsBoss_P',
        'WetlandsVault_P',
        'Wetlands_P',
]:
    for row_name in [
            'Rare',
            'VeryRare',
            'SuperRare',
    ]:
        mod.table_hotfix(
            Mod.LEVEL, level_name,
            '/Game/GameData/Balance/RareSpawns/Table_Async_RareSpawnRarities',
            row_name, 'Value', BVCF(bvc=100))
mod.newline()

# Some individual spawn rates which apparently don't use those values above
# TODO: I think Brood Mother (Pyre of Stars) needs something similar.  GBX
# has said that Brood Mother's spawn actually depends on killing mobs outside
# her lair, so I should look into that...
mod.header('Individual Spawn rates')

mod.comment('Wick and Warty')
mod.reg_hotfix(
    Mod.LEVEL, 'Towers_P',
    '/Game/Maps/Zone_1/Towers/Towers_Combat.Towers_Combat:PersistentLevel.OakMissionRareSpawner_VicAndWarty',
    'PercentChanceToSpawn', BVCF(bvc=100))
mod.newline()
예제 #15
0
        ('/Game/PatchDLC/Alisma/GameData/Loot/EnemyPools/ItemPoolList_StandardEnemyGunsandGear_Alisma',
         8),
        ('/Game/PatchDLC/Alisma/GameData/Loot/EnemyPools/ItemPoolList_BadassEnemyGunsGear_Alisma',
         9),
            # Yep, it's really in there twice
        ('/Game/PatchDLC/Alisma/GameData/Loot/EnemyPools/ItemPoolList_Boss_Alisma',
         3),
        ('/Game/PatchDLC/Alisma/GameData/Loot/EnemyPools/ItemPoolList_Boss_Alisma',
         4),
    ]:

        # Defaults vary depending on pool; for base-game it's 0.5%.
        # Tends to be higher for DLCs, I believe.
        mod.reg_hotfix(Mod.CHAR, 'MatchAll', pool,
                       'ItemPools[{}].PoolProbability'.format(index),
                       BVCF(bvc=rate))

    mod.newline()

    # Some extra things to do if we're disabling entirely.
    if rate == 0:

        # TODO: Still getting some cosmetic drops in DLC2, maybe from badasses?

        mod.header('Remove cosmetics from containers')

        mod.comment('Regular container ItemPools')
        for pool, index in [
            ('/Game/GameData/Loot/ItemPools/ItemPool_ChestFlaps', 5),
            ('/Game/GameData/Loot/ItemPools/ItemPool_RedChestFlaps', 4),
            ('/Game/GameData/Loot/ItemPools/ItemPool_Shields_Skins_Artifacts_GrenadeMods_ClassMods_Deco',
def zero_pool(hf_mode, hf_target, pool_name, index):
    global mod
    mod.reg_hotfix(hf_mode, hf_target, pool_name,
                   'BalancedItems.BalancedItems[{}].Weight'.format(index),
                   BVCF(bvc=0))
예제 #17
0
    def write_mod(self):

        mod = Mod(
            f'money_grenade_changes_{self.mod_filename}.bl3hotfix',
            f'Money Grenade Changes: {self.mod_label}',
            'Apocalyptech',
            [
                f"Changes the 'Money' grenade part to drop {self.desc_text} instead of money.",
            ],
            contact='https://apocalyptech.com/contact.php',
            lic=Mod.CC_BY_SA_40,
            v='1.0.0',
            cats='joke, gear-grenade, cheat',
            ss=
            f'https://raw.githubusercontent.com/BLCM/bl3mods/master/Apocalyptech/gear_changes/money_grenade_changes/screenshot_{self.mod_filename}.png',
        )

        mod.header('Update item prefixes')
        mod.reg_hotfix(
            Mod.PATCH, '',
            '/Game/Gear/GrenadeMods/_Design/Naming/GrenadeModNamingStrategy',
            'SingleNames.SingleNames[20].NamePart.Object..PartName',
            self.single_name)
        mod.reg_hotfix(
            Mod.PATCH, '',
            '/Game/Gear/GrenadeMods/_Design/Naming/GrenadeModNamingStrategy',
            'TripleNames.TripleNames[11].NamePart.Object..PartName',
            self.triple_name)
        self._combo_name(mod, 19, self.combo_name_mirv)
        self._combo_name(mod, 36, self.combo_name_rain)
        self._combo_name(mod, 52, self.combo_name_nuke)
        self._combo_name(mod, 67, self.combo_name_large)
        self._combo_name(mod, 81, self.combo_name_bouncy)
        self._combo_name(mod, 94, self.combo_name_spring)
        self._combo_name(mod, 106, self.combo_name_sticky)
        self._combo_name(mod, 117, self.combo_name_artillery)
        self._combo_name(mod, 127, self.combo_name_lingering)
        self._combo_name(mod, 136, self.combo_name_divide)
        self._combo_name(mod, 144, self.combo_name_singularity)
        self._combo_name(mod, 151, self.combo_name_roider)
        self._combo_name(mod, 157, self.combo_name_link)
        self._combo_name(mod, 162, self.combo_name_transfusion)
        self._combo_name(mod, 166, self.combo_name_generator)
        self._combo_name(mod, 169, self.combo_name_elemental)
        self._combo_name(mod, 171, self.combo_name_force)
        self._combo_name(mod, 172, self.combo_name_money)
        mod.newline()

        mod.header('Update part name')
        mod.reg_hotfix(
            Mod.PATCH, '',
            '/Game/Gear/GrenadeMods/_Design/PartSets/Part_Behavior/Money/UIStat_Grenade_Money_Icon',
            'Text', self.part_text.upper())
        mod.newline()

        mod.header('Update drop pools and card descriptions')
        for pool_label, rewards, (uistat_name, uistat_text) in [
            ('Single-part grenade', self.rewards1, self.uistat1),
            ('Double-part grenade', self.rewards2, self.uistat2),
            ('Triple-part grenade', self.rewards3, self.uistat3),
        ]:
            mod.comment(pool_label)
            mod.reg_hotfix(Mod.PATCH, '', rewards.pool_name, 'BalancedItems',
                           str(rewards))
            mod.reg_hotfix(Mod.PATCH, '', uistat_name, 'FormatText',
                           uistat_text.format(self.card_text))
            mod.newline()

        # Special-case hardcodes for some mod types
        if self.mod_label == 'Diamond Keys':

            # Fix up the diamond key item so that it looks better when dropped
            # (by default it just looks like eridium, and also doesn't auto-pickup)

            mod.header_lines([
                "Fixing up droppable Diamond Key object",
                "",
                "We're making a few changes here which affect the item card specifically,",
                "though the item card will basically never be seen since we're enabling",
                "auto-pickup as well.  If you *do* get a glimpse of the card (using Photo",
                "Mode or the like) you'll see a bit of weirdness still -- the card might",
                "inherit the visual price of whatever card was last looked at, and the",
                "loot beam icon might inherit the last-looked-at item as well.  In the",
                "end I'm not bothering to track that down, though, since you've gotta",
                "work hard to see the card anyway.",
            ])

            mod.comment('Use an actual key mesh')
            mod.reg_hotfix(
                Mod.PATCH,
                '',
                '/Game/PatchDLC/VaultCard/Data/Currency/BP_DiamondKey_Single.Default__BP_DiamondKey_Single_C',
                'ItemMeshComponent.Object..StaticMesh',

                # Tried lots of various meshes here; I'm tempted to go with one of the keyring ones,
                # but in the end I just went with one of the smaller single-key ones.  Would be nice
                # if I could get that Geranium Key-to-City ones to interact with physics, though
                # that might cause weird problems with the key during the plot (or on Decoration),
                # so eh.  We'll just stick with the tiny key for now.

                # A bit much, though it's a nice big size.  Is definitely a keyring, though.
                #Mod.get_full_cond('/Dandelion/Missions/Plot/Ep04_Trashtown/Janitor_Keyring/Model/Meshes/SM_Janitor_Keyring', 'StaticMesh'),

                # Definitely a keyring; probably better than SM_Janitor_Keyring if I feel okay with that...
                #Mod.get_full_cond('/Ixora2/Missions/02_Eden6/Keyring/Model/Meshes/SM_Key_Ring', 'StaticMesh'),

                # Hah, just a rusty sphere basically
                #Mod.get_full_cond('/Geranium/LevelArt/Lodge/Props/Model/Meshes/SM_Apple_Key', 'StaticMesh'),

                # Very nice (and rightly-sized!) key, but it weirdly doesn't seem to actually interact with physics
                # at all?  Just hangs there in midair.
                #Mod.get_full_cond('/Geranium/InteractiveObjects/MissionAssets/Plot_5/Model/Meshes/SM_Key_to_City', 'StaticMesh'),
                # Same story here; and seemingly identical to the other one.  Almost certainly a copied object.
                #Mod.get_full_cond('/Game/PatchDLC/Geranium/InteractiveObjects/PlayerQuarters/RoomDecoMeshes/Meshes/SM_Key_to_City_RoomDeco', 'StaticMesh'),

                # Perfectly serviceable key, but quite small
                #Mod.get_full_cond('/Hibiscus/InteractiveObjects/MissionSpecific/Plot/EP03/Pickups/SM_Key_GateKey', 'StaticMesh'),
                # Perfectly serviceable key, but quite small
                Mod.get_full_cond(
                    '/Game/LevelArt/Environments/_Global/Props/Keys/Key_Scooters/Model/Meshes/SM_Key_Scooters',
                    'StaticMesh'),
            )
            mod.newline()

            mod.comment('Set a different item card type')
            mod.reg_hotfix(
                Mod.PATCH,
                '',
                '/Game/PatchDLC/VaultCard/Data/Currency/InvData_DiamondKey',
                'ItemCardTypeFrameName',
                #'currencyEridium',
                # Mission is nice; has a diamondy icon, even!
                'Mission',
                # Cash works well
                #'Cash',
            )
            mod.newline()

            mod.comment(
                'Set monetary value to 1; might prevent currency from inheriting from previously-viewed items'
            )
            mod.reg_hotfix(
                Mod.PATCH, '',
                '/Game/PatchDLC/VaultCard/Data/Currency/InvData_DiamondKey',
                'MonetaryValue', BVCF(bvc=1))
            mod.newline()

            mod.comment(
                'Assign Legendary rarity (instead of Eridium), for loot bar + frame color'
            )
            mod.reg_hotfix(
                Mod.PATCH, '',
                '/Game/PatchDLC/VaultCard/Data/Currency/InventoryBalance_DiamondKey',
                'RarityData',
                Mod.get_full_cond(
                    '/Game/GameData/Loot/RarityData/RarityData_05_Legendary',
                    'RarityData'))
            mod.newline()

            mod.comment('Auto-pickup Diamond Keys')
            mod.reg_hotfix(
                Mod.PATCH,
                '',
                '/Game/Gear/_Shared/_Design/InventoryCategories/InventoryCategory_DiamondKey',
                'PickupActionType',
                #'OnUseOnly',
                'OnUseOrTouch',
            )
            mod.newline()

        mod.close()
        '/Game/PatchDLC/Ixora2/Gear/ClassMods/_Design/SRN/L01/InvBalD_CM_Ixora2_SRN_L01',
]:

    bal = Balance.from_data(data, bal_name)

    found_part = False
    part_idx = 0
    for cat in bal.categories:
        for part in cat.partlist:
            # Our PRIMARY part will always be category index 3
            if cat.index == 3 and part.part_name == 'None':
                # Set the weight of the `None` part to zero
                mod.reg_hotfix(
                    Mod.PATCH, '', bal_name,
                    'RuntimePartList.AllParts.AllParts[{}].Weight'.format(
                        part_idx), BVCF(bvc=0))

                # Make sure we're using weights
                mod.reg_hotfix(
                    Mod.PATCH, '', bal.partset_name,
                    'ActorPartLists.ActorPartLists[{}].bUseWeightWithMultiplePartSelection'
                    .format(cat.index), 'True')

                # Break out of the loop
                found_part = True
                break
            part_idx += 1
        if found_part:
            break

mod.close()