From 542ec413b3bbcd0cc73d58c6771f113b11f116b6 Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Wed, 8 Apr 2015 22:50:23 -0500 Subject: [PATCH 001/103] Add support for setting challenge data --- README.md | 51 ++++ savefile.py | 823 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 873 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 63be4d2..6b5a5f9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ +# WARNING ABOUT THIS FORK + +I've forked this over from pclifford/borderlands2 primarily to add in some +support for editing savegames' challenge data. Mostly of use to someone who +is looking to beef up their Badass Ranks (though using a profile.dat editor +would be a much simpler way of doing so), or if someone wanted to set up +their character to more easily unlock all the various skins/heads that you +get while working your way through the challenges (though I believe the +aforementioned profile.dat editor can unlock all those, too, and you could +also get those via some editing in Gibbed if that's your thing). + +So: it's a bit silly, but here it is regardless. + +**WARNING:** This fork currently only supports little-endian (PC) savegame +files, 'cause the processing just hardcodes for little-endian. As it is, +don't expect a game saved with this fork to work on non-PC platforms. +Additionally, you must save using --little-endian or -l, or the file will +not work even on PC. (For all the time I've spent disclaiming this fact +in this README, I probably could've just fixed the endianness to Do The Right +Thing...) + +**ALSO:** The JSON, etc, generated by this fork won't be compatible with the JSON +generated by the main branch, 'cause the "stats" layout's completely changed. + +**ALSO ALSO:** I have not tried this on a savegame created on a copy which doesn't +have all four of the main DLCs enabled. I suspect that it would work fine +still, but I don't know. + # Read and write Borderlands 2 save files A simple command line utility to extract player information from a Borderlands @@ -12,6 +40,10 @@ Note the following before trying to use it: ## How do I modify values in a save file? +**WARNING AGAIN:** Currently this fork will only generate little-endian +files properly. Save using the --little-endian flag or the file will not +work on PC, too! + Modify save file data by changing one or more of "level", "skillpoints", "money", "eridium", "seraph", "tokens", "gunslots", "backpack", "bank", "unlocks", or "itemlevels": @@ -53,6 +85,15 @@ Unlock both at once: python savefile.py -m unlocks=slaughterdome:truevaulthunter old.sav new.sav +Set all your non-level-specific challenges to one under their maximum value: + + python savefile.py -m challenges=max old.sav new.sav + +Set all the challenges which have skin/head bonuses to be one under the value which +provides those bonuses: + + python savefile.py -m challenges=bonus old.sav new.sav + Or many changes at once, separated by commas: python savefile.py -m level=7,skillpoints=42,money=1234,eridium=12,seraph=120,itemlevels old.sav new.sav @@ -65,6 +106,8 @@ the console versions): ## How do I convert a PC save to work on a console? +**WARNING AGAIN:** This will not work properly, currently, with this fork! + A PC save file is automatically detected and read, and the default is to write in the correct format for a console. If you don't want to make any changes except to the format: @@ -89,6 +132,10 @@ editor: ## How do I import those items back into a character? +**WARNING AGAIN:** Currently this fork will only generate little-endian +files properly. Save using the --little-endian flag or the file will not +work on PC, too! + A text file of codes generated as above, or assembled by hand, can be imported into a character like so: @@ -135,6 +182,10 @@ have a backup first. ## How do I write the player data back to a new save file? +**WARNING AGAIN:** Currently this fork will only generate little-endian +files properly. Save using the --little-endian flag or the file will not +work on PC, too! + Create a new save file from protocol buffer data: python savefile.py player.p your-new-save-game.sav diff --git a/savefile.py b/savefile.py index da9a7b9..7fbf508 100755 --- a/savefile.py +++ b/savefile.py @@ -463,6 +463,812 @@ def wrap_black_market(value): sdus = [value[k] for k in black_market_keys[: len(value)]] return write_repeated_protobuf_value(sdus, 0) +# Challenge categories +challenge_cat_dlc3 = "Hammerlock's Hunt" +challenge_cat_dlc2 = "Campaign of Carnage" +challenge_cat_dlc4 = "Dragon Keep" +challenge_cat_dlc1 = "Pirate's Booty" +challenge_cat_enemies = "Enemies" +challenge_cat_elemental = "Elemental" +challenge_cat_loot = "Loot" +challenge_cat_money = "Money and Trading" +challenge_cat_vehicle = "Vehicle" +challenge_cat_health = "Health and Recovery" +challenge_cat_grenades = "Grenades" +challenge_cat_shields = "Shields" +challenge_cat_rockets = "Rocket Launcher" +challenge_cat_sniper = "Sniper Rifle" +challenge_cat_ar = "Assault Rifle" +challenge_cat_smg = "SMG" +challenge_cat_shotgun = "Shotgun" +challenge_cat_pistol = "Pistol" +challenge_cat_melee = "Melee" +challenge_cat_combat = "General Combat" +challenge_cat_misc = "Miscellaneous" + +class Challenge(object): + """ + A simple little object to hold information about our non-level-specific + challenges. This is *mostly* just a glorified dict. + """ + + def __init__(self, position, identifier, cat, name, description, levels, bonus=None): + self.position = position + self.identifier = identifier + self.cat = cat + self.name = name + self.description = description + self.levels = levels + self.bonus = bonus + + def get_max(self): + """ + Returns the point value for the challenge JUST before its maximum level. + """ + return self.levels[-1] - 1 + + def get_bonus(self): + """ + Returns the point value for the challenge JUST before getting the challenge's + bonus reward, if any. Will return None if no bonus is present for the + challenge. + """ + if self.bonus: + return self.levels[self.bonus-1] - 1 + else: + return None + + +challenges = {} + +# Hammerlock DLC Challenges +challenges[305] = Challenge(305, 1752, challenge_cat_dlc3, + "Savage Bloody Savage", + "Kill savages", + (20, 50, 100, 250, 500)) +challenges[303] = Challenge(303, 1750, challenge_cat_dlc3, + "Harder They Fall", + "Kill drifters", + (5, 15, 30, 40, 50)) +challenges[304] = Challenge(304, 1751, challenge_cat_dlc3, + "Fan Boy", + "Kill Fan Boats", + (5, 10, 15, 20, 30)) +challenges[306] = Challenge(306, 1753, challenge_cat_dlc3, + "Voracidous the Invincible", + "Defeat Voracidous the Invincible", + (1, 3, 5, 10, 15)) +challenges[307] = Challenge(307, 1952, challenge_cat_dlc3, + "Boroking Around", + "kill boroks", + (10, 20, 50, 80, 120)) +challenges[308] = Challenge(308, 1953, challenge_cat_dlc3, + "Stinging Sensation", + "Kill scaylions", + (10, 20, 50, 80, 120)) + +# Torgue DLC Challenges +challenges[310] = Challenge(310, 1756, challenge_cat_dlc2, + "Bikes Destroyed", + "Destroy Bikes", + (10, 20, 30, 50, 80)) +challenges[311] = Challenge(311, 1757, challenge_cat_dlc2, + "Bikers Killed", + "Bikers Killed", + (50, 100, 150, 200, 250)) +challenges[316] = Challenge(316, 1950, challenge_cat_dlc2, + "Torgue Tokens Acquired", + "Acquire Torgue Tokens", + (100, 250, 500, 750, 1000)) +challenges[315] = Challenge(315, 1949, challenge_cat_dlc2, + "Torgue Items Purchased", + "Purchase Torgue Items with Tokens", + (2, 5, 8, 12, 15)) +challenges[312] = Challenge(312, 1758, challenge_cat_dlc2, + "Battles Completed", + "Complete All Battles", + (1, 4, 8, 12)) +challenges[313] = Challenge(313, 1759, challenge_cat_dlc2, + "Pete The Invincible Defeated", + "Defeat Pete the Invincible", + (1, 3, 5, 10, 15)) + +# Tiny Tina DLC Challenges +challenges[318] = Challenge(318, 1954, challenge_cat_dlc4, + "Scot-Free", + "Kill dwarves", + (50, 100, 150, 200, 250)) +challenges[320] = Challenge(320, 1768, challenge_cat_dlc4, + "Rock Out With Your Rock Out", + "Kill golems", + (10, 25, 50, 80, 120)) +challenges[321] = Challenge(321, 1769, challenge_cat_dlc4, + "Knighty Knight", + "Kill knights", + (10, 25, 75, 120, 175)) +challenges[323] = Challenge(323, 1771, challenge_cat_dlc4, + "Orcs Should Perish", + "Kill orcs", + (50, 100, 150, 200, 250)) +challenges[324] = Challenge(324, 1772, challenge_cat_dlc4, + "Bone Breaker", + "Kill skeletons", + (50, 100, 150, 200, 250)) +challenges[325] = Challenge(325, 1773, challenge_cat_dlc4, + "Ew Ew Ew Ew", + "Kill spiders", + (25, 50, 100, 150, 200)) +challenges[326] = Challenge(326, 1774, challenge_cat_dlc4, + "Cheerful Green Giants", + "Kill treants", + (10, 20, 50, 80, 120)) +challenges[327] = Challenge(327, 1775, challenge_cat_dlc4, + "Magical Massacre", + "Kill wizards", + (10, 20, 50, 80, 120)) +challenges[317] = Challenge(317, 1754, challenge_cat_dlc4, + "Fus Roh Die", + "Kill dragons", + (10, 20, 50, 80, 110)) +challenges[322] = Challenge(322, 1770, challenge_cat_dlc4, + "Can't Fool Me", + "Kill mimics", + (5, 15, 30, 50, 75)) + +# Captain Scarlett DLC Challenges +challenges[298] = Challenge(298, 1743, challenge_cat_dlc1, + "In The Pink", + "Collect Seraph Crystals", + (80, 160, 240, 320, 400)) +challenges[299] = Challenge(299, 1755, challenge_cat_dlc1, + "Shady Dealings", + "Purchase Items With Seraph Crystals", + (1, 3, 5, 10, 15)) +challenges[294] = Challenge(294, 1745, challenge_cat_dlc1, + "Worm Killer", + "Kill Sand Worms", + (10, 20, 30, 50, 80)) +challenges[295] = Challenge(295, 1746, challenge_cat_dlc1, + "Land Lubber", + "Kill Pirates", + (50, 100, 150, 200, 250)) +challenges[296] = Challenge(296, 1747, challenge_cat_dlc1, + "Hovernator", + "Destroy Pirate Hovercrafts", + (5, 10, 15, 20, 30)) +challenges[297] = Challenge(297, 1748, challenge_cat_dlc1, + "Pirate Booty", + "Open Pirate Chests", + (25, 75, 150, 250, 375)) +challenges[292] = Challenge(292, 1742, challenge_cat_dlc1, + "Hyperius the Not-So-Invincible", + "Divide Hyperius by zero", + (1, 3, 5, 10, 15)) +challenges[293] = Challenge(293, 1744, challenge_cat_dlc1, + "Master Worm Food", + "Feed Master Gee to his worms", + (1, 3, 5, 10, 15)) + +# Enemies +challenges[24] = Challenge(24, 1632, challenge_cat_enemies, + "Skags to Riches", + "Kill skags", + (10, 25, 75, 150, 300)) +challenges[84] = Challenge(84, 1675, challenge_cat_enemies, + "Constructor Destructor", + "Kill constructors", + (5, 12, 20, 30, 50)) +challenges[80] = Challenge(80, 1655, challenge_cat_enemies, + "Load and Lock", + "Kill loaders", + (20, 100, 500, 1000, 1500), + bonus=3) +challenges[76] = Challenge(76, 1651, challenge_cat_enemies, + "Bully the Bullies", + "Kill bullymongs", + (25, 50, 150, 300, 750)) +challenges[77] = Challenge(77, 1652, challenge_cat_enemies, + "Crystals are a Girl's Best Friend", + "Kill crystalisks", + (10, 25, 50, 80, 120)) +challenges[78] = Challenge(78, 1653, challenge_cat_enemies, + "WHY SO MUCH HURT?!", + "Kill goliaths", + (10, 25, 50, 80, 120)) +challenges[79] = Challenge(79, 1654, challenge_cat_enemies, + "Paingineering", + "Kill Hyperion personnel", + (10, 25, 75, 150, 300)) +challenges[83] = Challenge(83, 1658, challenge_cat_enemies, + "Just a Moment of Your Time...", + "Kill surveyors", + (10, 25, 75, 150, 300)) +challenges[87] = Challenge(87, 1694, challenge_cat_enemies, + "You (No)Mad, Bro?", + "Kill nomads", + (10, 25, 75, 150, 300)) +challenges[88] = Challenge(88, 1695, challenge_cat_enemies, + "Mama's Boys", + "Kill psychos", + (50, 100, 150, 300, 500)) +challenges[89] = Challenge(89, 1696, challenge_cat_enemies, + "You Dirty Rat", + "Kill rats. Yes, really.", + (10, 25, 75, 150, 300)) +challenges[93] = Challenge(93, 1791, challenge_cat_enemies, + "Pest Control", + "Kill spiderants", + (10, 25, 75, 150, 300)) +challenges[94] = Challenge(94, 1792, challenge_cat_enemies, + "You're One Ugly Mother...", + "Kill stalkers", + (10, 25, 75, 150, 300)) +challenges[95] = Challenge(95, 1793, challenge_cat_enemies, + "Tentacle Obsession", + "Kill threshers", + (10, 25, 75, 150, 300)) +challenges[86] = Challenge(86, 1693, challenge_cat_enemies, + "Marauder? I Hardly Know 'Er", + "Kill marauders", + (20, 100, 500, 1000, 1500), + bonus=3) +challenges[96] = Challenge(96, 1794, challenge_cat_enemies, + "Another Bug Hunt", + "Kill varkids", + (10, 25, 75, 150, 300)) +challenges[97] = Challenge(97, 1795, challenge_cat_enemies, + "Die in the Friendly Skies", + "Kill buzzards", + (10, 25, 45, 70, 100)) +challenges[98] = Challenge(98, 1796, challenge_cat_enemies, + "Little Person, Big Pain", + "Kill midgets", + (10, 25, 75, 150, 300)) +challenges[249] = Challenge(249, 1895, challenge_cat_enemies, + "Hurly Burly", + "Shoot bullymong-tossed projectiles out of midair", + (10, 25, 50, 125, 250)) +challenges[250] = Challenge(250, 1896, challenge_cat_enemies, + "Short-Chained", + "Shoot chains to release midgets from shields", + (1, 5, 15, 30, 50)) +challenges[99] = Challenge(99, 1934, challenge_cat_enemies, + "Cruising for a Bruising", + "Kill bruisers", + (10, 25, 75, 150, 300)) +challenges[91] = Challenge(91, 1732, challenge_cat_enemies, + "Pod Pew Pew", + "Kill varkid pods before they hatch", + (10, 25, 45, 70, 100)) + +# Elemental +challenges[225] = Challenge(225, 1873, challenge_cat_elemental, + "Cowering Inferno", + "Ignite enemies", + (25, 100, 400, 1000, 2000)) +challenges[40] = Challenge(40, 1642, challenge_cat_elemental, + "Acid Trip", + "Kill enemies with corrode damage", + (20, 75, 250, 600, 1000)) +challenges[43] = Challenge(43, 1645, challenge_cat_elemental, + "Boom.", + "Kill enemies with explosive damage", + (20, 75, 250, 600, 1000), + bonus=3) +challenges[229] = Challenge(229, 1877, challenge_cat_elemental, + "I Just Want to Set the World on Fire", + "Deal burn damage", + (2500, 20000, 100000, 500000, 1000000), + bonus=5) +challenges[230] = Challenge(230, 1878, challenge_cat_elemental, + "Corroderate", + "Deal corrode damage", + (2500, 20000, 100000, 500000, 1000000)) +challenges[231] = Challenge(231, 1879, challenge_cat_elemental, + 'Say "Watt" Again', + "Deal electrocute damage", + (5000, 20000, 100000, 500000, 1000000)) +challenges[232] = Challenge(232, 1880, challenge_cat_elemental, + "Slag-Licked", + "Deal bonus damage to Slagged enemies", + (5000, 25000, 150000, 1000000, 5000000), + bonus=3) + +# Loot +challenges[251] = Challenge(251, 1898, challenge_cat_loot, + "Another Man's Treasure", + "Loot or purchase white items", + (50, 125, 250, 400, 600)) +challenges[252] = Challenge(252, 1899, challenge_cat_loot, + "It's Not Easy Looting Green", + "Loot or purchase green items", + (20, 50, 75, 125, 200), + bonus=3) +challenges[253] = Challenge(253, 1900, challenge_cat_loot, + "I Like My Treasure Rare", + "Loot or purchase blue items", + (5, 12, 20, 30, 45)) +challenges[254] = Challenge(254, 1901, challenge_cat_loot, + "Purple Reign", + "Loot or purchase purple items", + (2, 4, 7, 12, 20)) +challenges[255] = Challenge(255, 1902, challenge_cat_loot, + "Nothing Rhymes with Orange", + "Loot or purchase orange items", + (1, 3, 6, 10, 15), + bonus=5) +challenges[108] = Challenge(108, 1669, challenge_cat_loot, + "The Call of Booty", + "Open treasure chests", + (5, 25, 50, 125, 250)) +challenges[109] = Challenge(109, 1670, challenge_cat_loot, + "Open Pandora's Boxes", + "Open lootable chests, lockers, and other objects", + (50, 250, 750, 1500, 2500), + bonus=3) +challenges[8] = Challenge(8, 1630, challenge_cat_loot, + "Gun Runner", + "Pick up or purchase weapons", + (10, 25, 150, 300, 750)) + +# Money +challenges[118] = Challenge(118, 1858, challenge_cat_money, + "For the Hoard!", + "Save a lot of money", + (10000, 50000, 250000, 1000000, 3000000), + bonus=3) +challenges[119] = Challenge(119, 1859, challenge_cat_money, + "Dolla Dolla Bills, Y'all", + "Collect dollars from cash drops", + (5000, 25000, 125000, 500000, 1000000)) +challenges[112] = Challenge(112, 1678, challenge_cat_money, + "Wholesale", + "Sell items to vending machines", + (10, 25, 150, 300, 750)) +challenges[113] = Challenge(113, 1860, challenge_cat_money, + "Limited-Time Offer", + "Buy Items of the Day", + (1, 5, 15, 30, 50)) +challenges[111] = Challenge(111, 1810, challenge_cat_money, + "Whaddaya Buyin'?", + "Purchase items with Eridium", + (2, 5, 9, 14, 20), + bonus=4) +challenges[214] = Challenge(214, 1805, challenge_cat_money, + "Psst, Hey Buddy...", + "Trade with other players", + (1, 5, 15, 30, 50)) + +# Vehicle +challenges[37] = Challenge(37, 1640, challenge_cat_vehicle, + "Hit-and-Fun", + "Kill enemies by ramming them with a vehicle", + (5, 10, 50, 100, 200)) +challenges[275] = Challenge(275, 1920, challenge_cat_vehicle, + "Blue Sparks", + "Kill enemies by power-sliding over them in a vehicle", + (5, 15, 30, 50, 75), + bonus=3) +challenges[38] = Challenge(38, 1641, challenge_cat_vehicle, + "Turret Syndrome", + "Kill enemies using a turret or vehicle-mounted weapon", + (10, 25, 150, 300, 750)) +challenges[277] = Challenge(277, 1922, challenge_cat_vehicle, + "...One Van Leaves", + "Kill vehicles while in a vehicle", + (5, 10, 50, 100, 200)) +challenges[274] = Challenge(274, 1919, challenge_cat_vehicle, + "Passive Aggressive", + "Kill enemies while riding as a passenger (not a gunner) in a vehicle", + (1, 10, 50, 100, 200)) + +# Health +challenges[270] = Challenge(270, 1917, challenge_cat_health, + "Heal Plz", + "Recover health", + (1000, 25000, 150000, 1000000, 5000000)) +challenges[200] = Challenge(200, 1865, challenge_cat_health, + "I'll Just Help Myself", + "Get Second Winds by killing an enemy", + (5, 10, 50, 100, 200)) +challenges[201] = Challenge(201, 1866, challenge_cat_health, + "Badass Bingo", + "Get Second Winds by killing a badass enemy", + (1, 5, 15, 30, 50), + bonus=5) +challenges[204] = Challenge(204, 1868, challenge_cat_health, + "This is No Time for Lazy!", + "Revive a co-op partner", + (5, 10, 50, 100, 200), + bonus=5) +challenges[198] = Challenge(198, 1834, challenge_cat_health, + "Death, Wind, and Fire", + "Get Second Winds by killing enemies with a burn DoT (damage over time)", + (1, 5, 15, 30, 50)) +challenges[197] = Challenge(197, 1833, challenge_cat_health, + "Green Meanie", + "Get Second Winds by killing enemies with a corrosive DoT (damage over time)", + (1, 5, 15, 30, 50)) +challenges[199] = Challenge(199, 1835, challenge_cat_health, + "I'm Back! Shocked?", + "Get Second Winds by killing enemies with an electrocute DoT (damage over time)", + (1, 5, 15, 30, 50)) + +# Grenades +challenges[31] = Challenge(31, 1639, challenge_cat_grenades, + "Pull the Pin", + "Kill enemies with grenades", + (10, 25, 150, 300, 750), + bonus=3) +challenges[238] = Challenge(238, 1886, challenge_cat_grenades, + "Singled Out", + "Kill enemies with Singularity grenades", + (10, 25, 75, 150, 300)) +challenges[237] = Challenge(237, 1885, challenge_cat_grenades, + "EXPLOOOOOSIONS!", + "Kill enemies with Mirv grenades", + (10, 25, 75, 150, 300), + bonus=3) +challenges[235] = Challenge(235, 1883, challenge_cat_grenades, + "Chemical Sprayer", + "Kill enemies with Area-of-Effect grenades", + (10, 25, 75, 150, 300)) +challenges[236] = Challenge(236, 1884, challenge_cat_grenades, + "Whoa, Black Betty", + "Kill enemies with Bouncing Betty grenades", + (10, 25, 75, 150, 300)) +challenges[239] = Challenge(239, 1918, challenge_cat_grenades, + "Health Vampire", + "Kill enemies with Transfusion grenades", + (10, 25, 75, 150, 300)) + +# Shields +challenges[243] = Challenge(243, 1889, challenge_cat_shields, + "Super Novas", + "Kill enemies with a Nova shield burst", + (5, 10, 50, 100, 200), + bonus=3) +challenges[244] = Challenge(244, 1890, challenge_cat_shields, + "Roid Rage", + 'Kill enemies while buffed by a "Maylay" shield', + (5, 10, 50, 100, 200)) +challenges[245] = Challenge(245, 1891, challenge_cat_shields, + "Game of Thorns", + "Kill enemies with reflected damage from a Spike shield", + (5, 10, 50, 100, 200)) +challenges[246] = Challenge(246, 1892, challenge_cat_shields, + "Amp It Up", + "Kill enemies while buffed by an Amplify shield", + (5, 10, 50, 100, 200)) +challenges[222] = Challenge(222, 1930, challenge_cat_shields, + "Ammo Eater", + "Absorb enemy ammo with an Absorption shield", + (20, 75, 250, 600, 1000), + bonus=5) + +# Rocket Launchers +challenges[32] = Challenge(32, 1762, challenge_cat_rockets, + "Rocket and Roll", + "Kill enemies with rocket launchers", + (10, 50, 100, 250, 500), + bonus=3) +challenges[192] = Challenge(192, 1828, challenge_cat_rockets, + "Gone with the Second Wind", + "Get Second Winds with rocket launchers", + (2, 5, 15, 30, 50)) +challenges[224] = Challenge(224, 1870, challenge_cat_rockets, + "Splish Splash", + "Kill enemies with rocket launcher splash damage", + (5, 10, 50, 100, 200)) +challenges[223] = Challenge(223, 1869, challenge_cat_rockets, + "Catch-a-Rocket!", + "Kill enemies with direct hits from rocket launchers", + (5, 10, 50, 100, 200), + bonus=5) +challenges[54] = Challenge(54, 1871, challenge_cat_rockets, + "Shield Basher", + "Kill shielded enemies with one rocket each", + (5, 15, 35, 75, 125)) +challenges[52] = Challenge(52, 1808, challenge_cat_rockets, + "Sky Rockets in Flight...", + "Kill enemies from long range with rocket launchers", + (25, 100, 400, 1000, 2000)) + +# Sniper Rifles +challenges[28] = Challenge(28, 1636, challenge_cat_sniper, + "Longshot", + "Kill enemies with sniper rifles", + (20, 100, 500, 2500, 5000), + bonus=3) +challenges[178] = Challenge(178, 1666, challenge_cat_sniper, + "Longshot Headshot", + "Get critical hits with sniper rifles", + (25, 100, 400, 1000, 2000)) +challenges[188] = Challenge(188, 1824, challenge_cat_sniper, + "Leaf on the Second Wind", + "Get Second Winds with sniper rifles", + (2, 5, 15, 30, 50)) +challenges[59] = Challenge(59, 1844, challenge_cat_sniper, + "Snipe Hunting", + "Kill enemies with critical hits using sniper rifles", + (10, 25, 75, 150, 300)) +challenges[47] = Challenge(47, 1798, challenge_cat_sniper, + "No Scope, No Problem", + "Kill enemies with sniper rifles without using ironsights", + (5, 10, 50, 100, 200)) +challenges[233] = Challenge(233, 1881, challenge_cat_sniper, + "Surprise!", + "Kill unaware enemies with sniper rifles", + (5, 10, 50, 100, 200)) +challenges[55] = Challenge(55, 1872, challenge_cat_sniper, + "Eviscerated", + "Kill shielded enemies with one shot using sniper rifles", + (5, 15, 35, 75, 125), + bonus=5) + +# Assault Rifles +challenges[29] = Challenge(29, 1637, challenge_cat_ar, + "Aggravated Assault", + "Kill enemies with assault rifles", + (25, 100, 400, 1000, 2000), + bonus=3) +challenges[179] = Challenge(179, 1667, challenge_cat_ar, + "This Is My Rifle...", + "Get critical hits with assault rifles", + (25, 100, 400, 1000, 2000)) +challenges[189] = Challenge(189, 1825, challenge_cat_ar, + "From My Cold, Dead Hands", + "Get Second Winds with assault rifles", + (5, 15, 30, 50, 75)) +challenges[60] = Challenge(60, 1845, challenge_cat_ar, + "... This Is My Gun", + "Kill enemies with critical hits using assault rifles", + (10, 25, 75, 150, 300)) +challenges[46] = Challenge(46, 1797, challenge_cat_ar, + "Crouching Tiger, Hidden Assault Rifle", + "Kill enemies with assault rifles while crouched", + (25, 75, 400, 1600, 3200), + bonus=5) + +# SMGs +challenges[27] = Challenge(27, 1635, challenge_cat_smg, + "Hail of Bullets", + "Kill enemies with SMGs", + (25, 100, 400, 1000, 2000), + bonus=3) +challenges[177] = Challenge(177, 1665, challenge_cat_smg, + "Constructive Criticism", + "Get critical hits with SMGs", + (25, 100, 400, 1000, 2000)) +challenges[58] = Challenge(58, 1843, challenge_cat_smg, + "High Rate of Ire", + "Kill enemies with critical hits using SMGs", + (10, 25, 75, 150, 300)) +challenges[187] = Challenge(187, 1823, challenge_cat_smg, + "More Like Submachine FUN", + "Get Second Winds with SMGs", + (2, 5, 15, 30, 50)) + +# Shotguns +challenges[26] = Challenge(26, 1634, challenge_cat_shotgun, + "Shotgun!", + "Kill enemies with shotguns", + (25, 100, 400, 1000, 2000), + bonus=3) +challenges[176] = Challenge(176, 1664, challenge_cat_shotgun, + "Faceful of Buckshot", + "Get critical hits with shotguns", + (50, 250, 1000, 2500, 5000)) +challenges[186] = Challenge(186, 1822, challenge_cat_shotgun, + "Lock, Stock, and...", + "Get Second Winds with shotguns", + (2, 5, 15, 30, 50)) +challenges[50] = Challenge(50, 1806, challenge_cat_shotgun, + "Open Wide!", + "Kill enemies from point-blank range with shotguns", + (10, 25, 150, 300, 750)) +challenges[51] = Challenge(51, 1807, challenge_cat_shotgun, + "Shotgun Sniper", + "Kill enemies from long range with shotguns", + (10, 25, 75, 150, 300)) +challenges[57] = Challenge(57, 1842, challenge_cat_shotgun, + "Shotgun Surgeon", + "Kill enemies with critical hits using shotguns", + (10, 50, 100, 250, 500)) + +# Pistols +challenges[25] = Challenge(25, 1633, challenge_cat_pistol, + "The Killer", + "Kill enemies with pistols", + (25, 100, 400, 1000, 2000), + bonus=3) +challenges[175] = Challenge(175, 1663, challenge_cat_pistol, + "Deadeye", + "Get critical hits with pistols", + (25, 100, 400, 1000, 2000)) +challenges[185] = Challenge(185, 1821, challenge_cat_pistol, + "Hard Boiled", + "Get Second Winds with pistols", + (2, 5, 15, 30, 50)) +challenges[56] = Challenge(56, 1841, challenge_cat_pistol, + "Pistolero", + "Kill enemies with critical hits using pistols", + (10, 25, 75, 150, 300)) +challenges[49] = Challenge(49, 1800, challenge_cat_pistol, + "Quickdraw", + "Kill enemies shortly after entering ironsights with a pistol", + (10, 25, 150, 300, 750), + bonus=5) + +# Melee +challenges[75] = Challenge(75, 1650, challenge_cat_melee, + "Fisticuffs!", + "Kill enemies with melee attacks", + (25, 100, 400, 1000, 2000), + bonus=3) +challenges[247] = Challenge(247, 1893, challenge_cat_melee, + "A Squall of Violence", + "Kill enemies with melee attacks using bladed guns", + (20, 75, 250, 600, 1000)) + +# General Combat +challenges[0] = Challenge(0, 1621, challenge_cat_combat, + "Knee-Deep in Brass", + "Fire a lot of rounds", + (1000, 5000, 10000, 25000, 50000), + bonus=5) +challenges[90] = Challenge(90, 1702, challenge_cat_combat, + "...To Pay the Bills", + "Kill enemies while using your Action Skill", + (20, 75, 250, 600, 1000), + bonus=5) +challenges[269] = Challenge(269, 1916, challenge_cat_combat, + "...I Got to Boogie", + "Kill enemies at night", + (10, 25, 150, 300, 750)) +challenges[268] = Challenge(268, 1915, challenge_cat_combat, + "Afternoon Delight", + "Kill enemies during the day", + (50, 250, 1000, 2500, 5000)) +challenges[261] = Challenge(261, 1908, challenge_cat_combat, + "Boomerbang", + "Kill enemies with Tediore reloads", + (5, 10, 50, 100, 200), + bonus=5) +challenges[262] = Challenge(262, 1909, challenge_cat_combat, + "Gun Slinger", + "Deal damage with Tediore reloads", + (5000, 20000, 100000, 500000, 1000000)) +challenges[265] = Challenge(265, 1912, challenge_cat_combat, + "Not Full of Monkeys", + "Kill enemies with stationary barrels", + (10, 25, 45, 70, 100), + bonus=3) +challenges[44] = Challenge(44, 1646, challenge_cat_combat, + "Critical Acclaim", + "Kill enemies with critical hits. And rainbows.", + (20, 100, 500, 1000, 1500)) + +# Miscellaneous +challenges[104] = Challenge(104, 1659, challenge_cat_misc, + "Haters Gonna Hate", + "Win duels", + (1, 5, 15, 30, 50)) +challenges[211] = Challenge(211, 1804, challenge_cat_misc, + "Sidejacked", + "Complete side missions", + (5, 15, 30, 55, 90)) +challenges[210] = Challenge(210, 1803, challenge_cat_misc, + "Compl33tionist", + "Complete optional mission objectives", + (10, 25, 45, 70, 100)) +challenges[173] = Challenge(173, 1698, challenge_cat_misc, + "Yo Dawg I Herd You Like Challenges", + "Complete many, many challenges", + (5, 25, 50, 100, 200)) +challenges[100] = Challenge(100, 1940, challenge_cat_misc, + "JEEEEENKINSSSSSS!!!", + "Find and eliminate Jimmy Jenkins", + (1, 3, 6, 10, 15), + bonus=5) + +def unwrap_challenges(data): + """ + Unwraps our challenge data. The first ten bytes are a header: + + int32: Unknown, is always "4" on my savegames, though. + int32: Size in bytes of all the challenges, plus two more bytes + for the next short + short: Number of challenges + + Each challenge takes up a total of 12 bytes, so num_challenges*12 + should always equal size_in_bytes-2. + + The structure of each challenge is: + + byte: unknown, possibly at least part of an ID, but not unique + on its own + byte: unknown, but is always (on my saves, anyway) 6 or 7. + byte: unknown, but is always 1. + int32: total value of the challenge, across all resets + byte: unknown, but is always 1 + int32: previous, pre-challenge-reset value. Will always be 0 + until challenges have been reset at least once. + + The first two bytes of each challenge can be taken together, and if so, can + serve as a unique identifier for the challenge. I decided to read them in + that way, as a short value. I wasn't able to glean any pattern to whether + a 6 or a 7 shows up in the second byte. + + Once your challenges have been reset in-game, the previous value is copied + into that second int32, but the total value itself remains unchanged, so at + that point you need to subtract previous_value from total_value to find the + actual current state of the challenge (that procedure is obviously true + prior to any resets, too, since previous_value is just zero in that case). + + It's also worth mentioning that challenge data keeps accumulating even + after the challenge itself is completed, so the number displayed in-game + for completed challenges is no longer accurate. + + """ + + # TODO: This assumes little-endian! + + global challenges + + (unknown, size_in_bytes, num_challenges) = struct.unpack(' Date: Thu, 9 Apr 2015 12:26:35 -0500 Subject: [PATCH 002/103] Switch to using the internal challenge ID for indexing, since that appears to be more canonical to B2 itself --- savefile.py | 332 +++++++++++++++++++++++++++------------------------- 1 file changed, 175 insertions(+), 157 deletions(-) diff --git a/savefile.py b/savefile.py index 7fbf508..e7bec57 100755 --- a/savefile.py +++ b/savefile.py @@ -519,654 +519,671 @@ def get_bonus(self): return None +# There are two possible ways of uniquely identifying challenges in this file: +# via their numeric position in the list, or by what looks like an internal +# ID (though that ID is constructed a little weirdly, so I'm not sure if it's +# actually intended to be used that way or not). +# +# I did run some tests, and it looks like internally, B2 probably does use +# that ID field to identify the challenges... You can mess around with the +# order in which they're saved to the file, but so long as the ID field +# is still pointing to the challenge you want, it'll be read in properly +# (and then when you save your game, they'll be written back out in the +# original order). +# +# Given that, I decided to go ahead and use that probably-ID field as the +# index on this dict, rather than the order. That should be slightly more +# flexible for anyone editing the JSON directly, and theoretically +# shouldn't be a problem in the future since there won't be any new major +# DLC for B2... challenges = {} # Hammerlock DLC Challenges -challenges[305] = Challenge(305, 1752, challenge_cat_dlc3, +challenges[1752] = Challenge(305, 1752, challenge_cat_dlc3, "Savage Bloody Savage", "Kill savages", (20, 50, 100, 250, 500)) -challenges[303] = Challenge(303, 1750, challenge_cat_dlc3, +challenges[1750] = Challenge(303, 1750, challenge_cat_dlc3, "Harder They Fall", "Kill drifters", (5, 15, 30, 40, 50)) -challenges[304] = Challenge(304, 1751, challenge_cat_dlc3, +challenges[1751] = Challenge(304, 1751, challenge_cat_dlc3, "Fan Boy", "Kill Fan Boats", (5, 10, 15, 20, 30)) -challenges[306] = Challenge(306, 1753, challenge_cat_dlc3, +challenges[1753] = Challenge(306, 1753, challenge_cat_dlc3, "Voracidous the Invincible", "Defeat Voracidous the Invincible", (1, 3, 5, 10, 15)) -challenges[307] = Challenge(307, 1952, challenge_cat_dlc3, +challenges[1952] = Challenge(307, 1952, challenge_cat_dlc3, "Boroking Around", "kill boroks", (10, 20, 50, 80, 120)) -challenges[308] = Challenge(308, 1953, challenge_cat_dlc3, +challenges[1953] = Challenge(308, 1953, challenge_cat_dlc3, "Stinging Sensation", "Kill scaylions", (10, 20, 50, 80, 120)) # Torgue DLC Challenges -challenges[310] = Challenge(310, 1756, challenge_cat_dlc2, +challenges[1756] = Challenge(310, 1756, challenge_cat_dlc2, "Bikes Destroyed", "Destroy Bikes", (10, 20, 30, 50, 80)) -challenges[311] = Challenge(311, 1757, challenge_cat_dlc2, +challenges[1757] = Challenge(311, 1757, challenge_cat_dlc2, "Bikers Killed", "Bikers Killed", (50, 100, 150, 200, 250)) -challenges[316] = Challenge(316, 1950, challenge_cat_dlc2, +challenges[1950] = Challenge(316, 1950, challenge_cat_dlc2, "Torgue Tokens Acquired", "Acquire Torgue Tokens", (100, 250, 500, 750, 1000)) -challenges[315] = Challenge(315, 1949, challenge_cat_dlc2, +challenges[1949] = Challenge(315, 1949, challenge_cat_dlc2, "Torgue Items Purchased", "Purchase Torgue Items with Tokens", (2, 5, 8, 12, 15)) -challenges[312] = Challenge(312, 1758, challenge_cat_dlc2, +challenges[1758] = Challenge(312, 1758, challenge_cat_dlc2, "Battles Completed", "Complete All Battles", (1, 4, 8, 12)) -challenges[313] = Challenge(313, 1759, challenge_cat_dlc2, +challenges[1759] = Challenge(313, 1759, challenge_cat_dlc2, "Pete The Invincible Defeated", "Defeat Pete the Invincible", (1, 3, 5, 10, 15)) # Tiny Tina DLC Challenges -challenges[318] = Challenge(318, 1954, challenge_cat_dlc4, +challenges[1954] = Challenge(318, 1954, challenge_cat_dlc4, "Scot-Free", "Kill dwarves", (50, 100, 150, 200, 250)) -challenges[320] = Challenge(320, 1768, challenge_cat_dlc4, +challenges[1768] = Challenge(320, 1768, challenge_cat_dlc4, "Rock Out With Your Rock Out", "Kill golems", (10, 25, 50, 80, 120)) -challenges[321] = Challenge(321, 1769, challenge_cat_dlc4, +challenges[1769] = Challenge(321, 1769, challenge_cat_dlc4, "Knighty Knight", "Kill knights", (10, 25, 75, 120, 175)) -challenges[323] = Challenge(323, 1771, challenge_cat_dlc4, +challenges[1771] = Challenge(323, 1771, challenge_cat_dlc4, "Orcs Should Perish", "Kill orcs", (50, 100, 150, 200, 250)) -challenges[324] = Challenge(324, 1772, challenge_cat_dlc4, +challenges[1772] = Challenge(324, 1772, challenge_cat_dlc4, "Bone Breaker", "Kill skeletons", (50, 100, 150, 200, 250)) -challenges[325] = Challenge(325, 1773, challenge_cat_dlc4, +challenges[1773] = Challenge(325, 1773, challenge_cat_dlc4, "Ew Ew Ew Ew", "Kill spiders", (25, 50, 100, 150, 200)) -challenges[326] = Challenge(326, 1774, challenge_cat_dlc4, +challenges[1774] = Challenge(326, 1774, challenge_cat_dlc4, "Cheerful Green Giants", "Kill treants", (10, 20, 50, 80, 120)) -challenges[327] = Challenge(327, 1775, challenge_cat_dlc4, +challenges[1775] = Challenge(327, 1775, challenge_cat_dlc4, "Magical Massacre", "Kill wizards", (10, 20, 50, 80, 120)) -challenges[317] = Challenge(317, 1754, challenge_cat_dlc4, +challenges[1754] = Challenge(317, 1754, challenge_cat_dlc4, "Fus Roh Die", "Kill dragons", (10, 20, 50, 80, 110)) -challenges[322] = Challenge(322, 1770, challenge_cat_dlc4, +challenges[1770] = Challenge(322, 1770, challenge_cat_dlc4, "Can't Fool Me", "Kill mimics", (5, 15, 30, 50, 75)) # Captain Scarlett DLC Challenges -challenges[298] = Challenge(298, 1743, challenge_cat_dlc1, +challenges[1743] = Challenge(298, 1743, challenge_cat_dlc1, "In The Pink", "Collect Seraph Crystals", (80, 160, 240, 320, 400)) -challenges[299] = Challenge(299, 1755, challenge_cat_dlc1, +challenges[1755] = Challenge(299, 1755, challenge_cat_dlc1, "Shady Dealings", "Purchase Items With Seraph Crystals", (1, 3, 5, 10, 15)) -challenges[294] = Challenge(294, 1745, challenge_cat_dlc1, +challenges[1745] = Challenge(294, 1745, challenge_cat_dlc1, "Worm Killer", "Kill Sand Worms", (10, 20, 30, 50, 80)) -challenges[295] = Challenge(295, 1746, challenge_cat_dlc1, +challenges[1746] = Challenge(295, 1746, challenge_cat_dlc1, "Land Lubber", "Kill Pirates", (50, 100, 150, 200, 250)) -challenges[296] = Challenge(296, 1747, challenge_cat_dlc1, +challenges[1747] = Challenge(296, 1747, challenge_cat_dlc1, "Hovernator", "Destroy Pirate Hovercrafts", (5, 10, 15, 20, 30)) -challenges[297] = Challenge(297, 1748, challenge_cat_dlc1, +challenges[1748] = Challenge(297, 1748, challenge_cat_dlc1, "Pirate Booty", "Open Pirate Chests", (25, 75, 150, 250, 375)) -challenges[292] = Challenge(292, 1742, challenge_cat_dlc1, +challenges[1742] = Challenge(292, 1742, challenge_cat_dlc1, "Hyperius the Not-So-Invincible", "Divide Hyperius by zero", (1, 3, 5, 10, 15)) -challenges[293] = Challenge(293, 1744, challenge_cat_dlc1, +challenges[1744] = Challenge(293, 1744, challenge_cat_dlc1, "Master Worm Food", "Feed Master Gee to his worms", (1, 3, 5, 10, 15)) # Enemies -challenges[24] = Challenge(24, 1632, challenge_cat_enemies, +challenges[1632] = Challenge(24, 1632, challenge_cat_enemies, "Skags to Riches", "Kill skags", (10, 25, 75, 150, 300)) -challenges[84] = Challenge(84, 1675, challenge_cat_enemies, +challenges[1675] = Challenge(84, 1675, challenge_cat_enemies, "Constructor Destructor", "Kill constructors", (5, 12, 20, 30, 50)) -challenges[80] = Challenge(80, 1655, challenge_cat_enemies, +challenges[1655] = Challenge(80, 1655, challenge_cat_enemies, "Load and Lock", "Kill loaders", (20, 100, 500, 1000, 1500), bonus=3) -challenges[76] = Challenge(76, 1651, challenge_cat_enemies, +challenges[1651] = Challenge(76, 1651, challenge_cat_enemies, "Bully the Bullies", "Kill bullymongs", (25, 50, 150, 300, 750)) -challenges[77] = Challenge(77, 1652, challenge_cat_enemies, +challenges[1652] = Challenge(77, 1652, challenge_cat_enemies, "Crystals are a Girl's Best Friend", "Kill crystalisks", (10, 25, 50, 80, 120)) -challenges[78] = Challenge(78, 1653, challenge_cat_enemies, +challenges[1653] = Challenge(78, 1653, challenge_cat_enemies, "WHY SO MUCH HURT?!", "Kill goliaths", (10, 25, 50, 80, 120)) -challenges[79] = Challenge(79, 1654, challenge_cat_enemies, +challenges[1654] = Challenge(79, 1654, challenge_cat_enemies, "Paingineering", "Kill Hyperion personnel", (10, 25, 75, 150, 300)) -challenges[83] = Challenge(83, 1658, challenge_cat_enemies, +challenges[1658] = Challenge(83, 1658, challenge_cat_enemies, "Just a Moment of Your Time...", "Kill surveyors", (10, 25, 75, 150, 300)) -challenges[87] = Challenge(87, 1694, challenge_cat_enemies, +challenges[1694] = Challenge(87, 1694, challenge_cat_enemies, "You (No)Mad, Bro?", "Kill nomads", (10, 25, 75, 150, 300)) -challenges[88] = Challenge(88, 1695, challenge_cat_enemies, +challenges[1695] = Challenge(88, 1695, challenge_cat_enemies, "Mama's Boys", "Kill psychos", (50, 100, 150, 300, 500)) -challenges[89] = Challenge(89, 1696, challenge_cat_enemies, +challenges[1696] = Challenge(89, 1696, challenge_cat_enemies, "You Dirty Rat", "Kill rats. Yes, really.", (10, 25, 75, 150, 300)) -challenges[93] = Challenge(93, 1791, challenge_cat_enemies, +challenges[1791] = Challenge(93, 1791, challenge_cat_enemies, "Pest Control", "Kill spiderants", (10, 25, 75, 150, 300)) -challenges[94] = Challenge(94, 1792, challenge_cat_enemies, +challenges[1792] = Challenge(94, 1792, challenge_cat_enemies, "You're One Ugly Mother...", "Kill stalkers", (10, 25, 75, 150, 300)) -challenges[95] = Challenge(95, 1793, challenge_cat_enemies, +challenges[1793] = Challenge(95, 1793, challenge_cat_enemies, "Tentacle Obsession", "Kill threshers", (10, 25, 75, 150, 300)) -challenges[86] = Challenge(86, 1693, challenge_cat_enemies, +challenges[1693] = Challenge(86, 1693, challenge_cat_enemies, "Marauder? I Hardly Know 'Er", "Kill marauders", (20, 100, 500, 1000, 1500), bonus=3) -challenges[96] = Challenge(96, 1794, challenge_cat_enemies, +challenges[1794] = Challenge(96, 1794, challenge_cat_enemies, "Another Bug Hunt", "Kill varkids", (10, 25, 75, 150, 300)) -challenges[97] = Challenge(97, 1795, challenge_cat_enemies, +challenges[1795] = Challenge(97, 1795, challenge_cat_enemies, "Die in the Friendly Skies", "Kill buzzards", (10, 25, 45, 70, 100)) -challenges[98] = Challenge(98, 1796, challenge_cat_enemies, +challenges[1796] = Challenge(98, 1796, challenge_cat_enemies, "Little Person, Big Pain", "Kill midgets", (10, 25, 75, 150, 300)) -challenges[249] = Challenge(249, 1895, challenge_cat_enemies, +challenges[1895] = Challenge(249, 1895, challenge_cat_enemies, "Hurly Burly", "Shoot bullymong-tossed projectiles out of midair", (10, 25, 50, 125, 250)) -challenges[250] = Challenge(250, 1896, challenge_cat_enemies, +challenges[1896] = Challenge(250, 1896, challenge_cat_enemies, "Short-Chained", "Shoot chains to release midgets from shields", (1, 5, 15, 30, 50)) -challenges[99] = Challenge(99, 1934, challenge_cat_enemies, +challenges[1934] = Challenge(99, 1934, challenge_cat_enemies, "Cruising for a Bruising", "Kill bruisers", (10, 25, 75, 150, 300)) -challenges[91] = Challenge(91, 1732, challenge_cat_enemies, +challenges[1732] = Challenge(91, 1732, challenge_cat_enemies, "Pod Pew Pew", "Kill varkid pods before they hatch", (10, 25, 45, 70, 100)) # Elemental -challenges[225] = Challenge(225, 1873, challenge_cat_elemental, +challenges[1873] = Challenge(225, 1873, challenge_cat_elemental, "Cowering Inferno", "Ignite enemies", (25, 100, 400, 1000, 2000)) -challenges[40] = Challenge(40, 1642, challenge_cat_elemental, +challenges[1642] = Challenge(40, 1642, challenge_cat_elemental, "Acid Trip", "Kill enemies with corrode damage", (20, 75, 250, 600, 1000)) -challenges[43] = Challenge(43, 1645, challenge_cat_elemental, +challenges[1645] = Challenge(43, 1645, challenge_cat_elemental, "Boom.", "Kill enemies with explosive damage", (20, 75, 250, 600, 1000), bonus=3) -challenges[229] = Challenge(229, 1877, challenge_cat_elemental, +challenges[1877] = Challenge(229, 1877, challenge_cat_elemental, "I Just Want to Set the World on Fire", "Deal burn damage", (2500, 20000, 100000, 500000, 1000000), bonus=5) -challenges[230] = Challenge(230, 1878, challenge_cat_elemental, +challenges[1878] = Challenge(230, 1878, challenge_cat_elemental, "Corroderate", "Deal corrode damage", (2500, 20000, 100000, 500000, 1000000)) -challenges[231] = Challenge(231, 1879, challenge_cat_elemental, +challenges[1879] = Challenge(231, 1879, challenge_cat_elemental, 'Say "Watt" Again', "Deal electrocute damage", (5000, 20000, 100000, 500000, 1000000)) -challenges[232] = Challenge(232, 1880, challenge_cat_elemental, +challenges[1880] = Challenge(232, 1880, challenge_cat_elemental, "Slag-Licked", "Deal bonus damage to Slagged enemies", (5000, 25000, 150000, 1000000, 5000000), bonus=3) # Loot -challenges[251] = Challenge(251, 1898, challenge_cat_loot, +challenges[1898] = Challenge(251, 1898, challenge_cat_loot, "Another Man's Treasure", "Loot or purchase white items", (50, 125, 250, 400, 600)) -challenges[252] = Challenge(252, 1899, challenge_cat_loot, +challenges[1899] = Challenge(252, 1899, challenge_cat_loot, "It's Not Easy Looting Green", "Loot or purchase green items", (20, 50, 75, 125, 200), bonus=3) -challenges[253] = Challenge(253, 1900, challenge_cat_loot, +challenges[1900] = Challenge(253, 1900, challenge_cat_loot, "I Like My Treasure Rare", "Loot or purchase blue items", (5, 12, 20, 30, 45)) -challenges[254] = Challenge(254, 1901, challenge_cat_loot, +challenges[1901] = Challenge(254, 1901, challenge_cat_loot, "Purple Reign", "Loot or purchase purple items", (2, 4, 7, 12, 20)) -challenges[255] = Challenge(255, 1902, challenge_cat_loot, +challenges[1902] = Challenge(255, 1902, challenge_cat_loot, "Nothing Rhymes with Orange", "Loot or purchase orange items", (1, 3, 6, 10, 15), bonus=5) -challenges[108] = Challenge(108, 1669, challenge_cat_loot, +challenges[1669] = Challenge(108, 1669, challenge_cat_loot, "The Call of Booty", "Open treasure chests", (5, 25, 50, 125, 250)) -challenges[109] = Challenge(109, 1670, challenge_cat_loot, +challenges[1670] = Challenge(109, 1670, challenge_cat_loot, "Open Pandora's Boxes", "Open lootable chests, lockers, and other objects", (50, 250, 750, 1500, 2500), bonus=3) -challenges[8] = Challenge(8, 1630, challenge_cat_loot, +challenges[1630] = Challenge(8, 1630, challenge_cat_loot, "Gun Runner", "Pick up or purchase weapons", (10, 25, 150, 300, 750)) # Money -challenges[118] = Challenge(118, 1858, challenge_cat_money, +challenges[1858] = Challenge(118, 1858, challenge_cat_money, "For the Hoard!", "Save a lot of money", (10000, 50000, 250000, 1000000, 3000000), bonus=3) -challenges[119] = Challenge(119, 1859, challenge_cat_money, +challenges[1859] = Challenge(119, 1859, challenge_cat_money, "Dolla Dolla Bills, Y'all", "Collect dollars from cash drops", (5000, 25000, 125000, 500000, 1000000)) -challenges[112] = Challenge(112, 1678, challenge_cat_money, +challenges[1678] = Challenge(112, 1678, challenge_cat_money, "Wholesale", "Sell items to vending machines", (10, 25, 150, 300, 750)) -challenges[113] = Challenge(113, 1860, challenge_cat_money, +challenges[1860] = Challenge(113, 1860, challenge_cat_money, "Limited-Time Offer", "Buy Items of the Day", (1, 5, 15, 30, 50)) -challenges[111] = Challenge(111, 1810, challenge_cat_money, +challenges[1810] = Challenge(111, 1810, challenge_cat_money, "Whaddaya Buyin'?", "Purchase items with Eridium", (2, 5, 9, 14, 20), bonus=4) -challenges[214] = Challenge(214, 1805, challenge_cat_money, +challenges[1805] = Challenge(214, 1805, challenge_cat_money, "Psst, Hey Buddy...", "Trade with other players", (1, 5, 15, 30, 50)) # Vehicle -challenges[37] = Challenge(37, 1640, challenge_cat_vehicle, +challenges[1640] = Challenge(37, 1640, challenge_cat_vehicle, "Hit-and-Fun", "Kill enemies by ramming them with a vehicle", (5, 10, 50, 100, 200)) -challenges[275] = Challenge(275, 1920, challenge_cat_vehicle, +challenges[1920] = Challenge(275, 1920, challenge_cat_vehicle, "Blue Sparks", "Kill enemies by power-sliding over them in a vehicle", (5, 15, 30, 50, 75), bonus=3) -challenges[38] = Challenge(38, 1641, challenge_cat_vehicle, +challenges[1641] = Challenge(38, 1641, challenge_cat_vehicle, "Turret Syndrome", "Kill enemies using a turret or vehicle-mounted weapon", (10, 25, 150, 300, 750)) -challenges[277] = Challenge(277, 1922, challenge_cat_vehicle, +challenges[1922] = Challenge(277, 1922, challenge_cat_vehicle, "...One Van Leaves", "Kill vehicles while in a vehicle", (5, 10, 50, 100, 200)) -challenges[274] = Challenge(274, 1919, challenge_cat_vehicle, +challenges[1919] = Challenge(274, 1919, challenge_cat_vehicle, "Passive Aggressive", "Kill enemies while riding as a passenger (not a gunner) in a vehicle", (1, 10, 50, 100, 200)) # Health -challenges[270] = Challenge(270, 1917, challenge_cat_health, +challenges[1917] = Challenge(270, 1917, challenge_cat_health, "Heal Plz", "Recover health", (1000, 25000, 150000, 1000000, 5000000)) -challenges[200] = Challenge(200, 1865, challenge_cat_health, +challenges[1865] = Challenge(200, 1865, challenge_cat_health, "I'll Just Help Myself", "Get Second Winds by killing an enemy", (5, 10, 50, 100, 200)) -challenges[201] = Challenge(201, 1866, challenge_cat_health, +challenges[1866] = Challenge(201, 1866, challenge_cat_health, "Badass Bingo", "Get Second Winds by killing a badass enemy", (1, 5, 15, 30, 50), bonus=5) -challenges[204] = Challenge(204, 1868, challenge_cat_health, +challenges[1868] = Challenge(204, 1868, challenge_cat_health, "This is No Time for Lazy!", "Revive a co-op partner", (5, 10, 50, 100, 200), bonus=5) -challenges[198] = Challenge(198, 1834, challenge_cat_health, +challenges[1834] = Challenge(198, 1834, challenge_cat_health, "Death, Wind, and Fire", "Get Second Winds by killing enemies with a burn DoT (damage over time)", (1, 5, 15, 30, 50)) -challenges[197] = Challenge(197, 1833, challenge_cat_health, +challenges[1833] = Challenge(197, 1833, challenge_cat_health, "Green Meanie", "Get Second Winds by killing enemies with a corrosive DoT (damage over time)", (1, 5, 15, 30, 50)) -challenges[199] = Challenge(199, 1835, challenge_cat_health, +challenges[1835] = Challenge(199, 1835, challenge_cat_health, "I'm Back! Shocked?", "Get Second Winds by killing enemies with an electrocute DoT (damage over time)", (1, 5, 15, 30, 50)) # Grenades -challenges[31] = Challenge(31, 1639, challenge_cat_grenades, +challenges[1639] = Challenge(31, 1639, challenge_cat_grenades, "Pull the Pin", "Kill enemies with grenades", (10, 25, 150, 300, 750), bonus=3) -challenges[238] = Challenge(238, 1886, challenge_cat_grenades, +challenges[1886] = Challenge(238, 1886, challenge_cat_grenades, "Singled Out", "Kill enemies with Singularity grenades", (10, 25, 75, 150, 300)) -challenges[237] = Challenge(237, 1885, challenge_cat_grenades, +challenges[1885] = Challenge(237, 1885, challenge_cat_grenades, "EXPLOOOOOSIONS!", "Kill enemies with Mirv grenades", (10, 25, 75, 150, 300), bonus=3) -challenges[235] = Challenge(235, 1883, challenge_cat_grenades, +challenges[1883] = Challenge(235, 1883, challenge_cat_grenades, "Chemical Sprayer", "Kill enemies with Area-of-Effect grenades", (10, 25, 75, 150, 300)) -challenges[236] = Challenge(236, 1884, challenge_cat_grenades, +challenges[1884] = Challenge(236, 1884, challenge_cat_grenades, "Whoa, Black Betty", "Kill enemies with Bouncing Betty grenades", (10, 25, 75, 150, 300)) -challenges[239] = Challenge(239, 1918, challenge_cat_grenades, +challenges[1918] = Challenge(239, 1918, challenge_cat_grenades, "Health Vampire", "Kill enemies with Transfusion grenades", (10, 25, 75, 150, 300)) # Shields -challenges[243] = Challenge(243, 1889, challenge_cat_shields, +challenges[1889] = Challenge(243, 1889, challenge_cat_shields, "Super Novas", "Kill enemies with a Nova shield burst", (5, 10, 50, 100, 200), bonus=3) -challenges[244] = Challenge(244, 1890, challenge_cat_shields, +challenges[1890] = Challenge(244, 1890, challenge_cat_shields, "Roid Rage", 'Kill enemies while buffed by a "Maylay" shield', (5, 10, 50, 100, 200)) -challenges[245] = Challenge(245, 1891, challenge_cat_shields, +challenges[1891] = Challenge(245, 1891, challenge_cat_shields, "Game of Thorns", "Kill enemies with reflected damage from a Spike shield", (5, 10, 50, 100, 200)) -challenges[246] = Challenge(246, 1892, challenge_cat_shields, +challenges[1892] = Challenge(246, 1892, challenge_cat_shields, "Amp It Up", "Kill enemies while buffed by an Amplify shield", (5, 10, 50, 100, 200)) -challenges[222] = Challenge(222, 1930, challenge_cat_shields, +challenges[1930] = Challenge(222, 1930, challenge_cat_shields, "Ammo Eater", "Absorb enemy ammo with an Absorption shield", (20, 75, 250, 600, 1000), bonus=5) # Rocket Launchers -challenges[32] = Challenge(32, 1762, challenge_cat_rockets, +challenges[1762] = Challenge(32, 1762, challenge_cat_rockets, "Rocket and Roll", "Kill enemies with rocket launchers", (10, 50, 100, 250, 500), bonus=3) -challenges[192] = Challenge(192, 1828, challenge_cat_rockets, +challenges[1828] = Challenge(192, 1828, challenge_cat_rockets, "Gone with the Second Wind", "Get Second Winds with rocket launchers", (2, 5, 15, 30, 50)) -challenges[224] = Challenge(224, 1870, challenge_cat_rockets, +challenges[1870] = Challenge(224, 1870, challenge_cat_rockets, "Splish Splash", "Kill enemies with rocket launcher splash damage", (5, 10, 50, 100, 200)) -challenges[223] = Challenge(223, 1869, challenge_cat_rockets, +challenges[1869] = Challenge(223, 1869, challenge_cat_rockets, "Catch-a-Rocket!", "Kill enemies with direct hits from rocket launchers", (5, 10, 50, 100, 200), bonus=5) -challenges[54] = Challenge(54, 1871, challenge_cat_rockets, +challenges[1871] = Challenge(54, 1871, challenge_cat_rockets, "Shield Basher", "Kill shielded enemies with one rocket each", (5, 15, 35, 75, 125)) -challenges[52] = Challenge(52, 1808, challenge_cat_rockets, +challenges[1808] = Challenge(52, 1808, challenge_cat_rockets, "Sky Rockets in Flight...", "Kill enemies from long range with rocket launchers", (25, 100, 400, 1000, 2000)) # Sniper Rifles -challenges[28] = Challenge(28, 1636, challenge_cat_sniper, +challenges[1636] = Challenge(28, 1636, challenge_cat_sniper, "Longshot", "Kill enemies with sniper rifles", (20, 100, 500, 2500, 5000), bonus=3) -challenges[178] = Challenge(178, 1666, challenge_cat_sniper, +challenges[1666] = Challenge(178, 1666, challenge_cat_sniper, "Longshot Headshot", "Get critical hits with sniper rifles", (25, 100, 400, 1000, 2000)) -challenges[188] = Challenge(188, 1824, challenge_cat_sniper, +challenges[1824] = Challenge(188, 1824, challenge_cat_sniper, "Leaf on the Second Wind", "Get Second Winds with sniper rifles", (2, 5, 15, 30, 50)) -challenges[59] = Challenge(59, 1844, challenge_cat_sniper, +challenges[1844] = Challenge(59, 1844, challenge_cat_sniper, "Snipe Hunting", "Kill enemies with critical hits using sniper rifles", (10, 25, 75, 150, 300)) -challenges[47] = Challenge(47, 1798, challenge_cat_sniper, +challenges[1798] = Challenge(47, 1798, challenge_cat_sniper, "No Scope, No Problem", "Kill enemies with sniper rifles without using ironsights", (5, 10, 50, 100, 200)) -challenges[233] = Challenge(233, 1881, challenge_cat_sniper, +challenges[1881] = Challenge(233, 1881, challenge_cat_sniper, "Surprise!", "Kill unaware enemies with sniper rifles", (5, 10, 50, 100, 200)) -challenges[55] = Challenge(55, 1872, challenge_cat_sniper, +challenges[1872] = Challenge(55, 1872, challenge_cat_sniper, "Eviscerated", "Kill shielded enemies with one shot using sniper rifles", (5, 15, 35, 75, 125), bonus=5) # Assault Rifles -challenges[29] = Challenge(29, 1637, challenge_cat_ar, +challenges[1637] = Challenge(29, 1637, challenge_cat_ar, "Aggravated Assault", "Kill enemies with assault rifles", (25, 100, 400, 1000, 2000), bonus=3) -challenges[179] = Challenge(179, 1667, challenge_cat_ar, +challenges[1667] = Challenge(179, 1667, challenge_cat_ar, "This Is My Rifle...", "Get critical hits with assault rifles", (25, 100, 400, 1000, 2000)) -challenges[189] = Challenge(189, 1825, challenge_cat_ar, +challenges[1825] = Challenge(189, 1825, challenge_cat_ar, "From My Cold, Dead Hands", "Get Second Winds with assault rifles", (5, 15, 30, 50, 75)) -challenges[60] = Challenge(60, 1845, challenge_cat_ar, +challenges[1845] = Challenge(60, 1845, challenge_cat_ar, "... This Is My Gun", "Kill enemies with critical hits using assault rifles", (10, 25, 75, 150, 300)) -challenges[46] = Challenge(46, 1797, challenge_cat_ar, +challenges[1797] = Challenge(46, 1797, challenge_cat_ar, "Crouching Tiger, Hidden Assault Rifle", "Kill enemies with assault rifles while crouched", (25, 75, 400, 1600, 3200), bonus=5) # SMGs -challenges[27] = Challenge(27, 1635, challenge_cat_smg, +challenges[1635] = Challenge(27, 1635, challenge_cat_smg, "Hail of Bullets", "Kill enemies with SMGs", (25, 100, 400, 1000, 2000), bonus=3) -challenges[177] = Challenge(177, 1665, challenge_cat_smg, +challenges[1665] = Challenge(177, 1665, challenge_cat_smg, "Constructive Criticism", "Get critical hits with SMGs", (25, 100, 400, 1000, 2000)) -challenges[58] = Challenge(58, 1843, challenge_cat_smg, +challenges[1843] = Challenge(58, 1843, challenge_cat_smg, "High Rate of Ire", "Kill enemies with critical hits using SMGs", (10, 25, 75, 150, 300)) -challenges[187] = Challenge(187, 1823, challenge_cat_smg, +challenges[1823] = Challenge(187, 1823, challenge_cat_smg, "More Like Submachine FUN", "Get Second Winds with SMGs", (2, 5, 15, 30, 50)) # Shotguns -challenges[26] = Challenge(26, 1634, challenge_cat_shotgun, +challenges[1634] = Challenge(26, 1634, challenge_cat_shotgun, "Shotgun!", "Kill enemies with shotguns", (25, 100, 400, 1000, 2000), bonus=3) -challenges[176] = Challenge(176, 1664, challenge_cat_shotgun, +challenges[1664] = Challenge(176, 1664, challenge_cat_shotgun, "Faceful of Buckshot", "Get critical hits with shotguns", (50, 250, 1000, 2500, 5000)) -challenges[186] = Challenge(186, 1822, challenge_cat_shotgun, +challenges[1822] = Challenge(186, 1822, challenge_cat_shotgun, "Lock, Stock, and...", "Get Second Winds with shotguns", (2, 5, 15, 30, 50)) -challenges[50] = Challenge(50, 1806, challenge_cat_shotgun, +challenges[1806] = Challenge(50, 1806, challenge_cat_shotgun, "Open Wide!", "Kill enemies from point-blank range with shotguns", (10, 25, 150, 300, 750)) -challenges[51] = Challenge(51, 1807, challenge_cat_shotgun, +challenges[1807] = Challenge(51, 1807, challenge_cat_shotgun, "Shotgun Sniper", "Kill enemies from long range with shotguns", (10, 25, 75, 150, 300)) -challenges[57] = Challenge(57, 1842, challenge_cat_shotgun, +challenges[1842] = Challenge(57, 1842, challenge_cat_shotgun, "Shotgun Surgeon", "Kill enemies with critical hits using shotguns", (10, 50, 100, 250, 500)) # Pistols -challenges[25] = Challenge(25, 1633, challenge_cat_pistol, +challenges[1633] = Challenge(25, 1633, challenge_cat_pistol, "The Killer", "Kill enemies with pistols", (25, 100, 400, 1000, 2000), bonus=3) -challenges[175] = Challenge(175, 1663, challenge_cat_pistol, +challenges[1663] = Challenge(175, 1663, challenge_cat_pistol, "Deadeye", "Get critical hits with pistols", (25, 100, 400, 1000, 2000)) -challenges[185] = Challenge(185, 1821, challenge_cat_pistol, +challenges[1821] = Challenge(185, 1821, challenge_cat_pistol, "Hard Boiled", "Get Second Winds with pistols", (2, 5, 15, 30, 50)) -challenges[56] = Challenge(56, 1841, challenge_cat_pistol, +challenges[1841] = Challenge(56, 1841, challenge_cat_pistol, "Pistolero", "Kill enemies with critical hits using pistols", (10, 25, 75, 150, 300)) -challenges[49] = Challenge(49, 1800, challenge_cat_pistol, +challenges[1800] = Challenge(49, 1800, challenge_cat_pistol, "Quickdraw", "Kill enemies shortly after entering ironsights with a pistol", (10, 25, 150, 300, 750), bonus=5) # Melee -challenges[75] = Challenge(75, 1650, challenge_cat_melee, +challenges[1650] = Challenge(75, 1650, challenge_cat_melee, "Fisticuffs!", "Kill enemies with melee attacks", (25, 100, 400, 1000, 2000), bonus=3) -challenges[247] = Challenge(247, 1893, challenge_cat_melee, +challenges[1893] = Challenge(247, 1893, challenge_cat_melee, "A Squall of Violence", "Kill enemies with melee attacks using bladed guns", (20, 75, 250, 600, 1000)) # General Combat -challenges[0] = Challenge(0, 1621, challenge_cat_combat, +challenges[1621] = Challenge(0, 1621, challenge_cat_combat, "Knee-Deep in Brass", "Fire a lot of rounds", (1000, 5000, 10000, 25000, 50000), bonus=5) -challenges[90] = Challenge(90, 1702, challenge_cat_combat, +challenges[1702] = Challenge(90, 1702, challenge_cat_combat, "...To Pay the Bills", "Kill enemies while using your Action Skill", (20, 75, 250, 600, 1000), bonus=5) -challenges[269] = Challenge(269, 1916, challenge_cat_combat, +challenges[1916] = Challenge(269, 1916, challenge_cat_combat, "...I Got to Boogie", "Kill enemies at night", (10, 25, 150, 300, 750)) -challenges[268] = Challenge(268, 1915, challenge_cat_combat, +challenges[1915] = Challenge(268, 1915, challenge_cat_combat, "Afternoon Delight", "Kill enemies during the day", (50, 250, 1000, 2500, 5000)) -challenges[261] = Challenge(261, 1908, challenge_cat_combat, +challenges[1908] = Challenge(261, 1908, challenge_cat_combat, "Boomerbang", "Kill enemies with Tediore reloads", (5, 10, 50, 100, 200), bonus=5) -challenges[262] = Challenge(262, 1909, challenge_cat_combat, +challenges[1909] = Challenge(262, 1909, challenge_cat_combat, "Gun Slinger", "Deal damage with Tediore reloads", (5000, 20000, 100000, 500000, 1000000)) -challenges[265] = Challenge(265, 1912, challenge_cat_combat, +challenges[1912] = Challenge(265, 1912, challenge_cat_combat, "Not Full of Monkeys", "Kill enemies with stationary barrels", (10, 25, 45, 70, 100), bonus=3) -challenges[44] = Challenge(44, 1646, challenge_cat_combat, +challenges[1646] = Challenge(44, 1646, challenge_cat_combat, "Critical Acclaim", "Kill enemies with critical hits. And rainbows.", (20, 100, 500, 1000, 1500)) # Miscellaneous -challenges[104] = Challenge(104, 1659, challenge_cat_misc, +challenges[1659] = Challenge(104, 1659, challenge_cat_misc, "Haters Gonna Hate", "Win duels", (1, 5, 15, 30, 50)) -challenges[211] = Challenge(211, 1804, challenge_cat_misc, +challenges[1804] = Challenge(211, 1804, challenge_cat_misc, "Sidejacked", "Complete side missions", (5, 15, 30, 55, 90)) -challenges[210] = Challenge(210, 1803, challenge_cat_misc, +challenges[1803] = Challenge(210, 1803, challenge_cat_misc, "Compl33tionist", "Complete optional mission objectives", (10, 25, 45, 70, 100)) -challenges[173] = Challenge(173, 1698, challenge_cat_misc, +challenges[1698] = Challenge(173, 1698, challenge_cat_misc, "Yo Dawg I Herd You Like Challenges", "Complete many, many challenges", (5, 25, 50, 100, 200)) -challenges[100] = Challenge(100, 1940, challenge_cat_misc, +challenges[1940] = Challenge(100, 1940, challenge_cat_misc, "JEEEEENKINSSSSSS!!!", "Find and eliminate Jimmy Jenkins", (1, 3, 6, 10, 15), @@ -1233,15 +1250,16 @@ def unwrap_challenges(data): mydict['challenges'] = [] for challenge in range(num_challenges): idx = 10+(challenge*12) - mydict['challenges'].append(dict(zip( + challenge_dict = dict(zip( ['id', 'first_one', 'total_value', 'second_one', 'previous_value'], - struct.unpack(' Date: Thu, 9 Apr 2015 12:37:15 -0500 Subject: [PATCH 003/103] Added an option to zero out all non-location-specific challenges --- README.md | 4 ++++ savefile.py | 25 ++++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6b5a5f9..72a3f9a 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,10 @@ provides those bonuses: python savefile.py -m challenges=bonus old.sav new.sav +To zero out all non-location-specific challenges: + + python savefile.py -m challenges=zero old.sav new.sav + Or many changes at once, separated by commas: python savefile.py -m level=7,skillpoints=42,money=1234,eridium=12,seraph=120,itemlevels old.sav new.sav diff --git a/savefile.py b/savefile.py index e7bec57..0e274da 100755 --- a/savefile.py +++ b/savefile.py @@ -1807,17 +1807,24 @@ def modify_save(data, changes, endian=1): if changes.has_key("challenges"): data = unwrap_challenges(player[15][0][1]) - # You can specify "max" and "bonus" at the same time, which will then put - # everything at its max value, and then potentially lower the ones which - # have bonuses. - if "max" in changes["challenges"]: - for save_challenge in data['challenges']: - if save_challenge['id'] in challenges: + # You can specify multiple options at once. Specifying "max" and + # "bonus" at the same time, for instance, will put everything at its + # max value, and then potentially lower the ones which have bonuses. + do_zero = "zero" in changes["challenges"] + do_max = "max" in changes["challenges"] + do_bonus = "bonus" in changes["challenges"] + + # Loop through + for save_challenge in data['challenges']: + if save_challenge['id'] in challenges: + if do_zero: + save_challenge['total_value'] = save_challenge['previous_value'] + if do_max: save_challenge['total_value'] = save_challenge['previous_value'] + challenges[save_challenge['id']].get_max() - if "bonus" in changes["challenges"]: - for save_challenge in data['challenges']: - if save_challenge['id'] in challenges and challenges[save_challenge['id']].bonus: + if do_bonus and challenges[save_challenge['id']].bonus: save_challenge['total_value'] = save_challenge['previous_value'] + challenges[save_challenge['id']].get_bonus() + + # Re-wrap the data player[15][0][1] = wrap_challenges(data) return wrap_player_data(write_protobuf(player), endian) From a3af497587f4292f107bc08eb9cce0d3ca02cadd Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Thu, 9 Apr 2015 17:08:45 -0500 Subject: [PATCH 004/103] Add ability to unlock all challenges --- README.md | 7 ++++++- savefile.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72a3f9a..69bdc46 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,12 @@ Unlock both at once: python savefile.py -m unlocks=slaughterdome:truevaulthunter old.sav new.sav -Set all your non-level-specific challenges to one under their maximum value: +Unlock all non-level-specific challenges: + + python savefile.py -m unlocks=challenges old.sav new.sav + +Set all your non-level-specific challenges to one under their maximum value (you'll +probably want to unlock all challenges at the same time, if you do this): python savefile.py -m challenges=max old.sav new.sav diff --git a/savefile.py b/savefile.py index 0e274da..bdb5a8e 100755 --- a/savefile.py +++ b/savefile.py @@ -463,6 +463,39 @@ def wrap_black_market(value): sdus = [value[k] for k in black_market_keys[: len(value)]] return write_repeated_protobuf_value(sdus, 0) +# Unlockable challenge strings (for the challenges which only show up +# after entering certain areas, or after completing specific other +# challenges, etc) +unlockable_challenge_strings = ["GD_Challenges.enemies.Enemies_KillVarkidPods", + "GD_Challenges.enemies.Enemies_KillSpiderants", + "GD_Challenges.enemies.Enemies_KillGyros", + "GD_Challenges.enemies.Enemies_KillVarkid", + "GD_Challenges.enemies.Enemies_KillSkags", + "GD_Challenges.enemies.Enemies_KillCrystalisks", + "GD_Challenges.enemies.Enemies_KillStalkers", + "GD_Challenges.enemies.Enemies_KillThreshers", + "GD_Challenges.Player.Player_SecondWindFromCorrosive", + "GD_Challenges.Player.Player_SecondWindFromFire", + "GD_Challenges.Player.Player_SecondWindFromShock", + "GD_Challenges.Weapons.Launcher_KillsSplashDamage", + "GD_Challenges.Weapons.Launcher_KillsFullShieldEnemy", + "GD_Challenges.Weapons.Launcher_KillsDirectHit", + "GD_Challenges.Weapons.Launcher_KillsLongRange", + "GD_Challenges.Weapons.SniperRifle_KillsFullShieldEnemy", + "GD_Challenges.Weapons.SniperRifle_KillsFromHip", + "GD_Challenges.Weapons.SniperRifle_KillsUnaware", + "GD_Challenges.Weapons.Sniper_CriticalHitKills", + "GD_Challenges.Weapons.SMG_CriticalHitKills", + "GD_Challenges.Weapons.AssaultRifle_KillsCrouched", + "GD_Challenges.Weapons.AssaultRifle_CriticalHitKills", + "GD_Challenges.Weapons.Shotgun_KillsLongRange", + "GD_Challenges.Weapons.Shotgun_KillsPointBlank", + "GD_Challenges.Weapons.Shotgun_CriticalHitKills", + "GD_Challenges.Weapons.Pistol_KillsQuickshot", + "GD_Challenges.Weapons.Pistol_CriticalHitKills", + "GD_Challenges.Melee.Melee_KillsBladed", + ] + # Challenge categories challenge_cat_dlc3 = "Hammerlock's Hunt" challenge_cat_dlc2 = "Campaign of Carnage" @@ -1804,6 +1837,17 @@ def modify_save(data, changes, endian=1): if "truevaulthunter" in unlocks: if player[7][0][1] < 1: player[7][0][1] = 1 + if "challenges" in unlocks: + challenge_unlocks = [apply_structure(read_protobuf(d[1]), save_structure[38][2]) for d in player[38]] + seen_challenges = {} + for unlock in challenge_unlocks: + seen_challenges[unlock['name']] = True + for unlock in unlockable_challenge_strings: + if unlock not in seen_challenges: + challenge_unlocks.append(dict([('dlc_id', 0), ('is_from_dlc', 0), ('name', unlock)])) + inverted_structure = invert_structure(save_structure[38][2]) + data = [write_protobuf(remove_structure(d, inverted_structure)) for d in challenge_unlocks] + player[38] = [[2, d] for d in data] if changes.has_key("challenges"): data = unwrap_challenges(player[15][0][1]) From 272b4d26b16a668d068d9682f817c4a8a90f8f5d Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Thu, 9 Apr 2015 17:27:43 -0500 Subject: [PATCH 005/103] Streamline our challenge unlock, a bit. --- savefile.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/savefile.py b/savefile.py index bdb5a8e..6a0546a 100755 --- a/savefile.py +++ b/savefile.py @@ -1839,15 +1839,16 @@ def modify_save(data, changes, endian=1): player[7][0][1] = 1 if "challenges" in unlocks: challenge_unlocks = [apply_structure(read_protobuf(d[1]), save_structure[38][2]) for d in player[38]] + inverted_structure = invert_structure(save_structure[38][2]) seen_challenges = {} for unlock in challenge_unlocks: seen_challenges[unlock['name']] = True for unlock in unlockable_challenge_strings: if unlock not in seen_challenges: - challenge_unlocks.append(dict([('dlc_id', 0), ('is_from_dlc', 0), ('name', unlock)])) - inverted_structure = invert_structure(save_structure[38][2]) - data = [write_protobuf(remove_structure(d, inverted_structure)) for d in challenge_unlocks] - player[38] = [[2, d] for d in data] + player[38].append([2, write_protobuf(remove_structure(dict([ + ('dlc_id', 0), + ('is_from_dlc', 0), + ('name', unlock)]), inverted_structure))]) if changes.has_key("challenges"): data = unwrap_challenges(player[15][0][1]) From 2cbaf1199c20f21862866c40ca640a2425c22cbf Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Fri, 10 Apr 2015 00:45:12 -0500 Subject: [PATCH 006/103] Some changes to how we unlock challenges. Technically more robust, even if it'll probably never make use of that robustness. Still, we've also got the official BL2 challenge names associated with the challenges, so there's that. --- savefile.py | 679 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 475 insertions(+), 204 deletions(-) diff --git a/savefile.py b/savefile.py index 6a0546a..9766eb2 100755 --- a/savefile.py +++ b/savefile.py @@ -463,61 +463,42 @@ def wrap_black_market(value): sdus = [value[k] for k in black_market_keys[: len(value)]] return write_repeated_protobuf_value(sdus, 0) -# Unlockable challenge strings (for the challenges which only show up -# after entering certain areas, or after completing specific other -# challenges, etc) -unlockable_challenge_strings = ["GD_Challenges.enemies.Enemies_KillVarkidPods", - "GD_Challenges.enemies.Enemies_KillSpiderants", - "GD_Challenges.enemies.Enemies_KillGyros", - "GD_Challenges.enemies.Enemies_KillVarkid", - "GD_Challenges.enemies.Enemies_KillSkags", - "GD_Challenges.enemies.Enemies_KillCrystalisks", - "GD_Challenges.enemies.Enemies_KillStalkers", - "GD_Challenges.enemies.Enemies_KillThreshers", - "GD_Challenges.Player.Player_SecondWindFromCorrosive", - "GD_Challenges.Player.Player_SecondWindFromFire", - "GD_Challenges.Player.Player_SecondWindFromShock", - "GD_Challenges.Weapons.Launcher_KillsSplashDamage", - "GD_Challenges.Weapons.Launcher_KillsFullShieldEnemy", - "GD_Challenges.Weapons.Launcher_KillsDirectHit", - "GD_Challenges.Weapons.Launcher_KillsLongRange", - "GD_Challenges.Weapons.SniperRifle_KillsFullShieldEnemy", - "GD_Challenges.Weapons.SniperRifle_KillsFromHip", - "GD_Challenges.Weapons.SniperRifle_KillsUnaware", - "GD_Challenges.Weapons.Sniper_CriticalHitKills", - "GD_Challenges.Weapons.SMG_CriticalHitKills", - "GD_Challenges.Weapons.AssaultRifle_KillsCrouched", - "GD_Challenges.Weapons.AssaultRifle_CriticalHitKills", - "GD_Challenges.Weapons.Shotgun_KillsLongRange", - "GD_Challenges.Weapons.Shotgun_KillsPointBlank", - "GD_Challenges.Weapons.Shotgun_CriticalHitKills", - "GD_Challenges.Weapons.Pistol_KillsQuickshot", - "GD_Challenges.Weapons.Pistol_CriticalHitKills", - "GD_Challenges.Melee.Melee_KillsBladed", - ] +class ChallengeCat(object): + """ + Simple little class to hold information about challenge + categories. Mostly just a glorified dict. + """ + + def __init__(self, name, dlc=0): + self.name = name + self.dlc = dlc + if self.dlc == 0: + self.is_from_dlc = 0 + else: + self.is_from_dlc = 1 # Challenge categories -challenge_cat_dlc3 = "Hammerlock's Hunt" -challenge_cat_dlc2 = "Campaign of Carnage" -challenge_cat_dlc4 = "Dragon Keep" -challenge_cat_dlc1 = "Pirate's Booty" -challenge_cat_enemies = "Enemies" -challenge_cat_elemental = "Elemental" -challenge_cat_loot = "Loot" -challenge_cat_money = "Money and Trading" -challenge_cat_vehicle = "Vehicle" -challenge_cat_health = "Health and Recovery" -challenge_cat_grenades = "Grenades" -challenge_cat_shields = "Shields" -challenge_cat_rockets = "Rocket Launcher" -challenge_cat_sniper = "Sniper Rifle" -challenge_cat_ar = "Assault Rifle" -challenge_cat_smg = "SMG" -challenge_cat_shotgun = "Shotgun" -challenge_cat_pistol = "Pistol" -challenge_cat_melee = "Melee" -challenge_cat_combat = "General Combat" -challenge_cat_misc = "Miscellaneous" +challenge_cat_dlc4 = ChallengeCat("Hammerlock's Hunt", 4) +challenge_cat_dlc3 = ChallengeCat("Campaign of Carnage", 3) +challenge_cat_dlc9 = ChallengeCat("Dragon Keep", 9) +challenge_cat_dlc1 = ChallengeCat("Pirate's Booty", 1) +challenge_cat_enemies = ChallengeCat("Enemies") +challenge_cat_elemental = ChallengeCat("Elemental") +challenge_cat_loot = ChallengeCat("Loot") +challenge_cat_money = ChallengeCat("Money and Trading") +challenge_cat_vehicle = ChallengeCat("Vehicle") +challenge_cat_health = ChallengeCat("Health and Recovery") +challenge_cat_grenades = ChallengeCat("Grenades") +challenge_cat_shields = ChallengeCat("Shields") +challenge_cat_rockets = ChallengeCat("Rocket Launcher") +challenge_cat_sniper = ChallengeCat("Sniper Rifle") +challenge_cat_ar = ChallengeCat("Assault Rifle") +challenge_cat_smg = ChallengeCat("SMG") +challenge_cat_shotgun = ChallengeCat("Shotgun") +challenge_cat_pistol = ChallengeCat("Pistol") +challenge_cat_melee = ChallengeCat("Melee") +challenge_cat_combat = ChallengeCat("General Combat") +challenge_cat_misc = ChallengeCat("Miscellaneous") class Challenge(object): """ @@ -525,9 +506,10 @@ class Challenge(object): challenges. This is *mostly* just a glorified dict. """ - def __init__(self, position, identifier, cat, name, description, levels, bonus=None): + def __init__(self, position, identifier, id_text, cat, name, description, levels, bonus=None): self.position = position self.identifier = identifier + self.id_text = id_text self.cat = cat self.name = name self.description = description @@ -572,651 +554,939 @@ def get_bonus(self): challenges = {} # Hammerlock DLC Challenges -challenges[1752] = Challenge(305, 1752, challenge_cat_dlc3, +challenges[1752] = Challenge(305, 1752, + "GD_Sage_Challenges.Challenges.Challenge_Sage_KillSavages", + challenge_cat_dlc4, "Savage Bloody Savage", "Kill savages", (20, 50, 100, 250, 500)) -challenges[1750] = Challenge(303, 1750, challenge_cat_dlc3, +challenges[1750] = Challenge(303, 1750, + "GD_Sage_Challenges.Challenges.Challenge_Sage_KillDrifters", + challenge_cat_dlc4, "Harder They Fall", "Kill drifters", (5, 15, 30, 40, 50)) -challenges[1751] = Challenge(304, 1751, challenge_cat_dlc3, +challenges[1751] = Challenge(304, 1751, + "GD_Sage_Challenges.Challenges.Challenge_Sage_KillFanBoats", + challenge_cat_dlc4, "Fan Boy", "Kill Fan Boats", (5, 10, 15, 20, 30)) -challenges[1753] = Challenge(306, 1753, challenge_cat_dlc3, +challenges[1753] = Challenge(306, 1753, + "GD_Sage_Challenges.Challenges.Challenge_Sage_RaidBossA", + challenge_cat_dlc4, "Voracidous the Invincible", "Defeat Voracidous the Invincible", (1, 3, 5, 10, 15)) -challenges[1952] = Challenge(307, 1952, challenge_cat_dlc3, +challenges[1952] = Challenge(307, 1952, + "GD_Sage_Challenges.Challenges.Challenge_Sage_KillBoroks", + challenge_cat_dlc4, "Boroking Around", "kill boroks", (10, 20, 50, 80, 120)) -challenges[1953] = Challenge(308, 1953, challenge_cat_dlc3, +challenges[1953] = Challenge(308, 1953, + "GD_Sage_Challenges.Challenges.Challenge_Sage_KillScaylions", + challenge_cat_dlc4, "Stinging Sensation", "Kill scaylions", (10, 20, 50, 80, 120)) # Torgue DLC Challenges -challenges[1756] = Challenge(310, 1756, challenge_cat_dlc2, +challenges[1756] = Challenge(310, 1756, + "GD_Iris_Challenges.Challenges.Challenge_Iris_KillMotorcycles", + challenge_cat_dlc3, "Bikes Destroyed", "Destroy Bikes", (10, 20, 30, 50, 80)) -challenges[1757] = Challenge(311, 1757, challenge_cat_dlc2, +challenges[1757] = Challenge(311, 1757, + "GD_Iris_Challenges.Challenges.Challenge_Iris_KillBikers", + challenge_cat_dlc3, "Bikers Killed", "Bikers Killed", (50, 100, 150, 200, 250)) -challenges[1950] = Challenge(316, 1950, challenge_cat_dlc2, +challenges[1950] = Challenge(316, 1950, + "GD_Iris_Challenges.Challenges.Challenge_Iris_TorgueTokens", + challenge_cat_dlc3, "Torgue Tokens Acquired", "Acquire Torgue Tokens", (100, 250, 500, 750, 1000)) -challenges[1949] = Challenge(315, 1949, challenge_cat_dlc2, +challenges[1949] = Challenge(315, 1949, + "GD_Iris_Challenges.Challenges.Challenge_Iris_BuyTorgueItems", + challenge_cat_dlc3, "Torgue Items Purchased", "Purchase Torgue Items with Tokens", (2, 5, 8, 12, 15)) -challenges[1758] = Challenge(312, 1758, challenge_cat_dlc2, +challenges[1758] = Challenge(312, 1758, + "GD_Iris_Challenges.Challenges.Challenge_Iris_CompleteBattles", + challenge_cat_dlc3, "Battles Completed", "Complete All Battles", (1, 4, 8, 12)) -challenges[1759] = Challenge(313, 1759, challenge_cat_dlc2, +challenges[1759] = Challenge(313, 1759, + "GD_Iris_Challenges.Challenges.Challenge_Iris_Raid1", + challenge_cat_dlc3, "Pete The Invincible Defeated", "Defeat Pete the Invincible", (1, 3, 5, 10, 15)) # Tiny Tina DLC Challenges -challenges[1954] = Challenge(318, 1954, challenge_cat_dlc4, +challenges[1954] = Challenge(318, 1954, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillDwarves", + challenge_cat_dlc9, "Scot-Free", "Kill dwarves", (50, 100, 150, 200, 250)) -challenges[1768] = Challenge(320, 1768, challenge_cat_dlc4, +challenges[1768] = Challenge(320, 1768, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillGolems", + challenge_cat_dlc9, "Rock Out With Your Rock Out", "Kill golems", (10, 25, 50, 80, 120)) -challenges[1769] = Challenge(321, 1769, challenge_cat_dlc4, +challenges[1769] = Challenge(321, 1769, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillKnights", + challenge_cat_dlc9, "Knighty Knight", "Kill knights", (10, 25, 75, 120, 175)) -challenges[1771] = Challenge(323, 1771, challenge_cat_dlc4, +challenges[1771] = Challenge(323, 1771, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillOrcs", + challenge_cat_dlc9, "Orcs Should Perish", "Kill orcs", (50, 100, 150, 200, 250)) -challenges[1772] = Challenge(324, 1772, challenge_cat_dlc4, +challenges[1772] = Challenge(324, 1772, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillSkeletons", + challenge_cat_dlc9, "Bone Breaker", "Kill skeletons", (50, 100, 150, 200, 250)) -challenges[1773] = Challenge(325, 1773, challenge_cat_dlc4, +challenges[1773] = Challenge(325, 1773, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillSpiders", + challenge_cat_dlc9, "Ew Ew Ew Ew", "Kill spiders", (25, 50, 100, 150, 200)) -challenges[1774] = Challenge(326, 1774, challenge_cat_dlc4, +challenges[1774] = Challenge(326, 1774, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillTreants", + challenge_cat_dlc9, "Cheerful Green Giants", "Kill treants", (10, 20, 50, 80, 120)) -challenges[1775] = Challenge(327, 1775, challenge_cat_dlc4, +challenges[1775] = Challenge(327, 1775, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillWizards", + challenge_cat_dlc9, "Magical Massacre", "Kill wizards", (10, 20, 50, 80, 120)) -challenges[1754] = Challenge(317, 1754, challenge_cat_dlc4, +challenges[1754] = Challenge(317, 1754, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillDragons", + challenge_cat_dlc9, "Fus Roh Die", "Kill dragons", (10, 20, 50, 80, 110)) -challenges[1770] = Challenge(322, 1770, challenge_cat_dlc4, +challenges[1770] = Challenge(322, 1770, + "GD_Aster_Challenges.Challenges.Challenge_Aster_KillMimics", + challenge_cat_dlc9, "Can't Fool Me", "Kill mimics", (5, 15, 30, 50, 75)) # Captain Scarlett DLC Challenges -challenges[1743] = Challenge(298, 1743, challenge_cat_dlc1, +challenges[1743] = Challenge(298, 1743, + "GD_Orchid_Challenges.Challenges.Challenge_Orchid_Crystals", + challenge_cat_dlc1, "In The Pink", "Collect Seraph Crystals", (80, 160, 240, 320, 400)) -challenges[1755] = Challenge(299, 1755, challenge_cat_dlc1, +challenges[1755] = Challenge(299, 1755, + "GD_Orchid_Challenges.Challenges.Challenge_Orchid_Purchase", + challenge_cat_dlc1, "Shady Dealings", "Purchase Items With Seraph Crystals", (1, 3, 5, 10, 15)) -challenges[1745] = Challenge(294, 1745, challenge_cat_dlc1, +challenges[1745] = Challenge(294, 1745, + "GD_Orchid_Challenges.Challenges.Challenge_Orchid_KillWorms", + challenge_cat_dlc1, "Worm Killer", "Kill Sand Worms", (10, 20, 30, 50, 80)) -challenges[1746] = Challenge(295, 1746, challenge_cat_dlc1, +challenges[1746] = Challenge(295, 1746, + "GD_Orchid_Challenges.Challenges.Challenge_Orchid_KillBandits", + challenge_cat_dlc1, "Land Lubber", "Kill Pirates", (50, 100, 150, 200, 250)) -challenges[1747] = Challenge(296, 1747, challenge_cat_dlc1, +challenges[1747] = Challenge(296, 1747, + "GD_Orchid_Challenges.Challenges.Challenge_Orchid_KillHovercrafts", + challenge_cat_dlc1, "Hovernator", "Destroy Pirate Hovercrafts", (5, 10, 15, 20, 30)) -challenges[1748] = Challenge(297, 1748, challenge_cat_dlc1, +challenges[1748] = Challenge(297, 1748, + "GD_Orchid_Challenges.Challenges.Challenge_Orchid_PirateChests", + challenge_cat_dlc1, "Pirate Booty", "Open Pirate Chests", (25, 75, 150, 250, 375)) -challenges[1742] = Challenge(292, 1742, challenge_cat_dlc1, +challenges[1742] = Challenge(292, 1742, + "GD_Orchid_Challenges.Challenges.Challenge_Orchid_Raid1", + challenge_cat_dlc1, "Hyperius the Not-So-Invincible", "Divide Hyperius by zero", (1, 3, 5, 10, 15)) -challenges[1744] = Challenge(293, 1744, challenge_cat_dlc1, +challenges[1744] = Challenge(293, 1744, + "GD_Orchid_Challenges.Challenges.Challenge_Orchid_Raid3", + challenge_cat_dlc1, "Master Worm Food", "Feed Master Gee to his worms", (1, 3, 5, 10, 15)) # Enemies -challenges[1632] = Challenge(24, 1632, challenge_cat_enemies, +challenges[1632] = Challenge(24, 1632, + "GD_Challenges.enemies.Enemies_KillSkags", + challenge_cat_enemies, "Skags to Riches", "Kill skags", (10, 25, 75, 150, 300)) -challenges[1675] = Challenge(84, 1675, challenge_cat_enemies, +challenges[1675] = Challenge(84, 1675, + "GD_Challenges.enemies.Enemies_KillConstructors", + challenge_cat_enemies, "Constructor Destructor", "Kill constructors", (5, 12, 20, 30, 50)) -challenges[1655] = Challenge(80, 1655, challenge_cat_enemies, +challenges[1655] = Challenge(80, 1655, + "GD_Challenges.enemies.Enemies_KillLoaders", + challenge_cat_enemies, "Load and Lock", "Kill loaders", (20, 100, 500, 1000, 1500), bonus=3) -challenges[1651] = Challenge(76, 1651, challenge_cat_enemies, +challenges[1651] = Challenge(76, 1651, + "GD_Challenges.enemies.Enemies_KillBullymongs", + challenge_cat_enemies, "Bully the Bullies", "Kill bullymongs", (25, 50, 150, 300, 750)) -challenges[1652] = Challenge(77, 1652, challenge_cat_enemies, +challenges[1652] = Challenge(77, 1652, + "GD_Challenges.enemies.Enemies_KillCrystalisks", + challenge_cat_enemies, "Crystals are a Girl's Best Friend", "Kill crystalisks", (10, 25, 50, 80, 120)) -challenges[1653] = Challenge(78, 1653, challenge_cat_enemies, +challenges[1653] = Challenge(78, 1653, + "GD_Challenges.enemies.Enemies_KillGoliaths", + challenge_cat_enemies, "WHY SO MUCH HURT?!", "Kill goliaths", (10, 25, 50, 80, 120)) -challenges[1654] = Challenge(79, 1654, challenge_cat_enemies, +challenges[1654] = Challenge(79, 1654, + "GD_Challenges.enemies.Enemies_KillEngineers", + challenge_cat_enemies, "Paingineering", "Kill Hyperion personnel", (10, 25, 75, 150, 300)) -challenges[1658] = Challenge(83, 1658, challenge_cat_enemies, +challenges[1658] = Challenge(83, 1658, + "GD_Challenges.enemies.Enemies_KillSurveyors", + challenge_cat_enemies, "Just a Moment of Your Time...", "Kill surveyors", (10, 25, 75, 150, 300)) -challenges[1694] = Challenge(87, 1694, challenge_cat_enemies, +challenges[1694] = Challenge(87, 1694, + "GD_Challenges.enemies.Enemies_KillNomads", + challenge_cat_enemies, "You (No)Mad, Bro?", "Kill nomads", (10, 25, 75, 150, 300)) -challenges[1695] = Challenge(88, 1695, challenge_cat_enemies, +challenges[1695] = Challenge(88, 1695, + "GD_Challenges.enemies.Enemies_KillPsychos", + challenge_cat_enemies, "Mama's Boys", "Kill psychos", (50, 100, 150, 300, 500)) -challenges[1696] = Challenge(89, 1696, challenge_cat_enemies, +challenges[1696] = Challenge(89, 1696, + "GD_Challenges.enemies.Enemies_KillRats", + challenge_cat_enemies, "You Dirty Rat", "Kill rats. Yes, really.", (10, 25, 75, 150, 300)) -challenges[1791] = Challenge(93, 1791, challenge_cat_enemies, +challenges[1791] = Challenge(93, 1791, + "GD_Challenges.enemies.Enemies_KillSpiderants", + challenge_cat_enemies, "Pest Control", "Kill spiderants", (10, 25, 75, 150, 300)) -challenges[1792] = Challenge(94, 1792, challenge_cat_enemies, +challenges[1792] = Challenge(94, 1792, + "GD_Challenges.enemies.Enemies_KillStalkers", + challenge_cat_enemies, "You're One Ugly Mother...", "Kill stalkers", (10, 25, 75, 150, 300)) -challenges[1793] = Challenge(95, 1793, challenge_cat_enemies, +challenges[1793] = Challenge(95, 1793, + "GD_Challenges.enemies.Enemies_KillThreshers", + challenge_cat_enemies, "Tentacle Obsession", "Kill threshers", (10, 25, 75, 150, 300)) -challenges[1693] = Challenge(86, 1693, challenge_cat_enemies, +challenges[1693] = Challenge(86, 1693, + "GD_Challenges.enemies.Enemies_KillMarauders", + challenge_cat_enemies, "Marauder? I Hardly Know 'Er", "Kill marauders", (20, 100, 500, 1000, 1500), bonus=3) -challenges[1794] = Challenge(96, 1794, challenge_cat_enemies, +challenges[1794] = Challenge(96, 1794, + "GD_Challenges.enemies.Enemies_KillVarkid", + challenge_cat_enemies, "Another Bug Hunt", "Kill varkids", (10, 25, 75, 150, 300)) -challenges[1795] = Challenge(97, 1795, challenge_cat_enemies, +challenges[1795] = Challenge(97, 1795, + "GD_Challenges.enemies.Enemies_KillGyros", + challenge_cat_enemies, "Die in the Friendly Skies", "Kill buzzards", (10, 25, 45, 70, 100)) -challenges[1796] = Challenge(98, 1796, challenge_cat_enemies, +challenges[1796] = Challenge(98, 1796, + "GD_Challenges.enemies.Enemies_KillMidgets", + challenge_cat_enemies, "Little Person, Big Pain", "Kill midgets", (10, 25, 75, 150, 300)) -challenges[1895] = Challenge(249, 1895, challenge_cat_enemies, +challenges[1895] = Challenge(249, 1895, + "GD_Challenges.enemies.Enemies_ShootBullymongProjectiles", + challenge_cat_enemies, "Hurly Burly", "Shoot bullymong-tossed projectiles out of midair", (10, 25, 50, 125, 250)) -challenges[1896] = Challenge(250, 1896, challenge_cat_enemies, +challenges[1896] = Challenge(250, 1896, + "GD_Challenges.enemies.Enemies_ReleaseChainedMidgets", + challenge_cat_enemies, "Short-Chained", "Shoot chains to release midgets from shields", (1, 5, 15, 30, 50)) -challenges[1934] = Challenge(99, 1934, challenge_cat_enemies, +challenges[1934] = Challenge(99, 1934, + "GD_Challenges.enemies.Enemies_KillBruisers", + challenge_cat_enemies, "Cruising for a Bruising", "Kill bruisers", (10, 25, 75, 150, 300)) -challenges[1732] = Challenge(91, 1732, challenge_cat_enemies, +challenges[1732] = Challenge(91, 1732, + "GD_Challenges.enemies.Enemies_KillVarkidPods", + challenge_cat_enemies, "Pod Pew Pew", "Kill varkid pods before they hatch", (10, 25, 45, 70, 100)) # Elemental -challenges[1873] = Challenge(225, 1873, challenge_cat_elemental, +challenges[1873] = Challenge(225, 1873, + "GD_Challenges.elemental.Elemental_SetEnemiesOnFire", + challenge_cat_elemental, "Cowering Inferno", "Ignite enemies", (25, 100, 400, 1000, 2000)) -challenges[1642] = Challenge(40, 1642, challenge_cat_elemental, +challenges[1642] = Challenge(40, 1642, + "GD_Challenges.elemental.Elemental_KillEnemiesCorrosive", + challenge_cat_elemental, "Acid Trip", "Kill enemies with corrode damage", (20, 75, 250, 600, 1000)) -challenges[1645] = Challenge(43, 1645, challenge_cat_elemental, +challenges[1645] = Challenge(43, 1645, + "GD_Challenges.elemental.Elemental_KillEnemiesExplosive", + challenge_cat_elemental, "Boom.", "Kill enemies with explosive damage", (20, 75, 250, 600, 1000), bonus=3) -challenges[1877] = Challenge(229, 1877, challenge_cat_elemental, +challenges[1877] = Challenge(229, 1877, + "GD_Challenges.elemental.Elemental_DealFireDOTDamage", + challenge_cat_elemental, "I Just Want to Set the World on Fire", "Deal burn damage", (2500, 20000, 100000, 500000, 1000000), bonus=5) -challenges[1878] = Challenge(230, 1878, challenge_cat_elemental, +challenges[1878] = Challenge(230, 1878, + "GD_Challenges.elemental.Elemental_DealCorrosiveDOTDamage", + challenge_cat_elemental, "Corroderate", "Deal corrode damage", (2500, 20000, 100000, 500000, 1000000)) -challenges[1879] = Challenge(231, 1879, challenge_cat_elemental, +challenges[1879] = Challenge(231, 1879, + "GD_Challenges.elemental.Elemental_DealShockDOTDamage", + challenge_cat_elemental, 'Say "Watt" Again', "Deal electrocute damage", (5000, 20000, 100000, 500000, 1000000)) -challenges[1880] = Challenge(232, 1880, challenge_cat_elemental, +challenges[1880] = Challenge(232, 1880, + "GD_Challenges.elemental.Elemental_DealBonusSlagDamage", + challenge_cat_elemental, "Slag-Licked", "Deal bonus damage to Slagged enemies", (5000, 25000, 150000, 1000000, 5000000), bonus=3) # Loot -challenges[1898] = Challenge(251, 1898, challenge_cat_loot, +challenges[1898] = Challenge(251, 1898, + "GD_Challenges.Pickups.Inventory_PickupWhiteItems", + challenge_cat_loot, "Another Man's Treasure", "Loot or purchase white items", (50, 125, 250, 400, 600)) -challenges[1899] = Challenge(252, 1899, challenge_cat_loot, +challenges[1899] = Challenge(252, 1899, + "GD_Challenges.Pickups.Inventory_PickupGreenItems", + challenge_cat_loot, "It's Not Easy Looting Green", "Loot or purchase green items", (20, 50, 75, 125, 200), bonus=3) -challenges[1900] = Challenge(253, 1900, challenge_cat_loot, +challenges[1900] = Challenge(253, 1900, + "GD_Challenges.Pickups.Inventory_PickupBlueItems", + challenge_cat_loot, "I Like My Treasure Rare", "Loot or purchase blue items", (5, 12, 20, 30, 45)) -challenges[1901] = Challenge(254, 1901, challenge_cat_loot, +challenges[1901] = Challenge(254, 1901, + "GD_Challenges.Pickups.Inventory_PickupPurpleItems", + challenge_cat_loot, "Purple Reign", "Loot or purchase purple items", (2, 4, 7, 12, 20)) -challenges[1902] = Challenge(255, 1902, challenge_cat_loot, +challenges[1902] = Challenge(255, 1902, + "GD_Challenges.Pickups.Inventory_PickupOrangeItems", + challenge_cat_loot, "Nothing Rhymes with Orange", "Loot or purchase orange items", (1, 3, 6, 10, 15), bonus=5) -challenges[1669] = Challenge(108, 1669, challenge_cat_loot, +challenges[1669] = Challenge(108, 1669, + "GD_Challenges.Loot.Loot_OpenChests", + challenge_cat_loot, "The Call of Booty", "Open treasure chests", (5, 25, 50, 125, 250)) -challenges[1670] = Challenge(109, 1670, challenge_cat_loot, +challenges[1670] = Challenge(109, 1670, + "GD_Challenges.Loot.Loot_OpenLootables", + challenge_cat_loot, "Open Pandora's Boxes", "Open lootable chests, lockers, and other objects", (50, 250, 750, 1500, 2500), bonus=3) -challenges[1630] = Challenge(8, 1630, challenge_cat_loot, +challenges[1630] = Challenge(8, 1630, + "GD_Challenges.Loot.Loot_PickUpWeapons", + challenge_cat_loot, "Gun Runner", "Pick up or purchase weapons", (10, 25, 150, 300, 750)) # Money -challenges[1858] = Challenge(118, 1858, challenge_cat_money, +challenges[1858] = Challenge(118, 1858, + "GD_Challenges.Economy.Economy_MoneySaved", + challenge_cat_money, "For the Hoard!", "Save a lot of money", (10000, 50000, 250000, 1000000, 3000000), bonus=3) -challenges[1859] = Challenge(119, 1859, challenge_cat_money, +challenges[1859] = Challenge(119, 1859, + "GD_Challenges.Economy.General_MoneyFromCashDrops", + challenge_cat_money, "Dolla Dolla Bills, Y'all", "Collect dollars from cash drops", (5000, 25000, 125000, 500000, 1000000)) -challenges[1678] = Challenge(112, 1678, challenge_cat_money, +challenges[1678] = Challenge(112, 1678, + "GD_Challenges.Economy.Economy_SellItems", + challenge_cat_money, "Wholesale", "Sell items to vending machines", (10, 25, 150, 300, 750)) -challenges[1860] = Challenge(113, 1860, challenge_cat_money, +challenges[1860] = Challenge(113, 1860, + "GD_Challenges.Economy.Economy_PurchaseItemsOfTheDay", + challenge_cat_money, "Limited-Time Offer", "Buy Items of the Day", (1, 5, 15, 30, 50)) -challenges[1810] = Challenge(111, 1810, challenge_cat_money, +challenges[1810] = Challenge(111, 1810, + "GD_Challenges.Economy.Economy_BuyItemsWithEridium", + challenge_cat_money, "Whaddaya Buyin'?", "Purchase items with Eridium", (2, 5, 9, 14, 20), bonus=4) -challenges[1805] = Challenge(214, 1805, challenge_cat_money, +challenges[1805] = Challenge(214, 1805, + "GD_Challenges.Economy.Trade_ItemsWithPlayers", + challenge_cat_money, "Psst, Hey Buddy...", "Trade with other players", (1, 5, 15, 30, 50)) # Vehicle -challenges[1640] = Challenge(37, 1640, challenge_cat_vehicle, +challenges[1640] = Challenge(37, 1640, + "GD_Challenges.Vehicles.Vehicles_KillByRamming", + challenge_cat_vehicle, "Hit-and-Fun", "Kill enemies by ramming them with a vehicle", (5, 10, 50, 100, 200)) -challenges[1920] = Challenge(275, 1920, challenge_cat_vehicle, +challenges[1920] = Challenge(275, 1920, + "GD_Challenges.Vehicles.Vehicles_KillByPowerSlide", + challenge_cat_vehicle, "Blue Sparks", "Kill enemies by power-sliding over them in a vehicle", (5, 15, 30, 50, 75), bonus=3) -challenges[1641] = Challenge(38, 1641, challenge_cat_vehicle, +challenges[1641] = Challenge(38, 1641, + "GD_Challenges.Vehicles.Vehicles_KillsWithVehicleWeapon", + challenge_cat_vehicle, "Turret Syndrome", "Kill enemies using a turret or vehicle-mounted weapon", (10, 25, 150, 300, 750)) -challenges[1922] = Challenge(277, 1922, challenge_cat_vehicle, +challenges[1922] = Challenge(277, 1922, + "GD_Challenges.Vehicles.Vehicles_VehicleKillsVehicle", + challenge_cat_vehicle, "...One Van Leaves", "Kill vehicles while in a vehicle", (5, 10, 50, 100, 200)) -challenges[1919] = Challenge(274, 1919, challenge_cat_vehicle, +challenges[1919] = Challenge(274, 1919, + "GD_Challenges.Vehicles.Vehicles_KillsWhilePassenger", + challenge_cat_vehicle, "Passive Aggressive", "Kill enemies while riding as a passenger (not a gunner) in a vehicle", (1, 10, 50, 100, 200)) # Health -challenges[1917] = Challenge(270, 1917, challenge_cat_health, +challenges[1917] = Challenge(270, 1917, + "GD_Challenges.Player.Player_PointsHealed", + challenge_cat_health, "Heal Plz", "Recover health", (1000, 25000, 150000, 1000000, 5000000)) -challenges[1865] = Challenge(200, 1865, challenge_cat_health, +challenges[1865] = Challenge(200, 1865, + "GD_Challenges.Player.Player_SecondWind", + challenge_cat_health, "I'll Just Help Myself", "Get Second Winds by killing an enemy", (5, 10, 50, 100, 200)) -challenges[1866] = Challenge(201, 1866, challenge_cat_health, +challenges[1866] = Challenge(201, 1866, + "GD_Challenges.Player.Player_SecondWindFromBadass", + challenge_cat_health, "Badass Bingo", "Get Second Winds by killing a badass enemy", (1, 5, 15, 30, 50), bonus=5) -challenges[1868] = Challenge(204, 1868, challenge_cat_health, +challenges[1868] = Challenge(204, 1868, + "GD_Challenges.Player.Player_CoopRevivesOfFriends", + challenge_cat_health, "This is No Time for Lazy!", "Revive a co-op partner", (5, 10, 50, 100, 200), bonus=5) -challenges[1834] = Challenge(198, 1834, challenge_cat_health, +challenges[1834] = Challenge(198, 1834, + "GD_Challenges.Player.Player_SecondWindFromFire", + challenge_cat_health, "Death, Wind, and Fire", "Get Second Winds by killing enemies with a burn DoT (damage over time)", (1, 5, 15, 30, 50)) -challenges[1833] = Challenge(197, 1833, challenge_cat_health, +challenges[1833] = Challenge(197, 1833, + "GD_Challenges.Player.Player_SecondWindFromCorrosive", + challenge_cat_health, "Green Meanie", "Get Second Winds by killing enemies with a corrosive DoT (damage over time)", (1, 5, 15, 30, 50)) -challenges[1835] = Challenge(199, 1835, challenge_cat_health, +challenges[1835] = Challenge(199, 1835, + "GD_Challenges.Player.Player_SecondWindFromShock", + challenge_cat_health, "I'm Back! Shocked?", "Get Second Winds by killing enemies with an electrocute DoT (damage over time)", (1, 5, 15, 30, 50)) # Grenades -challenges[1639] = Challenge(31, 1639, challenge_cat_grenades, +challenges[1639] = Challenge(31, 1639, + "GD_Challenges.Grenades.Grenade_Kills", + challenge_cat_grenades, "Pull the Pin", "Kill enemies with grenades", (10, 25, 150, 300, 750), bonus=3) -challenges[1886] = Challenge(238, 1886, challenge_cat_grenades, +challenges[1886] = Challenge(238, 1886, + "GD_Challenges.Grenades.Grenade_KillsSingularityType", + challenge_cat_grenades, "Singled Out", "Kill enemies with Singularity grenades", (10, 25, 75, 150, 300)) -challenges[1885] = Challenge(237, 1885, challenge_cat_grenades, +challenges[1885] = Challenge(237, 1885, + "GD_Challenges.Grenades.Grenade_KillsMirvType", + challenge_cat_grenades, "EXPLOOOOOSIONS!", "Kill enemies with Mirv grenades", (10, 25, 75, 150, 300), bonus=3) -challenges[1883] = Challenge(235, 1883, challenge_cat_grenades, +challenges[1883] = Challenge(235, 1883, + "GD_Challenges.Grenades.Grenade_KillsAoEoTType", + challenge_cat_grenades, "Chemical Sprayer", "Kill enemies with Area-of-Effect grenades", (10, 25, 75, 150, 300)) -challenges[1884] = Challenge(236, 1884, challenge_cat_grenades, +challenges[1884] = Challenge(236, 1884, + "GD_Challenges.Grenades.Grenade_KillsBouncing", + challenge_cat_grenades, "Whoa, Black Betty", "Kill enemies with Bouncing Betty grenades", (10, 25, 75, 150, 300)) -challenges[1918] = Challenge(239, 1918, challenge_cat_grenades, +challenges[1918] = Challenge(239, 1918, + "GD_Challenges.Grenades.Grenade_KillsTransfusionType", + challenge_cat_grenades, "Health Vampire", "Kill enemies with Transfusion grenades", (10, 25, 75, 150, 300)) # Shields -challenges[1889] = Challenge(243, 1889, challenge_cat_shields, +challenges[1889] = Challenge(243, 1889, + "GD_Challenges.Shields.Shields_KillsNova", + challenge_cat_shields, "Super Novas", "Kill enemies with a Nova shield burst", (5, 10, 50, 100, 200), bonus=3) -challenges[1890] = Challenge(244, 1890, challenge_cat_shields, +challenges[1890] = Challenge(244, 1890, + "GD_Challenges.Shields.Shields_KillsRoid", + challenge_cat_shields, "Roid Rage", 'Kill enemies while buffed by a "Maylay" shield', (5, 10, 50, 100, 200)) -challenges[1891] = Challenge(245, 1891, challenge_cat_shields, +challenges[1891] = Challenge(245, 1891, + "GD_Challenges.Shields.Shields_KillsSpikes", + challenge_cat_shields, "Game of Thorns", "Kill enemies with reflected damage from a Spike shield", (5, 10, 50, 100, 200)) -challenges[1892] = Challenge(246, 1892, challenge_cat_shields, +challenges[1892] = Challenge(246, 1892, + "GD_Challenges.Shields.Shields_KillsImpact", + challenge_cat_shields, "Amp It Up", "Kill enemies while buffed by an Amplify shield", (5, 10, 50, 100, 200)) -challenges[1930] = Challenge(222, 1930, challenge_cat_shields, +challenges[1930] = Challenge(222, 1930, + "GD_Challenges.Shields.Shields_AbsorbAmmo", + challenge_cat_shields, "Ammo Eater", "Absorb enemy ammo with an Absorption shield", (20, 75, 250, 600, 1000), bonus=5) # Rocket Launchers -challenges[1762] = Challenge(32, 1762, challenge_cat_rockets, +challenges[1762] = Challenge(32, 1762, + "GD_Challenges.Weapons.Launcher_Kills", + challenge_cat_rockets, "Rocket and Roll", "Kill enemies with rocket launchers", (10, 50, 100, 250, 500), bonus=3) -challenges[1828] = Challenge(192, 1828, challenge_cat_rockets, +challenges[1828] = Challenge(192, 1828, + "GD_Challenges.Weapons.Launcher_SecondWinds", + challenge_cat_rockets, "Gone with the Second Wind", "Get Second Winds with rocket launchers", (2, 5, 15, 30, 50)) -challenges[1870] = Challenge(224, 1870, challenge_cat_rockets, +challenges[1870] = Challenge(224, 1870, + "GD_Challenges.Weapons.Launcher_KillsSplashDamage", + challenge_cat_rockets, "Splish Splash", "Kill enemies with rocket launcher splash damage", (5, 10, 50, 100, 200)) -challenges[1869] = Challenge(223, 1869, challenge_cat_rockets, +challenges[1869] = Challenge(223, 1869, + "GD_Challenges.Weapons.Launcher_KillsDirectHit", + challenge_cat_rockets, "Catch-a-Rocket!", "Kill enemies with direct hits from rocket launchers", (5, 10, 50, 100, 200), bonus=5) -challenges[1871] = Challenge(54, 1871, challenge_cat_rockets, +challenges[1871] = Challenge(54, 1871, + "GD_Challenges.Weapons.Launcher_KillsFullShieldEnemy", + challenge_cat_rockets, "Shield Basher", "Kill shielded enemies with one rocket each", (5, 15, 35, 75, 125)) -challenges[1808] = Challenge(52, 1808, challenge_cat_rockets, +challenges[1808] = Challenge(52, 1808, + "GD_Challenges.Weapons.Launcher_KillsLongRange", + challenge_cat_rockets, "Sky Rockets in Flight...", "Kill enemies from long range with rocket launchers", (25, 100, 400, 1000, 2000)) # Sniper Rifles -challenges[1636] = Challenge(28, 1636, challenge_cat_sniper, +challenges[1636] = Challenge(28, 1636, + "GD_Challenges.Weapons.SniperRifle_Kills", + challenge_cat_sniper, "Longshot", "Kill enemies with sniper rifles", (20, 100, 500, 2500, 5000), bonus=3) -challenges[1666] = Challenge(178, 1666, challenge_cat_sniper, +challenges[1666] = Challenge(178, 1666, + "GD_Challenges.Weapons.Sniper_CriticalHits", + challenge_cat_sniper, "Longshot Headshot", "Get critical hits with sniper rifles", (25, 100, 400, 1000, 2000)) -challenges[1824] = Challenge(188, 1824, challenge_cat_sniper, +challenges[1824] = Challenge(188, 1824, + "GD_Challenges.Weapons.Sniper_SecondWinds", + challenge_cat_sniper, "Leaf on the Second Wind", "Get Second Winds with sniper rifles", (2, 5, 15, 30, 50)) -challenges[1844] = Challenge(59, 1844, challenge_cat_sniper, +challenges[1844] = Challenge(59, 1844, + "GD_Challenges.Weapons.Sniper_CriticalHitKills", + challenge_cat_sniper, "Snipe Hunting", "Kill enemies with critical hits using sniper rifles", (10, 25, 75, 150, 300)) -challenges[1798] = Challenge(47, 1798, challenge_cat_sniper, +challenges[1798] = Challenge(47, 1798, + "GD_Challenges.Weapons.SniperRifle_KillsFromHip", + challenge_cat_sniper, "No Scope, No Problem", "Kill enemies with sniper rifles without using ironsights", (5, 10, 50, 100, 200)) -challenges[1881] = Challenge(233, 1881, challenge_cat_sniper, +challenges[1881] = Challenge(233, 1881, + "GD_Challenges.Weapons.SniperRifle_KillsUnaware", + challenge_cat_sniper, "Surprise!", "Kill unaware enemies with sniper rifles", (5, 10, 50, 100, 200)) -challenges[1872] = Challenge(55, 1872, challenge_cat_sniper, +challenges[1872] = Challenge(55, 1872, + "GD_Challenges.Weapons.SniperRifle_KillsFullShieldEnemy", + challenge_cat_sniper, "Eviscerated", "Kill shielded enemies with one shot using sniper rifles", (5, 15, 35, 75, 125), bonus=5) # Assault Rifles -challenges[1637] = Challenge(29, 1637, challenge_cat_ar, +challenges[1637] = Challenge(29, 1637, + "GD_Challenges.Weapons.AssaultRifle_Kills", + challenge_cat_ar, "Aggravated Assault", "Kill enemies with assault rifles", (25, 100, 400, 1000, 2000), bonus=3) -challenges[1667] = Challenge(179, 1667, challenge_cat_ar, +challenges[1667] = Challenge(179, 1667, + "GD_Challenges.Weapons.AssaultRifle_CriticalHits", + challenge_cat_ar, "This Is My Rifle...", "Get critical hits with assault rifles", (25, 100, 400, 1000, 2000)) -challenges[1825] = Challenge(189, 1825, challenge_cat_ar, +challenges[1825] = Challenge(189, 1825, + "GD_Challenges.Weapons.AssaultRifle_SecondWinds", + challenge_cat_ar, "From My Cold, Dead Hands", "Get Second Winds with assault rifles", (5, 15, 30, 50, 75)) -challenges[1845] = Challenge(60, 1845, challenge_cat_ar, +challenges[1845] = Challenge(60, 1845, + "GD_Challenges.Weapons.AssaultRifle_CriticalHitKills", + challenge_cat_ar, "... This Is My Gun", "Kill enemies with critical hits using assault rifles", (10, 25, 75, 150, 300)) -challenges[1797] = Challenge(46, 1797, challenge_cat_ar, +challenges[1797] = Challenge(46, 1797, + "GD_Challenges.Weapons.AssaultRifle_KillsCrouched", + challenge_cat_ar, "Crouching Tiger, Hidden Assault Rifle", "Kill enemies with assault rifles while crouched", (25, 75, 400, 1600, 3200), bonus=5) # SMGs -challenges[1635] = Challenge(27, 1635, challenge_cat_smg, +challenges[1635] = Challenge(27, 1635, + "GD_Challenges.Weapons.SMG_Kills", + challenge_cat_smg, "Hail of Bullets", "Kill enemies with SMGs", (25, 100, 400, 1000, 2000), bonus=3) -challenges[1665] = Challenge(177, 1665, challenge_cat_smg, +challenges[1665] = Challenge(177, 1665, + "GD_Challenges.Weapons.SMG_CriticalHits", + challenge_cat_smg, "Constructive Criticism", "Get critical hits with SMGs", (25, 100, 400, 1000, 2000)) -challenges[1843] = Challenge(58, 1843, challenge_cat_smg, +challenges[1843] = Challenge(58, 1843, + "GD_Challenges.Weapons.SMG_CriticalHitKills", + challenge_cat_smg, "High Rate of Ire", "Kill enemies with critical hits using SMGs", (10, 25, 75, 150, 300)) -challenges[1823] = Challenge(187, 1823, challenge_cat_smg, +challenges[1823] = Challenge(187, 1823, + "GD_Challenges.Weapons.SMG_SecondWinds", + challenge_cat_smg, "More Like Submachine FUN", "Get Second Winds with SMGs", (2, 5, 15, 30, 50)) # Shotguns -challenges[1634] = Challenge(26, 1634, challenge_cat_shotgun, +challenges[1634] = Challenge(26, 1634, + "GD_Challenges.Weapons.Shotgun_Kills", + challenge_cat_shotgun, "Shotgun!", "Kill enemies with shotguns", (25, 100, 400, 1000, 2000), bonus=3) -challenges[1664] = Challenge(176, 1664, challenge_cat_shotgun, +challenges[1664] = Challenge(176, 1664, + "GD_Challenges.Weapons.Shotgun_CriticalHits", + challenge_cat_shotgun, "Faceful of Buckshot", "Get critical hits with shotguns", (50, 250, 1000, 2500, 5000)) -challenges[1822] = Challenge(186, 1822, challenge_cat_shotgun, +challenges[1822] = Challenge(186, 1822, + "GD_Challenges.Weapons.Shotgun_SecondWinds", + challenge_cat_shotgun, "Lock, Stock, and...", "Get Second Winds with shotguns", (2, 5, 15, 30, 50)) -challenges[1806] = Challenge(50, 1806, challenge_cat_shotgun, +challenges[1806] = Challenge(50, 1806, + "GD_Challenges.Weapons.Shotgun_KillsPointBlank", + challenge_cat_shotgun, "Open Wide!", "Kill enemies from point-blank range with shotguns", (10, 25, 150, 300, 750)) -challenges[1807] = Challenge(51, 1807, challenge_cat_shotgun, +challenges[1807] = Challenge(51, 1807, + "GD_Challenges.Weapons.Shotgun_KillsLongRange", + challenge_cat_shotgun, "Shotgun Sniper", "Kill enemies from long range with shotguns", (10, 25, 75, 150, 300)) -challenges[1842] = Challenge(57, 1842, challenge_cat_shotgun, +challenges[1842] = Challenge(57, 1842, + "GD_Challenges.Weapons.Shotgun_CriticalHitKills", + challenge_cat_shotgun, "Shotgun Surgeon", "Kill enemies with critical hits using shotguns", (10, 50, 100, 250, 500)) # Pistols -challenges[1633] = Challenge(25, 1633, challenge_cat_pistol, +challenges[1633] = Challenge(25, 1633, + "GD_Challenges.Weapons.Pistol_Kills", + challenge_cat_pistol, "The Killer", "Kill enemies with pistols", (25, 100, 400, 1000, 2000), bonus=3) -challenges[1663] = Challenge(175, 1663, challenge_cat_pistol, +challenges[1663] = Challenge(175, 1663, + "GD_Challenges.Weapons.Pistol_CriticalHits", + challenge_cat_pistol, "Deadeye", "Get critical hits with pistols", (25, 100, 400, 1000, 2000)) -challenges[1821] = Challenge(185, 1821, challenge_cat_pistol, +challenges[1821] = Challenge(185, 1821, + "GD_Challenges.Weapons.Pistol_SecondWinds", + challenge_cat_pistol, "Hard Boiled", "Get Second Winds with pistols", (2, 5, 15, 30, 50)) -challenges[1841] = Challenge(56, 1841, challenge_cat_pistol, +challenges[1841] = Challenge(56, 1841, + "GD_Challenges.Weapons.Pistol_CriticalHitKills", + challenge_cat_pistol, "Pistolero", "Kill enemies with critical hits using pistols", (10, 25, 75, 150, 300)) -challenges[1800] = Challenge(49, 1800, challenge_cat_pistol, +challenges[1800] = Challenge(49, 1800, + "GD_Challenges.Weapons.Pistol_KillsQuickshot", + challenge_cat_pistol, "Quickdraw", "Kill enemies shortly after entering ironsights with a pistol", (10, 25, 150, 300, 750), bonus=5) # Melee -challenges[1650] = Challenge(75, 1650, challenge_cat_melee, +challenges[1650] = Challenge(75, 1650, + "GD_Challenges.Melee.Melee_Kills", + challenge_cat_melee, "Fisticuffs!", "Kill enemies with melee attacks", (25, 100, 400, 1000, 2000), bonus=3) -challenges[1893] = Challenge(247, 1893, challenge_cat_melee, +challenges[1893] = Challenge(247, 1893, + "GD_Challenges.Melee.Melee_KillsBladed", + challenge_cat_melee, "A Squall of Violence", "Kill enemies with melee attacks using bladed guns", (20, 75, 250, 600, 1000)) # General Combat -challenges[1621] = Challenge(0, 1621, challenge_cat_combat, +challenges[1621] = Challenge(0, 1621, + "GD_Challenges.GeneralCombat.General_RoundsFired", + challenge_cat_combat, "Knee-Deep in Brass", "Fire a lot of rounds", (1000, 5000, 10000, 25000, 50000), bonus=5) -challenges[1702] = Challenge(90, 1702, challenge_cat_combat, +challenges[1702] = Challenge(90, 1702, + "GD_Challenges.GeneralCombat.Player_KillsWithActionSkill", + challenge_cat_combat, "...To Pay the Bills", "Kill enemies while using your Action Skill", (20, 75, 250, 600, 1000), bonus=5) -challenges[1916] = Challenge(269, 1916, challenge_cat_combat, +challenges[1916] = Challenge(269, 1916, + "GD_Challenges.GeneralCombat.Kills_AtNight", + challenge_cat_combat, "...I Got to Boogie", "Kill enemies at night", (10, 25, 150, 300, 750)) -challenges[1915] = Challenge(268, 1915, challenge_cat_combat, +challenges[1915] = Challenge(268, 1915, + "GD_Challenges.GeneralCombat.Kills_AtDay", + challenge_cat_combat, "Afternoon Delight", "Kill enemies during the day", (50, 250, 1000, 2500, 5000)) -challenges[1908] = Challenge(261, 1908, challenge_cat_combat, +challenges[1908] = Challenge(261, 1908, + "GD_Challenges.GeneralCombat.Tediore_KillWithReload", + challenge_cat_combat, "Boomerbang", "Kill enemies with Tediore reloads", (5, 10, 50, 100, 200), bonus=5) -challenges[1909] = Challenge(262, 1909, challenge_cat_combat, +challenges[1909] = Challenge(262, 1909, + "GD_Challenges.GeneralCombat.Tediore_DamageFromReloads", + challenge_cat_combat, "Gun Slinger", "Deal damage with Tediore reloads", (5000, 20000, 100000, 500000, 1000000)) -challenges[1912] = Challenge(265, 1912, challenge_cat_combat, +challenges[1912] = Challenge(265, 1912, + "GD_Challenges.GeneralCombat.Barrels_KillEnemies", + challenge_cat_combat, "Not Full of Monkeys", "Kill enemies with stationary barrels", (10, 25, 45, 70, 100), bonus=3) -challenges[1646] = Challenge(44, 1646, challenge_cat_combat, +challenges[1646] = Challenge(44, 1646, + "GD_Challenges.GeneralCombat.Kills_FromCrits", + challenge_cat_combat, "Critical Acclaim", "Kill enemies with critical hits. And rainbows.", (20, 100, 500, 1000, 1500)) # Miscellaneous -challenges[1659] = Challenge(104, 1659, challenge_cat_misc, +challenges[1659] = Challenge(104, 1659, + "GD_Challenges.Dueling.DuelsWon_HatersGonnaHate", + challenge_cat_misc, "Haters Gonna Hate", "Win duels", (1, 5, 15, 30, 50)) -challenges[1804] = Challenge(211, 1804, challenge_cat_misc, +challenges[1804] = Challenge(211, 1804, + "GD_Challenges.Miscellaneous.Missions_SideMissionsCompleted", + challenge_cat_misc, "Sidejacked", "Complete side missions", (5, 15, 30, 55, 90)) -challenges[1803] = Challenge(210, 1803, challenge_cat_misc, +challenges[1803] = Challenge(210, 1803, + "GD_Challenges.Miscellaneous.Missions_OptionalObjectivesCompleted", + challenge_cat_misc, "Compl33tionist", "Complete optional mission objectives", (10, 25, 45, 70, 100)) -challenges[1698] = Challenge(173, 1698, challenge_cat_misc, +challenges[1698] = Challenge(173, 1698, + "GD_Challenges.Miscellaneous.Misc_CompleteChallenges", + challenge_cat_misc, "Yo Dawg I Herd You Like Challenges", "Complete many, many challenges", (5, 25, 50, 100, 200)) -challenges[1940] = Challenge(100, 1940, challenge_cat_misc, +challenges[1940] = Challenge(100, 1940, + "GD_Challenges.Miscellaneous.Misc_JimmyJenkins", + challenge_cat_misc, "JEEEEENKINSSSSSS!!!", "Find and eliminate Jimmy Jenkins", (1, 3, 6, 10, 15), @@ -1290,7 +1560,8 @@ def unwrap_challenges(data): if challenge_dict['id'] in challenges: info = challenges[challenge_dict['id']] - challenge_dict['_category'] = info.cat + challenge_dict['_id_text'] = info.id_text + challenge_dict['_category'] = info.cat.name challenge_dict['_name'] = info.name challenge_dict['_description'] = info.description @@ -1843,12 +2114,12 @@ def modify_save(data, changes, endian=1): seen_challenges = {} for unlock in challenge_unlocks: seen_challenges[unlock['name']] = True - for unlock in unlockable_challenge_strings: - if unlock not in seen_challenges: + for challenge in challenges.values(): + if challenge.id_text not in seen_challenges: player[38].append([2, write_protobuf(remove_structure(dict([ - ('dlc_id', 0), - ('is_from_dlc', 0), - ('name', unlock)]), inverted_structure))]) + ('dlc_id', challenge.cat.dlc), + ('is_from_dlc', challenge.cat.is_from_dlc), + ('name', challenge.id_text)]), inverted_structure))]) if changes.has_key("challenges"): data = unwrap_challenges(player[15][0][1]) From ecb214c822d2751cda3969fab34db598a67e8235 Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Fri, 10 Apr 2015 01:05:22 -0500 Subject: [PATCH 007/103] Add functionality to set the character name and save_game_id --- README.md | 9 +++++++++ savefile.py | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/README.md b/README.md index 69bdc46..213ff26 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,15 @@ To zero out all non-location-specific challenges: python savefile.py -m challenges=zero old.sav new.sav +To change the character's name: + + python savefile.py -m "name=Mrs. Henderson" old.sav new.sav + +To change the character's save slot (I believe this is overridden by the game itself, +rather than used from this save file, but I always go ahead and updte it anyway): + + python savefile.py -m save_game_id=12 old.sav new.sav + Or many changes at once, separated by commas: python savefile.py -m level=7,skillpoints=42,money=1234,eridium=12,seraph=120,itemlevels old.sav new.sav diff --git a/savefile.py b/savefile.py index 9766eb2..a023b39 100755 --- a/savefile.py +++ b/savefile.py @@ -2143,6 +2143,17 @@ def modify_save(data, changes, endian=1): # Re-wrap the data player[15][0][1] = wrap_challenges(data) + if changes.has_key("name") and len(changes["name"]) > 0: + data = apply_structure(read_protobuf(player[19][0][1]), save_structure[19][2]) + data["name"] = changes["name"] + player[19][0][1] = write_protobuf(remove_structure(data, invert_structure(save_structure[19][2]))) + + if changes.has_key("save_game_id") and len(changes["save_game_id"]) > 0: + try: + player[20][0][1] = int(changes["save_game_id"]) + except ValueError: + raise BL2Error("'%s' is not a numeric save_game_id" % (changes["save_game_id"])) + return wrap_player_data(write_protobuf(player), endian) def export_items(data, output): From 9417d742073dd08d9691b265d8c980381d01bbeb Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Fri, 10 Apr 2015 16:00:01 -0500 Subject: [PATCH 008/103] Added a little note about zeroing out challenges --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 213ff26..d4f6e80 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,10 @@ would be a much simpler way of doing so), or if someone wanted to set up their character to more easily unlock all the various skins/heads that you get while working your way through the challenges (though I believe the aforementioned profile.dat editor can unlock all those, too, and you could -also get those via some editing in Gibbed if that's your thing). +also get those via some editing in Gibbed if that's your thing). The one +thing that could be easily done via my changes which I don't think can +be easily done elsewhere would be to reset all your challenge statuses to +zero, which might possibly be of use to someone. So: it's a bit silly, but here it is regardless. From c138d723f65617d60b4fcd0bce7b38fafca91b6b Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Wed, 15 Apr 2015 10:35:45 -0500 Subject: [PATCH 009/103] Only set the bonus levels if we haven't reached them yet (or if zero/max have also been specified) --- savefile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/savefile.py b/savefile.py index a023b39..d0dbba3 100755 --- a/savefile.py +++ b/savefile.py @@ -2138,7 +2138,9 @@ def modify_save(data, changes, endian=1): if do_max: save_challenge['total_value'] = save_challenge['previous_value'] + challenges[save_challenge['id']].get_max() if do_bonus and challenges[save_challenge['id']].bonus: - save_challenge['total_value'] = save_challenge['previous_value'] + challenges[save_challenge['id']].get_bonus() + bonus_value = save_challenge['previous_value'] + challenges[save_challenge['id']].get_bonus() + if do_max or do_zero or save_challenge['total_value'] < bonus_value: + save_challenge['total_value'] = bonus_value # Re-wrap the data player[15][0][1] = wrap_challenges(data) From 7aaca8b363b32f67573de1984ae069a62870726a Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Mon, 1 Jun 2015 11:27:31 -0500 Subject: [PATCH 010/103] Option to automatically set the backpack space to max, without having to specify the number --- README.md | 4 ++++ savefile.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4f6e80..cb6ae0f 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ purchased backpack SDUs: python savefile.py -m backpack=27 old.sav new.sav +Or to set the backpack to the maximum size achievable in-game (which is 39): + + python savefile.py -m backpack old.sav new.sav + Set the size of your character's bank, and the corresponding number of purchased bank SDUs: diff --git a/savefile.py b/savefile.py index d0dbba3..adb9989 100755 --- a/savefile.py +++ b/savefile.py @@ -2059,7 +2059,10 @@ def modify_save(data, changes, endian=1): field[1] = write_protobuf(field_data) if changes.has_key("backpack"): - size = int(changes["backpack"]) + if changes["backpack"]: + size = int(changes["backpack"]) + else: + size = 39 sdus = int(math.ceil((size - 12) / 3.0)) size = 12 + (sdus * 3) slots = read_protobuf(player[13][0][1]) From 9529cb6c7787d16c80ffd92b4fa27646f045d813 Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Sun, 6 Dec 2015 22:07:47 -0600 Subject: [PATCH 011/103] Replacing optparse with argparse, and switching default endianness to little --- savefile.py | 194 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 72 deletions(-) diff --git a/savefile.py b/savefile.py index adb9989..0fe3412 100755 --- a/savefile.py +++ b/savefile.py @@ -6,11 +6,30 @@ import hashlib import json import math -import optparse +import argparse import random import struct import sys +class Config(object): + + # Given by the user, booleans + decode = False + json = False + bigendian = False + parse = False + + # Given by the user, strings + export_items = None + import_items = None + modify = None + input_filename = '-' + output_filename = '-' + + # Config options interpreted from the above + input_file = None + output_file = None + endian = 0 class BL2Error(Exception): pass @@ -2213,99 +2232,130 @@ def import_items(data, codelist, endian=1): return wrap_player_data(write_protobuf(player), endian) -def parse_args(): - usage = "usage: %prog [options] [source file] [destination file]" - p = optparse.OptionParser() - p.add_option( - "-d", "--decode", - action="store_true", - help="read from a save game, rather than creating one" - ) - p.add_option( - "-e", "--export-items", metavar="FILENAME", - help="save out codes for all bank and inventory items" - ) - p.add_option( - "-i", "--import-items", metavar="FILENAME", - help="read in codes for items and add them to the bank and inventory" - ) - p.add_option( - "-j", "--json", - action="store_true", - help="read or write save game data in JSON format, rather than raw protobufs" - ) - p.add_option( - "-l", "--little-endian", - action="store_true", - help="change the output format to little endian, to write PC-compatible save files" - ) - p.add_option( - "-m", "--modify", metavar="MODIFICATIONS", - help="comma separated list of modifications to make, eg money=99999999,eridium=99" - ) - p.add_option( - "-p", "--parse", - action="store_true", - help="parse the protocol buffer data further and generate more readable JSON" - ) - return p.parse_args() - -def main(options, args): - if len(args) >= 2 and args[0] != "-" and args[0] == args[1]: - print >>sys.stderr, "Cannot overwrite the save file, please use a different filename for the new save" - return - - if len(args) < 1 or args[0] == "-": - input = sys.stdin +def parse_args(argv): + + # Set up our config object + config = Config() + + parser = argparse.ArgumentParser(description='Modify Borderlands 2 Save Files', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument('-d', '--decode', + help='read from a save game, rather than creating one', + action='store_true', + ) + + parser.add_argument('-e', '--export-items', + dest='export_items', + help='save out codes for all bank and inventory items', + ) + + parser.add_argument('-i', '--import-items', + dest='import_items', + help='read in codes for items and add them to the bank and inventory', + ) + + parser.add_argument('-j', '--json', + action='store_true', + help='read or write save game data in JSON format, rather than raw protobufs', + ) + + parser.add_argument('-b', '--bigendian', + action='store_true', + help='change the output format to big-endian, to write PS/xbox save files', + ) + + parser.add_argument('-m', '--modify', + help='comma-separated list of modifications to make, eg money=999999999,eridium=99', + ) + + parser.add_argument('-p', '--parse', + action='store_true', + help='parse the protocol buffer data further and generate more readable JSON', + ) + + parser.add_argument('input_filename', + default='-', + nargs='?', + ) + + parser.add_argument('output_filename', + default='-', + nargs='?', + ) + + # Actually parse the args + parser.parse_args(argv, config) + + # Fiddle with our endianness + if config.bigendian: + config.endian = 1 else: - input = open(args[0], "rb") + config.endian = 0 + + # Can't read/write to the same file + if config.input_filename == config.output_filename and config.input_filename != '-': + parser.error('input_filename and output_filename cannot be the same file') - if len(args) < 2 or args[1] == "-": - output = sys.stdout + # Turn out input/output args into filehandles (input) + if config.input_filename == '-': + config.input_file = sys.stdin else: - output = open(args[1], "wb") + try: + config.input_file = open(config.input_filename, 'rb') + except IOError, e: + parser.error('Cannot open input file %s: %s' % (config.input_filename, e)) - if options.little_endian: - endian = 0 + # Now output + if config.output_filename == '-': + config.output_file = sys.stdout else: - endian = 1 + try: + config.output_file = open(config.output_filename, 'wb') + except IOError(e): + parser.error('Cannot open output file %s: %s' % (config.output_filename, e)) + + # Return + return config + +def main(config): - if options.modify is not None: + if config.modify is not None: changes = {} - if options.modify: - for m in options.modify.split(","): + if config.modify: + for m in config.modify.split(","): k, v = (m.split("=", 1) + [None])[: 2] changes[k] = v - output.write(modify_save(input.read(), changes, endian)) - elif options.export_items: - output = open(options.export_items, "w") - export_items(input.read(), output) - elif options.import_items: - itemlist = open(options.import_items, "r") - output.write(import_items(input.read(), itemlist.read(), endian)) - elif options.decode: - savegame = input.read() + config.output_file.write(modify_save(config.input_file.read(), changes, config.endian)) + elif config.export_items: + config.output_file = open(config.export_items, "w") + config.export_items(config.input_file.read(), config.output_file) + elif config.import_items: + itemlist = open(config.import_items, "r") + config.output_file.write(import_items(config.input_file.read(), itemlist.read(), config.endian)) + elif config.decode: + savegame = config.input_file.read() player = unwrap_player_data(savegame) - if options.json: + if config.json: data = read_protobuf(player) - if options.parse: + if config.parse: data = apply_structure(data, save_structure) player = json.dumps(data, encoding="latin1", sort_keys=True, indent=4) - output.write(player) + config.output_file.write(player) else: - player = input.read() - if options.json: + player = config.input_file.read() + if config.json: data = json.loads(player, encoding="latin1") if not data.has_key("1"): data = remove_structure(data, invert_structure(save_structure)) player = write_protobuf(data) savegame = wrap_player_data(player, endian) - output.write(savegame) + config.output_file.write(savegame) if __name__ == "__main__": - options, args = parse_args() + config = parse_args(sys.argv[1:]) try: - main(options, args) + main(config) except: print >>sys.stderr, ( "Something went wrong, but please ensure you have the latest " From c401e987f6c8f6c6c020cbcac6e8f515316c20bb Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Sun, 6 Dec 2015 22:49:23 -0600 Subject: [PATCH 012/103] Changing all the former 'modify' options to their own arguments. Not actually tied-in yet, though. --- savefile.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 6 deletions(-) diff --git a/savefile.py b/savefile.py index 0fe3412..c479a92 100755 --- a/savefile.py +++ b/savefile.py @@ -22,9 +22,24 @@ class Config(object): # Given by the user, strings export_items = None import_items = None - modify = None input_filename = '-' output_filename = '-' + + # Former 'modify' options + name = None + save_game_id = None + level = None + skillpoints = None + money = None + eridium = None + seraph = None + torgue = None + itemlevels = None + backpack = None + bank = None + gunslots = None + unlocks = [] + challenges = [] # Config options interpreted from the above input_file = None @@ -2240,6 +2255,8 @@ def parse_args(argv): parser = argparse.ArgumentParser(description='Modify Borderlands 2 Save Files', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + # Optional args + parser.add_argument('-d', '--decode', help='read from a save game, rather than creating one', action='store_true', @@ -2265,15 +2282,98 @@ def parse_args(argv): help='change the output format to big-endian, to write PS/xbox save files', ) - parser.add_argument('-m', '--modify', - help='comma-separated list of modifications to make, eg money=999999999,eridium=99', - ) - parser.add_argument('-p', '--parse', action='store_true', help='parse the protocol buffer data further and generate more readable JSON', ) + # More optional args - used to be the "modify" option + + #parser.add_argument('-m', '--modify', + # help='comma-separated list of modifications to make, eg money=999999999,eridium=99', + # ) + + parser.add_argument('--name', + 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 of the character (probably not actually needed ever)', + ) + + parser.add_argument('--level', + type=int, + help='Set the character to this level', + ) + + parser.add_argument('--skillpoints', + type=int, + help='Skill Points to add to character (probably does not work right now)', + ) + + parser.add_argument('--money', + type=int, + help='Money to set for character', + ) + + parser.add_argument('--eridium', + type=int, + help='Eridium to set for character', + ) + + parser.add_argument('--seraph', + type=int, + help='Seraph tokens to set for character', + ) + + parser.add_argument('--torgue', + type=int, + help='Torgue tokens to set for character', + ) + + parser.add_argument('--itemlevels', + type=int, + nargs='?', + const=0, + help='Set item levels (default to the current player level)', + ) + + parser.add_argument('--backpack', + type=int, + nargs='?', + const=39, + help='Set size of backpack (defaults to 39)', + ) + + parser.add_argument('--bank', + type=int, + help='Set size of bank', + ) + + parser.add_argument('--gunslots', + type=int, + choices=[2,3,4], + help='Set number of gun slots open', + ) + + parser.add_argument('--unlocks', + action='append', + choices=['slaughterdome', 'truevaulthunter', 'challenges'], + default=[], + help='Game features to unlock', + ) + + parser.add_argument('--challenges', + action='append', + choices=['zero', 'max', 'bonus'], + default=[], + help='Levels to set on challenge data', + ) + + # Positional args + parser.add_argument('input_filename', default='-', nargs='?', @@ -2314,7 +2414,7 @@ def parse_args(argv): config.output_file = open(config.output_filename, 'wb') except IOError(e): parser.error('Cannot open output file %s: %s' % (config.output_filename, e)) - + # Return return config From 704b3507f312444725a1d4b92d742d1b34d718f1 Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Sun, 6 Dec 2015 23:43:44 -0600 Subject: [PATCH 013/103] Initial version of using our new argparse arguments for modifications. Totally untested at the moment. --- savefile.py | 145 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 59 deletions(-) diff --git a/savefile.py b/savefile.py index c479a92..9311f16 100755 --- a/savefile.py +++ b/savefile.py @@ -12,6 +12,9 @@ import sys class Config(object): + """ + Class to hold our configuration information. + """ # Given by the user, booleans decode = False @@ -38,13 +41,38 @@ class Config(object): backpack = None bank = None gunslots = None - unlocks = [] - challenges = [] + unlocks = {} + challenges = {} # Config options interpreted from the above input_file = None output_file = None endian = 0 + changes = False + +class DictAction(argparse.Action): + """ + Custom argparse action to put list-like arguments into + a dict (where the value will be True) rather than a list. + This is probably implemented fairly shoddily. + """ + def __init__(self, option_strings, dest, nargs=None, **kwargs): + """ + Constructor, taken right from https://docs.python.org/2.7/library/argparse.html#action + """ + if nargs is not None: + raise ValueError('nargs is not allowed') + super(DictAction, self).__init__(option_strings, dest, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + """ + Actually setting a value. Forces the attr into a dict if it isn't already. + """ + arg_value = getattr(namespace, self.dest) + if not isinstance(arg_value, dict): + arg_value = {} + arg_value[values] = True + setattr(namespace, self.dest, arg_value) class BL2Error(Exception): pass @@ -2048,39 +2076,38 @@ def lzo1x_1_compress(s): return str(dst) -def modify_save(data, changes, endian=1): +def modify_save(data, config): player = read_protobuf(unwrap_player_data(data)) - if changes.has_key("level"): - level = int(changes["level"]) - lower = int(60 * (level ** 2.8) - 59.2) - upper = int(60 * ((level + 1) ** 2.8) - 59.2) + if config.level is not None: + lower = int(60 * (config.level ** 2.8) - 59.2) + upper = int(60 * ((config.level + 1) ** 2.8) - 59.2) if player[3][0][1] not in range(lower, upper): player[3][0][1] = lower - player[2] = [[0, int(changes["level"])]] + player[2] = [[0, config.level]] - if changes.has_key("skillpoints"): - player[4] = [[0, int(changes["skillpoints"])]] + if config.skillpoints is not None: + player[4] = [[0, config.skillpoints]] - if any(map(changes.has_key, ("money", "eridium", "seraph", "tokens"))): + if any([x is not None for x in [config.money, config.eridium, config.seraph, config.torgue]]): raw = player[6][0][1] b = StringIO(raw) values = [] while b.tell() < len(raw): values.append(read_protobuf_value(b, 0)) - if changes.has_key("money"): - values[0] = int(changes["money"]) - if changes.has_key("eridium"): - values[1] = int(changes["eridium"]) - if changes.has_key("seraph"): - values[2] = int(changes["seraph"]) - if changes.has_key("tokens"): - values[4] = int(changes["tokens"]) + if config.money is not None: + values[0] = config.money + if config.eridium is not None: + values[1] = config.eridium + if config.seraph is not None: + values[2] = config.seraph + if config.torgue is not None: + values[4] = config.torgue player[6][0] = [0, values] - if changes.has_key("itemlevels"): - if changes["itemlevels"]: - level = int(changes["itemlevels"]) + if config.itemlevels is not None: + if config.itemlevels > 0: + level = config.itemlevels else: level = player[2][0][1] for field_number in (53, 54): @@ -2092,11 +2119,8 @@ def modify_save(data, changes, endian=1): field_data[1][0][1] = wrap_item(is_weapon, item, key) field[1] = write_protobuf(field_data) - if changes.has_key("backpack"): - if changes["backpack"]: - size = int(changes["backpack"]) - else: - size = 39 + if config.backpack is not None: + size = config.backpack sdus = int(math.ceil((size - 12) / 3.0)) size = 12 + (sdus * 3) slots = read_protobuf(player[13][0][1]) @@ -2105,8 +2129,8 @@ def modify_save(data, changes, endian=1): s = read_repeated_protobuf_value(player[36][0][1], 0) player[36][0][1] = write_repeated_protobuf_value(s[: 7] + [sdus] + s[8: ], 0) - if changes.has_key("bank"): - size = int(changes["bank"]) + if config.bank is not None: + size = config.bank sdus = int(min(255, math.ceil((size - 6) / 2.0))) size = 6 + (sdus * 2) if player.has_key(56): @@ -2118,22 +2142,21 @@ def modify_save(data, changes, endian=1): s = s + (9 - len(s)) * [0] player[36][0][1] = write_repeated_protobuf_value(s[: 8] + [sdus] + s[9: ], 0) - if changes.get("gunslots", "0") in "234": - n = int(changes["gunslots"]) + if config.gunslots is not None: + n = config.gunslots slots = read_protobuf(player[13][0][1]) slots[2][0][1] = n if slots[3][0][1] > n - 2: slots[3][0][1] = n - 2 player[13][0][1] = write_protobuf(slots) - if changes.has_key("unlocks"): + if len(config.unlocks) > 0: unlocked, notifications = [], [] if player.has_key(23): unlocked = map(ord, player[23][0][1]) if player.has_key(24): notifications = map(ord, player[24][0][1]) - unlocks = changes["unlocks"].split(":") - if "slaughterdome" in unlocks: + if 'slaughterdome' in config.unlocks: if 1 not in unlocked: unlocked.append(1) if 1 not in notifications: @@ -2142,10 +2165,10 @@ def modify_save(data, changes, endian=1): player[23] = [[2, "".join(map(chr, unlocked))]] if notifications: player[24] = [[2, "".join(map(chr, notifications))]] - if "truevaulthunter" in unlocks: + if 'truevaulthunter' in config.unlocks: if player[7][0][1] < 1: player[7][0][1] = 1 - if "challenges" in unlocks: + if 'challenges' in config.unlocks: challenge_unlocks = [apply_structure(read_protobuf(d[1]), save_structure[38][2]) for d in player[38]] inverted_structure = invert_structure(save_structure[38][2]) seen_challenges = {} @@ -2158,14 +2181,14 @@ def modify_save(data, changes, endian=1): ('is_from_dlc', challenge.cat.is_from_dlc), ('name', challenge.id_text)]), inverted_structure))]) - if changes.has_key("challenges"): + if len(config.challenges) > 0: data = unwrap_challenges(player[15][0][1]) # You can specify multiple options at once. Specifying "max" and # "bonus" at the same time, for instance, will put everything at its # max value, and then potentially lower the ones which have bonuses. - do_zero = "zero" in changes["challenges"] - do_max = "max" in changes["challenges"] - do_bonus = "bonus" in changes["challenges"] + do_zero = 'zero' in config.challenges + do_max = 'max' in config.challenges + do_bonus = 'bonus' in config.challenges # Loop through for save_challenge in data['challenges']: @@ -2182,18 +2205,15 @@ def modify_save(data, changes, endian=1): # Re-wrap the data player[15][0][1] = wrap_challenges(data) - if changes.has_key("name") and len(changes["name"]) > 0: + if config.name is not None and len(config.name) > 0: data = apply_structure(read_protobuf(player[19][0][1]), save_structure[19][2]) - data["name"] = changes["name"] + data['name'] = config.name player[19][0][1] = write_protobuf(remove_structure(data, invert_structure(save_structure[19][2]))) - if changes.has_key("save_game_id") and len(changes["save_game_id"]) > 0: - try: - player[20][0][1] = int(changes["save_game_id"]) - except ValueError: - raise BL2Error("'%s' is not a numeric save_game_id" % (changes["save_game_id"])) + if config.save_game_id is not None and config.save_game_id > 0: + player[20][0][1] = config.save_game_id - return wrap_player_data(write_protobuf(player), endian) + return wrap_player_data(write_protobuf(player), config.endian) def export_items(data, output): player = read_protobuf(unwrap_player_data(data)) @@ -2359,16 +2379,16 @@ def parse_args(argv): ) parser.add_argument('--unlocks', - action='append', + action=DictAction, choices=['slaughterdome', 'truevaulthunter', 'challenges'], - default=[], + default={}, help='Game features to unlock', ) parser.add_argument('--challenges', - action='append', + action=DictAction, choices=['zero', 'max', 'bonus'], - default=[], + default={}, help='Levels to set on challenge data', ) @@ -2393,6 +2413,18 @@ def parse_args(argv): else: config.endian = 0 + # Set our "changes" boolean + for var in [config.name, config.save_game_id, config.level, + config.skillpoints, config.money, config.eridium, + config.seraph, config.seraph, config.torgue, + config.itemlevels, config.backpack, config.bank, + config.gunslots]: + if var is not None: + config.changes = True + for var in [config.unlocks, config.challenges]: + if var != []: + config.changes = True + # Can't read/write to the same file if config.input_filename == config.output_filename and config.input_filename != '-': parser.error('input_filename and output_filename cannot be the same file') @@ -2420,13 +2452,8 @@ def parse_args(argv): def main(config): - if config.modify is not None: - changes = {} - if config.modify: - for m in config.modify.split(","): - k, v = (m.split("=", 1) + [None])[: 2] - changes[k] = v - config.output_file.write(modify_save(config.input_file.read(), changes, config.endian)) + if config.changes: + config.output_file.write(modify_save(config.input_file.read(), config)) elif config.export_items: config.output_file = open(config.export_items, "w") config.export_items(config.input_file.read(), config.output_file) From f001201a45e04adbc8e592be6b1b2f97121d036c Mon Sep 17 00:00:00 2001 From: CJ Kucera Date: Mon, 7 Dec 2015 01:20:54 -0600 Subject: [PATCH 014/103] Pulling basically everything into a big ol' class file. Very WIP, I'm sure various things got broken. --- README.md | 10 + savefile.py | 4567 ++++++++++++++++++++++++++------------------------- 2 files changed, 2342 insertions(+), 2235 deletions(-) diff --git a/README.md b/README.md index cb6ae0f..5cc52bc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ +# NOTE, December 6 2015 - Work-in-progress, probably busted. + +There've been a bunch of changes checked in starting December 6, 2015, +which have probably resulted in various bits of this getting broken. +I should've really been working in a branch or whatever, but I wasn't. +Grab it as of this commit if you'd like a version much more likely to +work properly, in the meantime: + +https://github.com/apocalyptech/borderlands2/tree/7aaca8b363b32f67573de1984ae069a62870726a + # WARNING ABOUT THIS FORK I've forked this over from pclifford/borderlands2 primarily to add in some diff --git a/savefile.py b/savefile.py index 9311f16..831d9e3 100755 --- a/savefile.py +++ b/savefile.py @@ -11,7 +11,7 @@ import struct import sys -class Config(object): +class Config(argparse.Namespace): """ Class to hold our configuration information. """ @@ -21,6 +21,7 @@ class Config(object): json = False bigendian = False parse = False + verbose = False # Given by the user, strings export_items = None @@ -50,6 +51,69 @@ class Config(object): endian = 0 changes = False + def finish(self, parser): + """ + Some extra sanity checks on our options. "parser" should + be an active ArgumentParser object we can use to raise + errors. + """ + + # Endianness + if self.bigendian: + self.endian = 1 + else: + self.endian = 0 + + # Set our "changes" boolean + for var in [self.name, self.save_game_id, self.level, + self.skillpoints, self.money, self.eridium, + self.seraph, self.seraph, self.torgue, + self.itemlevels, self.backpack, self.bank, + self.gunslots]: + if var is not None: + self.changes = True + for var in [self.unlocks, self.challenges]: + if len(var) > 0: + self.changes = True + + # Can't read/write to the same file + if self.input_filename == self.output_filename and self.input_filename != '-': + parser.error('input_filename and output_filename cannot be the same file') + + def open_filehandles(self, parser): + """ + Opens our filehandles. "parser" should be an active + ArgumentParser object we can use to raise errors. This... + probably shouldn't actually belong in here, but here it + is anyway. + """ + + # Input first + if self.input_filename == '-': + if self.verbose: + App.debug('Using STDIN for input file') + self.input_file = sys.stdin + else: + try: + if self.verbose: + App.debug('Opening %s for input file' % (self.input_filename)) + self.input_file = open(self.input_filename, 'rb') + except IOError, e: + parser.error('Cannot open input file %s: %s' % (self.input_filename, e)) + + # Now output + if self.output_filename == '-': + if self.verbose: + App.debug('Using STDOUT for output file') + self.output_file = sys.stdout + else: + try: + if self.verbose: + App.debug('Opening %s for output file' % (self.output_filename)) + self.output_file = open(self.output_filename, 'wb') + except IOError(e): + parser.error('Cannot open output file %s: %s' % (self.output_filename, e)) + class DictAction(argparse.Action): """ Custom argparse action to put list-like arguments into @@ -163,368 +227,6 @@ def getvalue(self): else: return self.s - -def read_huffman_tree(b): - node_type = b.read_bit() - if node_type == 0: - return (None, (read_huffman_tree(b), read_huffman_tree(b))) - else: - return (None, b.read_byte()) - -def write_huffman_tree(node, b): - if type(node[1]) is int: - b.write_bit(1) - b.write_byte(node[1]) - else: - b.write_bit(0) - write_huffman_tree(node[1][0], b) - write_huffman_tree(node[1][1], b) - -def make_huffman_tree(data): - frequencies = [0] * 256 - for c in data: - frequencies[ord(c)] += 1 - - nodes = [[f, i] for (i, f) in enumerate(frequencies) if f != 0] - nodes.sort() - - while len(nodes) > 1: - l, r = nodes[: 2] - nodes = nodes[2: ] - insort(nodes, [l[0] + r[0], [l, r]]) - - return nodes[0] - -def invert_tree(node, code=0, bits=0): - if type(node[1]) is int: - return {chr(node[1]): (code, bits)} - else: - d = {} - d.update(invert_tree(node[1][0], code << 1, bits + 1)) - d.update(invert_tree(node[1][1], (code << 1) | 1, bits + 1)) - return d - -def huffman_decompress(tree, bitstream, size): - output = "" - while len(output) < size: - node = tree - while 1: - b = bitstream.read_bit() - node = node[1][b] - if type(node[1]) is int: - output += chr(node[1]) - break - return output - -def huffman_compress(encoding, data, bitstream): - for c in data: - code, nbits = encoding[c] - bitstream.write_bits(code, nbits) - - -item_sizes = ( - (8, 17, 20, 11, 7, 7, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16), - (8, 13, 20, 11, 7, 7, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17) -) - -def pack_item_values(is_weapon, values): - i = 0 - bytes = [0] * 32 - for value, size in zip(values, item_sizes[is_weapon]): - if value is None: - break - j = i >> 3 - value = value << (i & 7) - while value != 0: - bytes[j] |= value & 0xff - value = value >> 8 - j = j + 1 - i = i + size - if (i & 7) != 0: - value = 0xff << (i & 7) - bytes[i >> 3] |= (value & 0xff) - return "".join(map(chr, bytes[: (i + 7) >> 3])) - -def unpack_item_values(is_weapon, data): - i = 8 - data = " " + data - values = [] - end = len(data) * 8 - for size in item_sizes[is_weapon]: - j = i + size - if j > end: - values.append(None) - continue - value = 0 - for b in data[j >> 3: (i >> 3) - 1: -1]: - value = (value << 8) | ord(b) - values.append((value >> (i & 7)) &~ (0xff << size)) - i = j - return values - -def rotate_data_right(data, steps): - steps = steps % len(data) - return data[-steps: ] + data[: -steps] - -def rotate_data_left(data, steps): - steps = steps % len(data) - return data[steps: ] + data[: steps] - -def xor_data(data, key): - key = key & 0xffffffff - output = "" - for c in data: - key = (key * 279470273) % 4294967291 - output += chr((ord(c) ^ key) & 0xff) - return output - -def wrap_item(is_weapon, values, key): - item = pack_item_values(is_weapon, values) - header = struct.pack(">Bi", (is_weapon << 7) | 7, key) - padding = "\xff" * (33 - len(item)) - h = binascii.crc32(header + "\xff\xff" + item + padding) & 0xffffffff - checksum = struct.pack(">H", ((h >> 16) ^ h) & 0xffff) - body = xor_data(rotate_data_left(checksum + item, key & 31), key >> 5) - return header + body - -def unwrap_item(data): - version_type, key = struct.unpack(">Bi", data[: 5]) - is_weapon = version_type >> 7 - raw = rotate_data_right(xor_data(data[5: ], key >> 5), key & 31) - return is_weapon, unpack_item_values(is_weapon, raw[2: ]), key - -def replace_raw_item_key(data, key): - old_key = struct.unpack(">i", data[1: 5])[0] - item = rotate_data_right(xor_data(data[5: ], old_key >> 5), old_key & 31)[2: ] - header = data[0] + struct.pack(">i", key) - padding = "\xff" * (33 - len(item)) - h = binascii.crc32(header + "\xff\xff" + item + padding) & 0xffffffff - checksum = struct.pack(">H", ((h >> 16) ^ h) & 0xffff) - body = xor_data(rotate_data_left(checksum + item, key & 31), key >> 5) - return header + body - - -def read_varint(f): - value = 0 - offset = 0 - while 1: - b = ord(f.read(1)) - value |= (b & 0x7f) << offset - if (b & 0x80) == 0: - break - offset = offset + 7 - return value - -def write_varint(f, i): - while i > 0x7f: - f.write(chr(0x80 | (i & 0x7f))) - i = i >> 7 - f.write(chr(i)) - -def read_protobuf(data): - fields = {} - end_position = len(data) - bytestream = StringIO(data) - while bytestream.tell() < end_position: - key = read_varint(bytestream) - field_number = key >> 3 - wire_type = key & 7 - value = read_protobuf_value(bytestream, wire_type) - fields.setdefault(field_number, []).append([wire_type, value]) - return fields - -def read_protobuf_value(b, wire_type): - if wire_type == 0: - value = read_varint(b) - elif wire_type == 1: - value = struct.unpack("> 1) - else: - return i >> 1 - - -def apply_structure(pbdata, s): - fields = {} - raw = {} - for k, data in pbdata.items(): - mapping = s.get(k) - if mapping is None: - raw[k] = data - continue - elif type(mapping) is str: - fields[mapping] = data[0][1] - continue - key, repeated, child_s = mapping - if child_s is None: - values = [d[1] for d in data] - fields[key] = values if repeated else values[0] - elif type(child_s) is int: - if repeated: - fields[key] = read_repeated_protobuf_value(data[0][1], child_s) - else: - fields[key] = data[0][1] - elif type(child_s) is tuple: - values = [child_s[0](d[1]) for d in data] - fields[key] = values if repeated else values[0] - elif type(child_s) is dict: - values = [apply_structure(read_protobuf(d[1]), child_s) for d in data] - fields[key] = values if repeated else values[0] - else: - raise Exception("Invalid mapping %r for %r: %r" % (mapping, k, data)) - if len(raw) != 0: - fields["_raw"] = {} - for k, values in raw.items(): - safe_values = [] - for (wire_type, v) in values: - if wire_type == 2: - v = [ord(c) for c in v] - safe_values.append([wire_type, v]) - fields["_raw"][k] = safe_values - return fields - -def remove_structure(data, inv): - pbdata = {} - pbdata.update(data.get("_raw", {})) - for k, value in data.items(): - if k == "_raw": - continue - mapping = inv.get(k) - if mapping is None: - raise BL2Error("Unknown key %r in data" % (k, )) - elif type(mapping) is int: - pbdata[mapping] = [[guess_wire_type(value), value]] - continue - key, repeated, child_inv = mapping - if child_inv is None: - value = [value] if not repeated else value - pbdata[key] = [[guess_wire_type(v), v] for v in value] - elif type(child_inv) is int: - if repeated: - b = StringIO() - for v in value: - write_protobuf_value(b, child_inv, v) - pbdata[key] = [[2, b.getvalue()]] - else: - pbdata[key] = [[child_inv, value]] - elif type(child_inv) is tuple: - value = [value] if not repeated else value - values = [] - for v in map(child_inv[1], value): - if type(v) is list: - values.append(v) - else: - values.append([guess_wire_type(v), v]) - pbdata[key] = values - elif type(child_inv) is dict: - value = [value] if not repeated else value - values = [] - for d in [remove_structure(v, child_inv) for v in value]: - values.append([2, write_protobuf(d)]) - pbdata[key] = values - else: - raise Exception("Invalid mapping %r for %r: %r" % (mapping, k, value)) - return pbdata - -def guess_wire_type(value): - return 2 if isinstance(value, basestring) else 0 - -def invert_structure(structure): - inv = {} - for k, v in structure.items(): - if type(v) is tuple: - if type(v[2]) is dict: - inv[v[0]] = (k, v[1], invert_structure(v[2])) - else: - inv[v[0]] = (k, ) + v[1: ] - else: - inv[v] = k - return inv - -def unwrap_bytes(value): - return [ord(d) for d in value] - -def wrap_bytes(value): - return "".join(map(chr, value)) - -def unwrap_float(v): - return struct.unpack("