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))
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]), ))
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('')
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"> </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()
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('')
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))