コード例 #1
0
def main():

    # Set up args
    parser = argparse.ArgumentParser(
        description='Borderlands 3 CLI Savegame Editor v{} (PC Only)'.format(
            bl3save.__version__),
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        epilog="""
                The default output type of "savegame" will output theoretically-valid
                savegames which can be loaded into BL3.  The output type "protobuf"
                will save out the extracted, decrypted protobufs.  The output
                type "json" will output a JSON-encoded version of the protobufs
                in question.  The output type "items" will output a text
                file containing base64-encoded representations of the user's
                inventory.  These can be read back in using the -i/--import-items
                option.  Note that these are NOT the same as the item strings used
                by the BL3 Memory Editor.
            """)

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version='BL3 CLI SaveEdit v{}'.format(bl3save.__version__),
    )

    parser.add_argument(
        '-o',
        '--output',
        choices=['savegame', 'protobuf', 'json', 'items'],
        default='savegame',
        help='Output file format',
    )

    parser.add_argument(
        '--csv',
        action='store_true',
        help='When importing or exporting items, use CSV files',
    )

    parser.add_argument(
        '-f',
        '--force',
        action='store_true',
        help='Force output file to overwrite',
    )

    parser.add_argument('-q',
                        '--quiet',
                        action='store_true',
                        help='Supress all non-essential output')

    # Actual changes the user can request
    parser.add_argument(
        '--name',
        type=str,
        help='Set the name of the character',
    )

    parser.add_argument(
        '--save-game-id',
        dest='save_game_id',
        type=int,
        help='Set the save game slot ID (possibly not actually ever needed)',
    )

    parser.add_argument(
        '--randomize-guid',
        dest='randomize_guid',
        action='store_true',
        help='Randomize the savegame GUID',
    )

    parser.add_argument(
        '--zero-guardian-rank',
        dest='zero_guardian_rank',
        action='store_true',
        help='Zero out savegame Guardian Rank',
    )

    levelgroup = parser.add_mutually_exclusive_group()

    levelgroup.add_argument(
        '--level',
        type=int,
        help='Set the character to this level (from 1 to {})'.format(
            bl3save.max_level),
    )

    levelgroup.add_argument(
        '--level-max',
        dest='level_max',
        action='store_true',
        help='Set the character to max level ({})'.format(bl3save.max_level),
    )

    itemlevelgroup = parser.add_mutually_exclusive_group()

    itemlevelgroup.add_argument(
        '--items-to-char',
        dest='items_to_char',
        action='store_true',
        help='Set all inventory items to the level of the character')

    itemlevelgroup.add_argument(
        '--item-levels',
        dest='item_levels',
        type=int,
        help='Set all inventory items to the specified level')

    itemmayhemgroup = parser.add_mutually_exclusive_group()

    itemmayhemgroup.add_argument(
        '--item-mayhem-max',
        dest='item_mayhem_max',
        action='store_true',
        help='Set all inventory items to the maximum Mayhem level ({})'.format(
            bl3save.mayhem_max))

    itemmayhemgroup.add_argument(
        '--item-mayhem-levels',
        dest='item_mayhem_levels',
        type=int,
        choices=range(bl3save.mayhem_max + 1),
        help=
        'Set all inventory items to the specified Mayhem level (0 to remove)')

    parser.add_argument(
        '--mayhem',
        type=int,
        choices=range(12),
        help=
        'Set the mayhem mode for all playthroughs (mostly useful for Normal mode)',
    )

    parser.add_argument(
        '--mayhem-seed',
        dest='mayhem_seed',
        type=int,
        help='Sets the mayhem random seed for all playthroughs',
    )

    parser.add_argument(
        '--money',
        type=int,
        help='Set money value',
    )

    parser.add_argument(
        '--eridium',
        type=int,
        help='Set Eridium value',
    )

    parser.add_argument(
        '--clear-takedowns',
        dest='clear_takedowns',
        action='store_true',
        help=
        'Clears out the Takedown Discovery missions so they don\'t clutter your UI',
    )

    unlock_choices = [
        'ammo',
        'backpack',
        'analyzer',
        'resonator',
        'gunslots',
        'artifactslot',
        'comslot',
        'allslots',
        'tvhm',
        'vehicles',
        'vehicleskins',
        'cubepuzzle',
    ]
    parser.add_argument(
        '--unlock',
        action=cli_common.DictAction,
        choices=unlock_choices + ['all'],
        default={},
        help='Game features to unlock',
    )

    tvhmgroup = parser.add_mutually_exclusive_group()

    tvhmgroup.add_argument(
        '--copy-nvhm',
        action='store_true',
        help='Copies NVHM/Normal state to TVHM',
    )

    tvhmgroup.add_argument(
        '--copy-tvhm',
        action='store_true',
        help='Copies TVHM state to NVHM/Normal',
    )

    tvhmgroup.add_argument(
        '--unfinish-nvhm',
        dest='unfinish_nvhm',
        action='store_true',
        help=
        '"Un-finishes" the game: remove all TVHM data and set Playthrough 1 to Not Completed',
    )

    parser.add_argument(
        '-i',
        '--import-items',
        dest='import_items',
        type=str,
        help='Import items from file',
    )

    parser.add_argument(
        '--allow-fabricator',
        dest='allow_fabricator',
        action='store_true',
        help='Allow importing Fabricator when importing items from file',
    )

    parser.add_argument(
        '--delete-pt1-mission',
        type=str,
        metavar='MISSIONPATH',
        action='append',
        help="""Deletes all stored info about the specified mission in
                Playthrough 1 (Normal).  Will only work on sidemissions.
                Use bl3-save-info's --mission-paths to see the correct
                mission path to use here.  This option can be specified
                more than once.""")

    parser.add_argument(
        '--delete-pt2-mission',
        type=str,
        metavar='MISSIONPATH',
        action='append',
        help="""Deletes all stored info about the specified mission in
                Playthrough 2 (TVHM).  Will only work on sidemissions.
                Use bl3-save-info's --mission-paths to see the correct
                mission path to use here.  This option can be specified
                more than once.""")

    parser.add_argument(
        '--clear-bloody-harvest',
        action='store_true',
        help='Clear Bloody Harvest challenge state',
    )

    parser.add_argument(
        '--clear-broken-hearts',
        action='store_true',
        help='Clear Broken Hearts challenge state',
    )

    parser.add_argument(
        '--clear-cartels',
        action='store_true',
        help='Clear Revenge of the Cartels challenge state',
    )

    parser.add_argument(
        '--clear-all-events',
        action='store_true',
        help='Clear all seasonal event challenge states',
    )

    # Positional args
    parser.add_argument(
        'input_filename',
        help='Input filename',
    )

    parser.add_argument(
        'output_filename',
        help='Output filename',
    )

    # Parse args
    args = parser.parse_args()
    if args.level is not None:
        if args.level < 1 or args.level > bl3save.max_supported_level:
            raise argparse.ArgumentTypeError(
                'Valid level range is 1 through {} (currently known in-game max of {})'
                .format(
                    bl3save.max_supported_level,
                    bl3save.max_level,
                ))
        if args.level > bl3save.max_level:
            print(
                'WARNING: Setting character level to {}, when {} is the currently-known max'
                .format(
                    args.level,
                    bl3save.max_level,
                ))

    # Expand any of our "all" unlock actions
    if 'all' in args.unlock:
        args.unlock = {k: True for k in unlock_choices}
    elif 'allslots' in args.unlock:
        args.unlock['gunslots'] = True
        args.unlock['artifactslot'] = True
        args.unlock['comslot'] = True

    # Make sure we're not trying to clear and unlock THVM at the same time
    if 'tvhm' in args.unlock and args.unfinish_nvhm:
        raise argparse.ArgumentTypeError(
            'Cannot both unlock TVHM and un-finish NVHM')

    # Set max level arg
    if args.level_max:
        args.level = bl3save.max_level

    # Set max mayhem arg
    if args.item_mayhem_max:
        args.item_mayhem_levels = bl3save.mayhem_max

    # Check item level.  The max storeable in the serial number is 127, but the
    # effective limit in-game is 100, thanks to MaxGameStage attributes.  We
    # could use `bl3save.max_level` here, too, of course, but in the event that
    # I don't get this updated in a timely fashion, having it higher would let
    # this util potentially continue to be able to level up gear.
    if args.item_levels:
        if args.item_levels < 1 or args.item_levels > 100:
            raise argparse.ArgumentTypeError(
                'Valid item level range is 1 through 100')
        if args.item_levels > bl3save.max_level:
            print(
                'WARNING: Setting item levels to {}, when {} is the currently-known max'
                .format(
                    args.item_levels,
                    bl3save.max_level,
                ))

    # Check to make sure that any deleted missions are not plot missions
    for arg in [args.delete_pt1_mission, args.delete_pt2_mission]:
        if arg is not None:
            for mission in arg:
                if mission.lower() in plot_missions:
                    raise argparse.ArgumentTypeError(
                        'Plot mission cannot be deleted: {}'.format(mission))

    # Check for overwrite warnings
    if os.path.exists(args.output_filename) and not args.force:
        if args.output_filename == args.input_filename:
            confirm_msg = 'Really overwrite {} with specified changes (no backup will be made)'.format(
                args.output_filename)
        else:
            confirm_msg = '{} already exists.  Overwrite'.format(
                args.output_filename)
        sys.stdout.write('WARNING: {} [y/N]? '.format(confirm_msg))
        sys.stdout.flush()
        response = sys.stdin.readline().strip().lower()
        if len(response) == 0 or response[0] != 'y':
            print('Aborting!')
            sys.exit(1)
        print('')

    # Now load the savegame
    if not args.quiet:
        print('Loading {}'.format(args.input_filename))
    save = BL3Save(args.input_filename)
    if not args.quiet:
        print('')

    # Some argument interactions we should check on
    if args.copy_nvhm:
        if save.get_playthroughs_completed() < 1:
            if 'tvhm' not in args.unlock:
                args.unlock['tvhm'] = True

    # If we've been told to copy TVHM state to NVHM, make sure we have TVHM data.
    # TODO: need to check this out
    if args.copy_tvhm:
        if save.get_playthroughs_completed() < 1:
            raise argparse.ArgumentTypeError(
                'TVHM State not found to copy in {}'.format(
                    args.input_filename))

    # Check to see if we have any changes to make
    have_changes = any([
        args.name,
        args.save_game_id is not None,
        args.randomize_guid,
        args.zero_guardian_rank,
        args.level is not None,
        args.mayhem is not None,
        args.mayhem_seed is not None,
        args.money is not None,
        args.eridium is not None,
        args.clear_takedowns,
        len(args.unlock) > 0,
        args.copy_nvhm,
        args.copy_tvhm,
        args.import_items,
        args.items_to_char,
        args.item_levels,
        args.unfinish_nvhm,
        args.item_mayhem_levels is not None,
        args.delete_pt1_mission is not None,
        args.delete_pt2_mission is not None,
        args.clear_bloody_harvest,
        args.clear_broken_hearts,
        args.clear_cartels,
        args.clear_all_events,
    ])

    # Make changes
    if have_changes:

        if not args.quiet:
            print('Making requested changes...')
            print('')

        # Char Name
        if args.name:
            if not args.quiet:
                print(' - Setting Character Name to: {}'.format(args.name))
            save.set_char_name(args.name)

        # Savegame ID
        if args.save_game_id is not None:
            if not args.quiet:
                print(' - Setting Savegame ID to: {}'.format(
                    args.save_game_id))
            save.set_savegame_id(args.save_game_id)

        # Savegame GUID
        if args.randomize_guid:
            if not args.quiet:
                print(' - Randomizing savegame GUID')
            save.randomize_guid()

        # Zeroing Guardian Rank
        if args.zero_guardian_rank:
            if not args.quiet:
                print(' - Zeroing Guardian Rank')
            save.zero_guardian_rank()

        # Mayhem Level
        if args.mayhem is not None:
            if not args.quiet:
                print(' - Setting Mayhem Level to: {}'.format(args.mayhem))
            save.set_all_mayhem_level(args.mayhem)
            if args.mayhem > 0:
                if not args.quiet:
                    print('   - Also ensuring that Mayhem Mode is unlocked')
                save.unlock_challenge(bl3save.MAYHEM)

        # Mayhem Seed
        if args.mayhem_seed is not None:
            if not args.quiet:
                print(' - Setting Mayhem Random Seed to: {}'.format(
                    args.mayhem_seed))
            save.set_all_mayhem_seeds(args.mayhem_seed)

        # Level
        if args.level is not None:
            if not args.quiet:
                print(' - Setting Character Level to: {}'.format(args.level))
            save.set_level(args.level)

        # Money
        if args.money is not None:
            if not args.quiet:
                print(' - Setting Money to: {}'.format(args.money))
            save.set_money(args.money)

        # Eridium
        if args.eridium is not None:
            if not args.quiet:
                print(' - Setting Eridium to: {}'.format(args.eridium))
            save.set_eridium(args.eridium)

        # Clearing Takedown Discovery
        if args.clear_takedowns:
            if not args.quiet:
                print(' - Clearing Takedown Discovery missions')
            save.clear_takedown_discovery()

        # Deleting missions
        for label, pt, arg in [
            ('Normal/NVHM', 0, args.delete_pt1_mission),
            ('TVHM', 1, args.delete_pt2_mission),
        ]:
            if arg is not None:
                for mission in arg:
                    if not args.quiet:
                        print(' - Deleting {} mission: {}'.format(
                            label, mission))
                    if not save.delete_mission(pt, mission):
                        if not args.quiet:
                            print(
                                '   NOTE: Could not find {} mission to delete: {}'
                                .format(
                                    label,
                                    mission,
                                ))

        # Clearing seasonal event status
        if args.clear_bloody_harvest or args.clear_all_events:
            if not args.quiet:
                print(' - Clearing Bloody Harvest challenge state')
            save.clear_bloody_harvest()

        if args.clear_broken_hearts or args.clear_all_events:
            if not args.quiet:
                print(' - Clearing Broken Hearts challenge state')
            save.clear_broken_hearts()

        if args.clear_cartels or args.clear_all_events:
            if not args.quiet:
                print(' - Clearing Cartels challenge state')
            save.clear_cartels()

        # Unlocks
        if len(args.unlock) > 0:
            if not args.quiet:
                print(' - Processing Unlocks:')

            # Ammo
            if 'ammo' in args.unlock:
                if not args.quiet:
                    print('   - Ammo SDUs (and setting ammo to max)')
                save.set_max_sdus(bl3save.ammo_sdus)
                save.set_max_ammo()

            # Backpack
            if 'backpack' in args.unlock:
                if not args.quiet:
                    print('   - Backpack SDUs')
                save.set_max_sdus([bl3save.SDU_BACKPACK])

            # Eridian Analyzer
            if 'analyzer' in args.unlock:
                if not args.quiet:
                    print('   - Eridian Analyzer')
                save.unlock_challenge(bl3save.ERIDIAN_ANALYZER)

            # Eridian Resonator
            if 'resonator' in args.unlock:
                if not args.quiet:
                    print('   - Eridian Resonator')
                save.unlock_challenge(bl3save.ERIDIAN_RESONATOR)

            # Gun Slots
            if 'gunslots' in args.unlock:
                if not args.quiet:
                    print('   - Weapon Slots (3+4)')
                save.unlock_slots([bl3save.WEAPON3, bl3save.WEAPON4])

            # Artifact Slot
            if 'artifactslot' in args.unlock:
                if not args.quiet:
                    print('   - Artifact Inventory Slot')
                save.unlock_slots([bl3save.ARTIFACT])

            # COM Slot
            if 'comslot' in args.unlock:
                if not args.quiet:
                    print('   - COM Inventory Slot')
                save.unlock_slots([bl3save.COM])

            # Vehicles
            if 'vehicles' in args.unlock:
                if not args.quiet:
                    print('   - Vehicles (and parts)')
                save.unlock_vehicle_chassis()
                save.unlock_vehicle_parts()
                if not args.quiet and not save.has_vehicle_chassis(
                        bl3save.jetbeast_main_chassis):
                    print(
                        '     - NOTE: The default Jetbeast chassis will be unlocked automatically by the game'
                    )

            # Vehicle Skins
            if 'vehicleskins' in args.unlock:
                if not args.quiet:
                    print('   - Vehicle Skins')
                save.unlock_vehicle_skins()

            # TVHM
            if 'tvhm' in args.unlock:
                if not args.quiet:
                    print('   - TVHM')
                save.set_playthroughs_completed(1)

            # Eridian Cube puzzle
            if 'cubepuzzle' in args.unlock:
                if not args.quiet:
                    print('   - Eridian Cube Puzzle')
                save.unlock_cube_puzzle()

        # Import Items
        if args.import_items:
            cli_common.import_items(
                args.import_items,
                save.create_new_item_encoded,
                save.add_item,
                file_csv=args.csv,
                allow_fabricator=args.allow_fabricator,
                quiet=args.quiet,
            )

        # Setting item levels.  Keep in mind that we'll want to do this *after*
        # various of the actions above.  If we've been asked to up the level of
        # the character, we'll want items to follow suit, and if we've been asked
        # to change the level of items, we'll want to do it after the item import.
        if args.items_to_char or args.item_levels:
            if args.items_to_char:
                to_level = save.get_level()
            else:
                to_level = args.item_levels
            cli_common.update_item_levels(
                save.get_items(),
                to_level,
                quiet=args.quiet,
            )

        # Item Mayhem level
        if args.item_mayhem_levels is not None:
            cli_common.update_item_mayhem_levels(
                save.get_items(),
                args.item_mayhem_levels,
                quiet=args.quiet,
            )

        # Copying NVHM/TVHM state (or otherwise fiddle with playthroughs)
        if args.copy_nvhm:
            if not args.quiet:
                print(' - Copying NVHM state to TVHM')
            save.copy_playthrough_data()
        elif args.copy_tvhm:
            if not args.quiet:
                print(' - Copying TVHM state to NVHM')
            save.copy_playthrough_data(from_pt=1, to_pt=0)
        elif args.unfinish_nvhm:
            if not args.quiet:
                print(' - Un-finishing NVHM state entirely')
            # ... or clearing TVHM state entirely.
            save.set_playthroughs_completed(0)
            save.clear_playthrough_data(1)

        # Newline at the end of all this.
        if not args.quiet:
            print('')

    # Write out
    if args.output == 'savegame':
        save.save_to(args.output_filename)
        if not args.quiet:
            print('Wrote savegame to {}'.format(args.output_filename))
    elif args.output == 'protobuf':
        save.save_protobuf_to(args.output_filename)
        if not args.quiet:
            print('Wrote protobuf to {}'.format(args.output_filename))
    elif args.output == 'json':
        save.save_json_to(args.output_filename)
        if not args.quiet:
            print('Wrote JSON to {}'.format(args.output_filename))
    elif args.output == 'items':
        if args.csv:
            cli_common.export_items_csv(
                save.get_items(),
                args.output_filename,
                quiet=args.quiet,
            )
        else:
            cli_common.export_items(
                save.get_items(),
                args.output_filename,
                quiet=args.quiet,
            )
    else:
        # Not sure how we'd ever get here
        raise Exception('Invalid output format specified: {}'.format(
            args.output))
コード例 #2
0
def main():

    # Arguments
    parser = argparse.ArgumentParser(
            description='Borderlands 3 Savegame Info Dumper v{}'.format(bl3save.__version__),
            )

    parser.add_argument('-V', '--version',
            action='version',
            version='BL3 CLI SaveEdit v{}'.format(bl3save.__version__),
            )

    parser.add_argument('-v', '--verbose',
            action='store_true',
            help='Show all available information',
            )

    parser.add_argument('-i', '--items',
            action='store_true',
            help='Show inventory items',
            )

    parser.add_argument('--all-missions',
            dest='all_missions',
            action='store_true',
            help='Show all missions')

    parser.add_argument('--mission-paths',
            action='store_true',
            help='Display raw mission paths when reporting on missions')

    parser.add_argument('--all-challenges',
            dest='all_challenges',
            action='store_true',
            help='Show all challenges')

    parser.add_argument('--fast-travel',
            dest='fast_travel',
            action='store_true',
            help='Show all unlocked Fast Travel stations')

    parser.add_argument('filename',
            help='Filename to process',
            )

    args = parser.parse_args()

    # Load the save
    save = BL3Save(args.filename)

    # Character name
    print('Character: {}'.format(save.get_char_name()))

    # Savegame ID
    print('Savegame ID: {}'.format(save.get_savegame_id()))

    # Savegame GUID
    print('Savegame GUID: {}'.format(save.get_savegame_guid()))

    # Pet Names
    petnames = save.get_pet_names(True)
    if len(petnames) > 0:
        for (pet_type, pet_name) in petnames.items():
            print(' - {} Name: {}'.format(pet_type, pet_name))

    # Class
    print('Player Class: {}'.format(save.get_class(True)))

    # XP/Level
    print('XP: {}'.format(save.get_xp()))
    print('Level: {}'.format(save.get_level()))
    print('Guardian Rank: {}'.format(save.get_guardian_rank()))

    # Currencies
    print('Money: {}'.format(save.get_money()))
    print('Eridium: {}'.format(save.get_eridium()))

    # Playthroughs
    print('Playthroughs Completed: {}'.format(save.get_playthroughs_completed()))

    # Playthrough-specific Data
    for pt, (mayhem,
                mayhem_seed,
                mapname,
                stations,
                active_missions,
                active_missions_obj,
                completed_missions,
                completed_missions_obj,
                ) in enumerate(itertools.zip_longest(
            save.get_pt_mayhem_levels(),
            save.get_pt_mayhem_seeds(),
            save.get_pt_last_maps(True),
            save.get_pt_active_ft_station_lists(),
            save.get_pt_active_mission_lists(True),
            save.get_pt_active_mission_lists(),
            save.get_pt_completed_mission_lists(True),
            save.get_pt_completed_mission_lists(),
            )):

        print('Playthrough {} Info:'.format(pt+1))

        # Mayhem
        if mayhem is not None:
            print(' - Mayhem Level: {}'.format(mayhem))
        if mayhem_seed is not None:
            print(' - Mayhem Random Seed: {}'.format(mayhem_seed))

        # Map
        if mapname is not None:
            print(' - In Map: {}'.format(mapname))

        # FT Stations
        if args.verbose or args.fast_travel:
            if stations is not None:
                if len(stations) == 0:
                    print(' - No Active Fast Travel Stations')
                else:
                    print(' - Active Fast Travel Stations:'.format(pt+1))
                    for station in stations:
                        print('   - {}'.format(station))

        # Missions
        if active_missions is not None:
            if len(active_missions) == 0:
                print(' - No Active Missions')
            else:
                print(' - Active Missions:')
                for mission, obj_name in sorted(zip(active_missions, active_missions_obj)):
                    print('   - {}'.format(mission))
                    if args.mission_paths:
                        print('     {}'.format(obj_name))

        # Completed mission count
        if completed_missions is not None:
            print(' - Missions completed: {}'.format(len(completed_missions)))

            # Show all missions if need be
            if args.verbose or args.all_missions:
                for mission, obj_name in sorted(zip(completed_missions, completed_missions_obj)):
                    print('   - {}'.format(mission))
                    if args.mission_paths:
                        print('     {}'.format(obj_name))

            # "Important" missions - I'm torn as to whether or not this kind of thing
            # should be in bl3save.py itself, or at least some constants in __init__.py
            mission_set = set(completed_missions)
            importants = []
            if 'Divine Retribution' in mission_set:
                importants.append('Main Game')
            if 'All Bets Off' in mission_set:
                importants.append('DLC1 - Moxxi\'s Heist of the Handsome Jackpot')
            if 'The Call of Gythian' in mission_set:
                importants.append('DLC2 - Guns, Love, and Tentacles')
            if 'Riding to Ruin' in mission_set:
                importants.append('DLC3 - Bounty of Blood')
            if 'Locus of Rage' in mission_set:
                importants.append('DLC4 - Psycho Krieg and the Fantastic Fustercluck')
            if "Mysteriouslier: Horror at Scryer's Crypt" in mission_set:
                importants.append('DLC6 - Director\'s Cut')
            if len(importants) > 0:
                print(' - Mission Milestones:')
                for important in importants:
                    print('   - Finished: {}'.format(important))

    # Inventory Slots that we care about
    print('Unlockable Inventory Slots:')
    for slot in [bl3save.WEAPON3, bl3save.WEAPON4, bl3save.COM, bl3save.ARTIFACT]:
        print(' - {}: {}'.format(
            bl3save.slot_to_eng[slot],
            save.get_equip_slot(slot).enabled(),
            ))

    # Inventory
    if args.verbose or args.items:
        items = save.get_items()
        if len(items) == 0:
            print('Nothing in Inventory')
        else:
            print('Inventory:')
            to_report = []
            for item in items:
                if item.eng_name:
                    to_report.append(' - {} ({}): {}'.format(item.eng_name, item.get_level_eng(), item.get_serial_base64()))
                else:
                    to_report.append(' - unknown item: {}'.format(item.get_serial_base64()))
            for line in sorted(to_report):
                print(line)

    # Equipped Items
    if args.verbose or args.items:
        items = save.get_equipped_items(True)
        if any(items.values()):
            print('Equipped Items:')
            to_report = []
            for (slot, item) in items.items():
                if item:
                    if item.eng_name:
                        to_report.append(' - {}: {} ({}): {}'.format(slot, item.eng_name, item.get_level_eng(), item.get_serial_base64()))
                    else:
                        to_report.append(' - {}: unknown item: {}'.format(slot, item.get_serial_base64()))
            for line in sorted(to_report):
                print(line)
        else:
            print('No Equipped Items')

    # SDUs
    sdus = save.get_sdus_with_max(True)
    if len(sdus) == 0:
        print('No SDUs Purchased')
    else:
        print('SDUs:')
        for sdu, (count, max_sdus) in sdus.items():
            print(' - {}: {}/{}'.format(sdu, count, max_sdus))

    # Ammo
    print('Ammo Pools:')
    for ammo, count in save.get_ammo_counts(True).items():
        print(' - {}: {}'.format(ammo, count))

    # Challenges
    print('Challenges we care about:')
    for challenge, status in save.get_interesting_challenges(True).items():
        print(' - {}: {}'.format(challenge, status))

    # "raw" Challenges
    if args.verbose or args.all_challenges:
        print('All Challenges:')
        for challenge in save.get_all_challenges_raw():
            print(' - {} (Completed: {}, Counter: {}, Progress: {})'.format(
                challenge.challenge_class_path,
                challenge.currently_completed,
                challenge.progress_counter,
                challenge.completed_progress_level,
                ))

    # Vehicle unlocks
    print('Unlocked Vehicle Parts:')
    for vehicle, chassis_count in save.get_vehicle_chassis_counts().items():
        eng = bl3save.vehicle_to_eng[vehicle]
        print(' - {} - Chassis (wheels): {}/{}, Parts: {}/{}, Skins: {}/{}'.format(
            eng,
            chassis_count, len(bl3save.vehicle_chassis[vehicle]),
            save.get_vehicle_part_count(vehicle), len(bl3save.vehicle_parts[vehicle]),
            save.get_vehicle_skin_count(vehicle), len(bl3save.vehicle_skins[vehicle]),
            ))
コード例 #3
0
def main():

    # Set up args
    parser = argparse.ArgumentParser(
        description='Copy BL3 Playthrough Data v{}'.format(
            bl3save.__version__), )

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version='BL3 CLI SaveEdit v{}'.format(bl3save.__version__),
    )

    parser.add_argument('-f',
                        '--from',
                        dest='filename_from',
                        type=str,
                        required=True,
                        help='Filename to copy playthrough data from')

    parser.add_argument('-t',
                        '--to',
                        dest='filename_to',
                        type=str,
                        required=True,
                        help='Filename to copy playthrough data to')

    parser.add_argument(
        '-p',
        '--playthrough',
        type=int,
        default=0,
        help='Playthrough to copy (defaults to all found playthroughs)')

    parser.add_argument('-c',
                        '--clobber',
                        action='store_true',
                        help='Clobber (overwrite) files without asking')

    # Parse args
    args = parser.parse_args()

    # Make sure that files exist
    if not os.path.exists(args.filename_from):
        raise Exception('From filename {} does not exist'.format(
            args.filename_from))
    if not os.path.exists(args.filename_to):
        raise Exception('From filename {} does not exist'.format(
            args.filename_to))
    if args.filename_from == args.filename_to:
        raise argparse.ArgumentTypeError(
            'To and From filenames cannot be the same')

    # Load the from file and do a quick sanity check
    save_from = BL3Save(args.filename_from)
    total_from_playthroughs = save_from.get_max_playthrough_with_data() + 1
    if args.playthrough > 0 and total_from_playthroughs < args.playthrough:
        raise Exception('{} does not have Playthrough {} data'.format(
            args.filename_from, args.playthrough))

    # Get a list of playthroughs that we'll process
    if args.playthrough == 0:
        playthroughs = list(range(total_from_playthroughs))
    else:
        playthroughs = [args.playthrough - 1]

    # Make sure that we can load our "to" file as well, and do a quick sanity check.
    # Given that there's only NVHM/TVHM at the moment, this should never actually
    # trigger, but I'd accidentally unlocked a third playthrough on my savegame
    # archives, so I was able to test it out regardless, in the event that BL3 ever
    # gets a third playthrough.
    save_to = BL3Save(args.filename_to)
    if args.playthrough > 0:
        total_to_playthroughs = save_to.get_max_playthrough_with_data() + 1
        if total_to_playthroughs == 1:
            plural = ''
        else:
            plural = 's'
        if total_to_playthroughs < args.playthrough - 1:
            raise Exception(
                'Cannot copy playthrough {} data to {}; only has {} playthrough{} currently'
                .format(
                    args.playthrough,
                    args.filename_to,
                    total_to_playthroughs,
                    plural,
                ))

    # If we've been given an info file, check to see if it exists
    if not args.clobber:
        if len(playthroughs) == 1:
            plural = ''
        else:
            plural = 's'

        print(
            'WARNING: Playthrough{} {} from {} will be copied into {}'.format(
                plural,
                '+'.join([str(p + 1) for p in playthroughs]),
                args.filename_from,
                args.filename_to,
            ))
        sys.stdout.write('Continue [y/N]? ')
        sys.stdout.flush()
        response = sys.stdin.readline().strip().lower()
        if response == 'y':
            pass
        else:
            print('')
            print('Aborting!')
            print('')
            sys.exit(1)

    # If we get here, we're good to go
    for pt in playthroughs:
        save_to.copy_playthrough_data(from_obj=save_from, from_pt=pt, to_pt=pt)

    # Update our Completed Playthroughs if we need to, so that the copied
    # playthroughs are actually active
    required_completion = max(playthroughs)
    if save_to.get_playthroughs_completed() < required_completion:
        save_to.set_playthroughs_completed(required_completion)

    # Write back out to the file
    save_to.save_to(args.filename_to)

    # Report!
    print('')
    print('Done!')
    print('')
コード例 #4
0
def main():

    # Set up args
    parser = argparse.ArgumentParser(
        description='Process Mod-Testing Borderlands 3 Archive Savegames v{}'.
        format(bl3save.__version__), )

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version='BL3 CLI SaveEdit v{}'.format(bl3save.__version__),
    )

    group = parser.add_mutually_exclusive_group()

    group.add_argument('-f',
                       '--filename',
                       type=str,
                       help='Specific filename to process')

    group.add_argument('-d',
                       '--directory',
                       type=str,
                       help='Directory to process (defaults to "step")')

    parser.add_argument('-i',
                        '--info',
                        type=str,
                        help='HTML File to write output summary to')

    parser.add_argument('-o',
                        '--output',
                        type=str,
                        required=True,
                        help='Output filename/directory to use')

    parser.add_argument('-c',
                        '--clobber',
                        action='store_true',
                        help='Clobber (overwrite) files without asking')

    # Parse args
    args = parser.parse_args()
    if not args.filename and not args.directory:
        args.directory = 'step'

    # Construct a list of filenames
    targets = []
    if args.directory:
        for filename in sorted(os.listdir(args.directory)):
            if '.sav' in filename:
                targets.append(os.path.join(args.directory, filename))
    else:
        targets.append(args.filename)

    # If we're a directory, make sure it exists
    if not os.path.exists(args.output):
        os.mkdir(args.output)

    # If we've been given an info file, check to see if it exists
    if args.info and not args.clobber and os.path.exists(args.info):
        sys.stdout.write(
            'WARNING: {} already exists.  Overwrite [y/N/a/q]? '.format(
                args.info))
        sys.stdout.flush()
        response = sys.stdin.readline().strip().lower()
        if response == 'y':
            pass
        elif response == 'n':
            args.info = None
        elif response == 'a':
            args.clobber = True
        elif response == 'q':
            sys.exit(1)
        else:
            # Default to No
            args.info = None

    # Open the info file, if we have one.
    if args.info:
        idf = open(args.info, 'w')

    # Now loop through and process
    files_written = 0
    for filename in targets:

        # Figure out an output filename
        if args.filename:
            base_filename = args.filename
            output_filename = args.output
        else:
            base_filename = filename.split('/')[-1]
            output_filename = os.path.join(args.output, base_filename)

        # See if the path already exists
        if os.path.exists(output_filename) and not args.clobber:
            sys.stdout.write(
                'WARNING: {} already exists.  Overwrite [y/N/a/q]? '.format(
                    output_filename))
            sys.stdout.flush()
            response = sys.stdin.readline().strip().lower()
            if response == 'y':
                pass
            elif response == 'n':
                continue
            elif response == 'a':
                args.clobber = True
            elif response == 'q':
                break
            else:
                # Default to No
                response = 'n'

        # Load!
        print('Processing: {}'.format(filename))
        save = BL3Save(filename)

        # Write to our info file, if we have it
        if args.info:

            # Write out the row
            print('<tr class="row{}">'.format(files_written % 2), file=idf)
            print('<td class="filename"><a href="bl3/{}">{}</a></td>'.format(
                base_filename, base_filename),
                  file=idf)
            print('<td class="in_map">{}</td>'.format(
                save.get_pt_last_map(0, True)),
                  file=idf)
            missions = save.get_pt_active_mission_list(0, True)
            if len(missions) == 0:
                print('<td class="empty_missions">&nbsp;</td>', file=idf)
            else:
                print('<td class="active_missions">', file=idf)
                print('<ul>', file=idf)
                for mission in sorted(missions):
                    print('<li>{}</li>'.format(mission), file=idf)
                print('</ul>', file=idf)
                print('</td>', file=idf)
            print('</tr>', file=idf)

        # May as well force the name, while we're at it
        save.set_char_name("BL3 Savegame Archive")

        # Max XP
        save.set_level(bl3save.max_level)

        # Max SDUs
        save.set_max_sdus()

        # Max Ammo
        save.set_max_ammo()

        # Unlock all inventory slots
        save.unlock_slots()

        # Unlock PT2
        # (In the original runthrough which I've already checked in, I'd accidentally set
        # this to 2.  Whoops!  Doesn't seem to matter, so whatever.)
        save.set_playthroughs_completed(1)

        # Remove our bogus third playthrough, if we're processing a file which happens
        # to still have that (thanks to our faux pas, above)
        if save.get_max_playthrough_with_data() > 1:
            save.clear_playthrough_data(2)

        # Copy mission/FT/location/mayhem status from PT1 to PT2
        save.copy_playthrough_data()

        # Inventory - force our testing gear
        # Gear data just taken from my modtest char.  Level 57 Mayhem 10, though
        # they'll get upgraded if needed, below.
        craders = 'BL3(AwAAAADHQ4C6yJOBkHsckEekyWhISinQpbNyysgdQgAAAAAAADIgAA==)'
        transformer = 'BL3(AwAAAACSdIC2t9hAkysShLxMKkMEAA==)'
        save.overwrite_item_in_slot_encoded(bl3save.WEAPON1, craders)
        save.overwrite_item_in_slot_encoded(bl3save.SHIELD, transformer)

        # Bring testing gear up to our max level, while we're at it.
        for item in save.get_items():
            if item.level != bl3save.max_level:
                item.level = bl3save.max_level
            if item.mayhem_level != bl3save.mayhem_max:
                item.mayhem_level = bl3save.mayhem_max

        # Wipe guardian rank
        save.zero_guardian_rank()

        # Write out
        save.save_to(output_filename)
        files_written += 1

    if args.filename:
        if files_written == 1:
            print('Done!  Wrote to {}'.format(args.output))
    else:
        if files_written == 1:
            plural = ''
        else:
            plural = 's'
        print('Done!  Wrote {} file{} to {}'.format(files_written, plural,
                                                    args.output))

    if args.info:
        print('Wrote HTML summary to {}'.format(args.info))
        idf.close()
コード例 #5
0
def main():

    # Set up args
    parser = argparse.ArgumentParser(
        description='Import BL3 Savegame JSON v{}'.format(
            bl3save.__version__), )

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version='BL3 CLI SaveEdit v{}'.format(bl3save.__version__),
    )

    parser.add_argument('-j',
                        '--json',
                        type=str,
                        required=True,
                        help='Filename containing JSON to import')

    parser.add_argument('-t',
                        '--to-filename',
                        dest='filename_to',
                        type=str,
                        required=True,
                        help='Filename to import JSON into')

    parser.add_argument('-c',
                        '--clobber',
                        action='store_true',
                        help='Clobber (overwrite) files without asking')

    # Parse args
    args = parser.parse_args()

    # Make sure that files exist
    if not os.path.exists(args.filename_to):
        raise Exception('Filename {} does not exist'.format(args.filename_to))
    if not os.path.exists(args.json):
        raise Exception('Filename {} does not exist'.format(args.json))

    # Load the savegame file
    save_file = BL3Save(args.filename_to)

    # Load the JSON file and import (so we know it's valid before
    # we ask for confirmation)
    with open(args.json, 'rt') as df:
        save_file.import_json(df.read())

    # Ask for confirmation
    if not args.clobber:
        sys.stdout.write('Really import JSON from {} into {} [y/N]? '.format(
            args.json,
            args.filename_to,
        ))
        sys.stdout.flush()
        response = sys.stdin.readline().strip().lower()
        if response == 'y':
            pass
        else:
            print('')
            print('Aborting!')
            print('')
            sys.exit(1)

    # ... and save.
    save_file.save_to(args.filename_to)

    # Report!
    print('')
    print('Done!')
    print('')
コード例 #6
0
ファイル: cli_edit.py プロジェクト: psiberx/bl3-cli-saveedit
def main():

    # Set up args
    parser = argparse.ArgumentParser(
            description='Borderlands 3 CLI Savegame Editor v{} (PC Only)'.format(bl3save.__version__),
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            epilog="""
                The default output type of "savegame" will output theoretically-valid
                savegames which can be loaded into BL3.  The output type "protobuf"
                will save out the extracted, decrypted protobufs.  The output
                type "json" will output a JSON-encoded version of the protobufs
                in question.  The output type "items" will output a text
                file containing base64-encoded representations of the user's
                inventory.  These can be read back in using the -i/--import-items
                option.  Note that these are NOT the same as the item strings used
                by the BL3 Memory Editor.
            """
            )

    parser.add_argument('-V', '--version',
            action='version',
            version='BL3 CLI SaveEdit v{}'.format(bl3save.__version__),
            )

    parser.add_argument('-o', '--output',
            choices=['savegame', 'protobuf', 'json', 'items'],
            default='savegame',
            help='Output file format',
            )

    parser.add_argument('-f', '--force',
            action='store_true',
            help='Force output file to overwrite',
            )

    parser.add_argument('-q', '--quiet',
            action='store_true',
            help='Supress all non-essential output')

    # Actual changes the user can request
    parser.add_argument('--name',
            type=str,
            help='Set the name of the character',
            )

    parser.add_argument('--save-game-id',
            dest='save_game_id',
            type=int,
            help='Set the save game slot ID (possibly not actually ever needed)',
            )

    levelgroup = parser.add_mutually_exclusive_group()

    levelgroup.add_argument('--level',
            type=int,
            help='Set the character to this level (from 1 to {})'.format(bl3save.max_level),
            )

    levelgroup.add_argument('--level-max',
            dest='level_max',
            action='store_true',
            help='Set the character to max level ({})'.format(bl3save.max_level),
            )

    itemlevelgroup = parser.add_mutually_exclusive_group()

    itemlevelgroup.add_argument('--items-to-char',
            dest='items_to_char',
            action='store_true',
            help='Set all inventory items to the level of the character')

    itemlevelgroup.add_argument('--item-levels',
            dest='item_levels',
            type=int,
            help='Set all inventory items to the specified level')

    itemmayhemgroup = parser.add_mutually_exclusive_group()

    itemmayhemgroup.add_argument('--item-mayhem-max',
            dest='item_mayhem_max',
            action='store_true',
            help='Set all inventory items to the maximum Mayhem level ({})'.format(bl3save.mayhem_max))

    itemmayhemgroup.add_argument('--item-mayhem-levels',
            dest='item_mayhem_levels',
            type=int,
            choices=range(bl3save.mayhem_max+1),
            help='Set all inventory items to the specified Mayhem level (0 to remove)')

    parser.add_argument('--weapon-anointment',
            dest='weapon_anointment',
            type=str,
            help='Set specified anointment for all weapons in the inventory')

    parser.add_argument('--shield-anointment',
            dest='shield_anointment',
            type=str,
            help='Set specified anointment for all shields in the inventory')

    parser.add_argument('--grenade-mod-anointment',
            dest='grenade_mod_anointment',
            type=str,
            help='Set specified anointment for all grenade mods in the inventory')

    parser.add_argument('--mayhem',
            type=int,
            choices=range(11),
            help='Set the mayhem mode for all playthroughs (mostly useful for Normal mode)',
            )

    parser.add_argument('--money',
            type=int,
            help='Set money value',
            )

    parser.add_argument('--eridium',
            type=int,
            help='Set Eridium value',
            )

    unlock_choices = [
            'ammo', 'backpack',
            'analyzer', 'resonator',
            'gunslots', 'artifactslot', 'comslot', 'allslots',
            'tvhm',
            'vehicles', 'vehicleskins',
            ]
    parser.add_argument('--unlock',
            action=cli_common.DictAction,
            choices=unlock_choices + ['all'],
            default={},
            help='Game features to unlock',
            )

    tvhmgroup = parser.add_mutually_exclusive_group()

    tvhmgroup.add_argument('--copy-nvhm',
            dest='copy_nvhm',
            action='store_true',
            help='Copies NVHM/Normal state to TVHM',
            )

    tvhmgroup.add_argument('--unfinish-nvhm',
            dest='unfinish_nvhm',
            action='store_true',
            help='"Un-finishes" the game: remove all TVHM data and set Playthrough 1 to Not Completed',
            )

    parser.add_argument('-i', '--import-items',
            dest='import_items',
            type=str,
            help='Import items from file',
            )

    parser.add_argument('--allow-fabricator',
            dest='allow_fabricator',
            action='store_true',
            help='Allow importing Fabricator when importing items from file',
            )

    # Positional args
    parser.add_argument('input_filename',
            help='Input filename',
            )

    parser.add_argument('output_filename',
            help='Output filename',
            )

    # Parse args
    args = parser.parse_args()
    if args.level is not None and (args.level < 1 or args.level > bl3save.max_level):
        raise argparse.ArgumentTypeError('Valid level range is 1 through {}'.format(bl3save.max_level))

    # Expand any of our "all" unlock actions
    if 'all' in args.unlock:
        args.unlock = {k: True for k in unlock_choices}
    elif 'allslots' in args.unlock:
        args.unlock['gunslots'] = True
        args.unlock['artifactslot'] = True
        args.unlock['comslot'] = True

    # Make sure we're not trying to clear and unlock THVM at the same time
    if 'tvhm' in args.unlock and args.unfinish_nvhm:
        raise argparse.ArgumentTypeError('Cannot both unlock TVHM and un-finish NVHM')

    # Set max level arg
    if args.level_max:
        args.level = bl3save.max_level

    # Set max mayhem arg
    if args.item_mayhem_max:
        args.item_mayhem_levels = bl3save.mayhem_max

    # Check item level.  The max storeable in the serial number is 127, but the
    # effective limit in-game is 100, thanks to MaxGameStage attributes.  We
    # could use `bl3save.max_level` here, too, of course, but in the event that
    # I don't get this updated in a timely fashion, having it higher would let
    # this util potentially continue to be able to level up gear.
    if args.item_levels:
        if args.item_levels < 1 or args.item_levels > 100:
            raise argparse.ArgumentTypeError('Valid item level range is 1 through 100')

    # Check for overwrite warnings
    if os.path.exists(args.output_filename) and not args.force:
        sys.stdout.write('WARNING: {} already exists.  Overwrite [y/N]? '.format(args.output_filename))
        sys.stdout.flush()
        response = sys.stdin.readline().strip().lower()
        if len(response) == 0 or response[0] != 'y':
            print('Aborting!')
            sys.exit(1)
        print('')

    # Now load the savegame
    if not args.quiet:
        print('Loading {}'.format(args.input_filename))
    save = BL3Save(args.input_filename)
    if not args.quiet:
        print('')

    # Some argument interactions we should check on
    if args.copy_nvhm:
        if save.get_playthroughs_completed() < 1:
            if 'tvhm' not in args.unlock:
                args.unlock['tvhm'] = True

    # Check to see if we have any changes to make
    have_changes = any([
        args.name,
        args.save_game_id is not None,
        args.level is not None,
        args.mayhem is not None,
        args.money is not None,
        args.eridium is not None,
        len(args.unlock) > 0,
        args.copy_nvhm,
        args.import_items,
        args.items_to_char,
        args.item_levels,
        args.unfinish_nvhm,
        args.item_mayhem_levels is not None,
        args.weapon_anointment is not None,
        args.shield_anointment is not None,
        args.grenade_mod_anointment is not None,
        ])

    # Make changes
    if have_changes:

        if not args.quiet:
            print('Making requested changes...')
            print('')

        # Char Name
        if args.name:
            if not args.quiet:
                print(' - Setting Character Name to: {}'.format(args.name))
            save.set_char_name(args.name)

        # Savegame ID
        if args.save_game_id is not None:
            if not args.quiet:
                print(' - Setting Savegame ID to: {}'.format(args.save_game_id))
            save.set_savegame_id(args.save_game_id)

        if args.mayhem is not None:
            if not args.quiet:
                print(' - Setting Mayhem Level to: {}'.format(args.mayhem))
            save.set_all_mayhem_level(args.mayhem)
            if args.mayhem > 0:
                if not args.quiet:
                    print('   - Also ensuring that Mayhem Mode is unlocked')
                save.unlock_challenge(bl3save.MAYHEM)

        # Level
        if args.level is not None:
            if not args.quiet:
                print(' - Setting Character Level to: {}'.format(args.level))
            save.set_level(args.level)

        # Money
        if args.money is not None:
            if not args.quiet:
                print(' - Setting Money to: {}'.format(args.money))
            save.set_money(args.money)

        # Eridium
        if args.eridium is not None:
            if not args.quiet:
                print(' - Setting Eridium to: {}'.format(args.eridium))
            save.set_eridium(args.eridium)

        # Unlocks
        if len(args.unlock) > 0:
            if not args.quiet:
                print(' - Processing Unlocks:')

            # Ammo
            if 'ammo' in args.unlock:
                if not args.quiet:
                    print('   - Ammo SDUs (and setting ammo to max)')
                save.set_max_sdus(bl3save.ammo_sdus)
                save.set_max_ammo()

            # Backpack
            if 'backpack' in args.unlock:
                if not args.quiet:
                    print('   - Backpack SDUs')
                save.set_max_sdus([bl3save.SDU_BACKPACK])

            # Eridian Analyzer
            if 'analyzer' in args.unlock:
                if not args.quiet:
                    print('   - Eridian Analyzer')
                save.unlock_challenge(bl3save.ERIDIAN_ANALYZER)

            # Eridian Resonator
            if 'resonator' in args.unlock:
                if not args.quiet:
                    print('   - Eridian Resonator')
                save.unlock_challenge(bl3save.ERIDIAN_RESONATOR)

            # Gun Slots
            if 'gunslots' in args.unlock:
                if not args.quiet:
                    print('   - Weapon Slots (3+4)')
                save.unlock_slots([bl3save.WEAPON3, bl3save.WEAPON4])

            # Artifact Slot
            if 'artifactslot' in args.unlock:
                if not args.quiet:
                    print('   - Artifact Inventory Slot')
                save.unlock_slots([bl3save.ARTIFACT])

            # COM Slot
            if 'comslot' in args.unlock:
                if not args.quiet:
                    print('   - COM Inventory Slot')
                save.unlock_slots([bl3save.COM])

            # Vehicles
            if 'vehicles' in args.unlock:
                if not args.quiet:
                    print('   - Vehicles (and parts)')
                save.unlock_vehicle_chassis()
                save.unlock_vehicle_parts()

            # Vehicle Skins
            if 'vehicleskins' in args.unlock:
                if not args.quiet:
                    print('   - Vehicle Skins')
                save.unlock_vehicle_skins()

            # TVHM
            if 'tvhm' in args.unlock:
                if not args.quiet:
                    print('   - TVHM')
                save.set_playthroughs_completed(1)

        # Import Items
        if args.import_items:
            cli_common.import_items(args.import_items,
                    save.create_new_item_encoded,
                    save.add_item,
                    allow_fabricator=args.allow_fabricator,
                    quiet=args.quiet,
                    )

        # Setting item levels.  Keep in mind that we'll want to do this *after*
        # various of the actions above.  If we've been asked to up the level of
        # the character, we'll want items to follow suit, and if we've been asked
        # to change the level of items, we'll want to do it after the item import.
        if args.items_to_char or args.item_levels:
            if args.items_to_char:
                to_level = save.get_level()
            else:
                to_level = args.item_levels
            cli_common.update_item_levels(save.get_items(),
                    to_level,
                    quiet=args.quiet,
                    )

        # Item Mayhem level
        if args.item_mayhem_levels is not None:
            cli_common.update_item_mayhem_levels(save.get_items(),
                    args.item_mayhem_levels,
                    quiet=args.quiet,
                    )

        # Weapon Anointment
        if args.weapon_anointment is not None:
            cli_common.update_item_anointments(
                    [item for item in save.get_items() if item.is_weapon()],
                    args.weapon_anointment,
                    quiet=args.quiet,
                    )

        # Shield Anointment
        if args.shield_anointment is not None:
            cli_common.update_item_anointments(
                    [item for item in save.get_items() if item.is_shield()],
                    args.shield_anointment,
                    quiet=args.quiet,
                    )

        # Grenade Mod Anointment
        if args.grenade_mod_anointment is not None:
            cli_common.update_item_anointments(
                    [item for item in save.get_items() if item.is_grenade_mod()],
                    args.grenade_mod_anointment,
                    quiet=args.quiet,
                    )

        # Copying NVHM state
        if args.copy_nvhm:
            if not args.quiet:
                print(' - Copying NVHM state to TVHM')
            save.copy_playthrough_data()
        elif args.unfinish_nvhm:
            if not args.quiet:
                print(' - Un-finishing NVHM state entirely')
            # ... or clearing TVHM state entirely.
            save.set_playthroughs_completed(0)
            save.clear_playthrough_data(1)

        # Newline at the end of all this.
        if not args.quiet:
            print('')

    # Write out
    if args.output == 'savegame':
        save.save_to(args.output_filename)
        if not args.quiet:
            print('Wrote savegame to {}'.format(args.output_filename))
    elif args.output == 'protobuf':
        save.save_protobuf_to(args.output_filename)
        if not args.quiet:
            print('Wrote protobuf to {}'.format(args.output_filename))
    elif args.output == 'json':
        save.save_json_to(args.output_filename)
        if not args.quiet:
            print('Wrote JSON to {}'.format(args.output_filename))
    elif args.output == 'items':
        cli_common.export_items(
                save.get_items(),
                args.output_filename,
                quiet=args.quiet,
                )
    else:
        # Not sure how we'd ever get here
        raise Exception('Invalid output format specified: {}'.format(args.output))