-
Notifications
You must be signed in to change notification settings - Fork 7
/
BL2_find_items.py
1402 lines (1341 loc) · 64.5 KB
/
BL2_find_items.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Parse Borderlands 2 savefiles and list all items across all characters
# See https://github.com/gibbed/Gibbed.Borderlands2 for a Windows-only
# program to do way more than this, including actually changing stuff.
# This is much simpler; its purpose is to help you twink items between
# your characters, or more specifically, to find the items that you want
# to twink. It should be able to handle Windows and Linux save files, but
# not save files from consoles (they may be big-endian, and/or use another
# compression algorithm). Currently the path is hard-coded for Linux though.
import argparse
import base64
import binascii
import collections
import hashlib
import itertools
import json
import math
import os.path
import struct
import sys
import random
from fnmatch import fnmatch
from dataclasses import dataclass # ImportError? Upgrade to Python 3.7 or pip install dataclasses
from pprint import pprint
import lzo # ImportError? pip install python-lzo
from BL1_find_items import FunctionArg, Consumable
from BL3_find_items import bogocrypt, ConsumableLE
# python-lzo 1.12 on Python 3.8 causes a DeprecationWarning regarding arg parsing with integers.
import warnings; warnings.filterwarnings("ignore")
loot_filter = FunctionArg("filter", 2)
@loot_filter
def level(usage, item, minlvl, maxlvl=None):
minlvl = int(minlvl)
if maxlvl is None: maxlvl = minlvl + 5
return minlvl <= item.grade <= int(maxlvl)
@loot_filter
def type(usage, item, type): return type in item.type
del type # I want the filter to be called type, but not to override type()
@loot_filter
def title(usage, item, tit): return item.title is not None and tit in item.title
@loot_filter
def loose(usage, item): return not usage.is_equipped() and usage.is_carried()
synthesizer = FunctionArg("synth", 1)
def strip_prefix(str): return str.split(".", 1)[1]
def armor_serial(serial): return base64.b64encode(serial).decode("ascii").strip("=")
def unarmor_serial(id): return base64.b64decode(id.strip("{}").encode("ascii") + b"====")
def partnames(is_weapon):
if is_weapon: return "body grip barrel sight stock elemental accessory1 accessory2".split()
return "alpha beta gamma delta epsilon zeta eta theta".split()
@synthesizer
def money(savefile): savefile.money[0] += 5000000 # Add more dollars
@synthesizer
def eridium(savefile): savefile.money[1] += 500 # Add more eridium/moonstones
@synthesizer
def seraph(savefile): savefile.money[2] += 500 # Not sure what, if anything, these two would do in TPS
@synthesizer
def torgue(savefile): savefile.money[4] += 500
@synthesizer
def xp(savefile, xp=None):
# Change the experience point count, without going beyond the level.
min = math.ceil(60 * savefile.level ** 2.8 - 60)
max = math.ceil(60 * (savefile.level + 1) ** 2.8 - 60) - 1
if xp is None: savefile.exp = max
elif min <= int(xp) <= max: savefile.exp = int(xp)
else: print("WARNING: Leaving XP unchanged - level %d needs %d-%d XP" % (savefile.level, min, max))
@synthesizer
def boost(savefile):
"""Boost the levels of all equipped gear lower than your current level"""
for i, weapon in enumerate(savefile.packed_weapon_data):
weap = Asset.decode_asset_library(weapon.serial)
if weap.grade < savefile.level and weapon.quickslot:
weap.grade = weap.stage = savefile.level
savefile.packed_weapon_data[i].serial = weap.encode_asset_library()
for i, item in enumerate(savefile.packed_item_data):
it = Asset.decode_asset_library(item.serial)
if it and it.grade < savefile.level and item.equipped:
it.grade = it.stage = savefile.level
savefile.packed_item_data[i].serial = it.encode_asset_library()
@synthesizer
def invdup(savefile, level):
"""Duplicate inventory at a new level for comparison"""
levels = [int(l) for l in level.split(",") if l]
if not levels: raise ValueError("C'mon, get on my level, man")
for weapon in savefile.packed_weapon_data:
weap = Asset.decode_asset_library(weapon.serial)
if weap.grade not in levels and not weapon.quickslot:
for level in levels:
weap.grade = weap.stage = level
weap.seed = random.randrange(1<<31)
savefile.add_inventory(weap)
for item in savefile.packed_item_data:
it = Asset.decode_asset_library(item.serial)
if it and it.grade not in levels and not item.equipped:
for level in levels:
it.grade = it.stage = level
it.seed = random.randrange(1<<31)
savefile.add_inventory(it)
def get_part_list(cls, lst):
if isinstance(lst, str):
# Instead of having all the parts here, they're a reference to another file
bal = get_asset(cls + " Balance Part Lists").get(lst)
if bal: return bal
type = get_asset(cls + " Part Lists").get(lst)
if type: return type
return [] # Unknown
return lst
@synthesizer
def item(savefile, bal):
"""Synthesize some possible weapon/item based on its Balance definition"""
# Random note: Glitch attachments that begin with 0 are identified correctly
# eg GD_Ma_Weapons.Glitch_Attachments.Glitch_Attachment_0421 gives O0L4M2A1.
# Other attachments have the internal name give an Amplify value higher by one
# eg GD_Ma_Weapons.Glitch_Attachments.Glitch_Attachment_2144 is O2L1M4A3. Odd.
bal, _, type = bal.partition("/")
try:
balance = get_balance_info(0, bal)
is_weapon = 0
except KeyError:
balance = get_balance_info(1, bal)
is_weapon = 1
if type:
# Custom type selected. Ensure that it's valid.
if type not in balance.get("types", [balance.get("type"), balance.get("item")]):
print("\nType invalid, will probably break")
elif "type" in balance: type = balance["type"]
elif "item" in balance: type = balance["item"]
elif "weapon_type" in balance: type = balance["weapon_type"]
elif "item_type" in balance: type = balance["item_type"]
else: type = balance["types"][0]
typeinfo = get_asset("Weapon Types" if is_weapon else "Item Types")[type]
def p(part):
b = balance.get(part) or balance["parts"].get(part)
if b: return b[0]
# if b is not None: return None # If balance returns [], return None, don't look at the type. Maybe. Not sure.
t = get_part_list("Weapon" if is_weapon else "Item", typeinfo.get(part + "_parts", typeinfo.get(part + "s")))
if t: return t[0]
return None
def sp(name): return name and strip_prefix(name)
lvl = savefile if isinstance(savefile, int) else savefile.level
obj = Asset(seed=random.randrange(1<<31), is_weapon=is_weapon, type=sp(type), balance=sp(bal),
brand=sp(balance["manufacturers"][0]), grade=lvl, stage=lvl,
pieces=[sp(p(n)) for n in partnames(is_weapon)], material=sp(p("material")),
pfx=sp(typeinfo.get("prefixes", [None])[0]), title=sp(typeinfo.get("titles", [None])[0]))
if isinstance(savefile, int): return obj
savefile.add_inventory(obj)
print("\nGiving", obj)
@synthesizer
def give(savefile, definitions):
"""Transfer an item from another source, such as the library"""
print()
for definition in definitions.split(","):
[id, *changes] = definition.split("-")
serial = unarmor_serial(id)
obj = Asset.decode_asset_library(serial)
if obj.seed == obj.grade == 50: changes.insert(0, "l") # Can be overridden by another "-l", but normally, assume you want library items at your level.
obj.seed = random.randrange(1<<31) # Rerandomize the seed
for change in changes:
if not change: continue
c = change[0].lower()
if c == "l": obj.grade = obj.stage = int(change[1:] or savefile.level)
# TODO: Add other changes as needed
savefile.add_inventory(obj)
print("Giving", obj)
def get_piece_options(obj):
# TODO: Make use of get_balance_info rather than duplicating the work
cls = "Weapon" if obj.is_weapon else "Item"
config = get_asset_library_manager()
setid, sublib, asset, cat = config["_find_asset"]["BalanceDefs"][obj.balance]
allbal = get_asset(cls + " Balance")
# Build up a full list of available parts
# Assumes that the "mode" is always "Selective" as I don't know how "Additive" works exactly
checkme = cat + "." + obj.balance
pieces = [None] * len(obj.partnames)
while checkme:
print(checkme)
if "type" in allbal[checkme] and "parts" not in allbal[checkme]:
# FIXME: When working with turtle shields, need to look up the type, but they
# also have some parts in the balance. Maybe always look up both and merge??
# Some items don't have their parts in their balance definition, but they have
# a type definition that has them instead.
typeinfo = get_asset(cls + " Types")[allbal[checkme]["item"]]
pieces = [p or get_part_list(cls, typeinfo.get(part + "_parts")) for p, part in zip(pieces, obj.partnames)]
# Is it possible to have a base but no parts?
break
pieces = [p for p, part in zip(pieces, obj.partnames)]
checkme = allbal[checkme].get("base")
return [p1 or [p2] for p1, p2 in zip(pieces, obj.pieces)] # Any still unfound, just leave the current piece (or None) in them
@synthesizer
def crossproduct(savefile, baseid):
baseid, *lockdown = baseid.split(",")
if "_" in baseid: obj = item(50, baseid) # Looks like a balance name, not an item ID
else: obj = Asset.decode_asset_library(unarmor_serial(baseid))
print()
print("Basis:", obj)
pieces = get_piece_options(obj)
interactive = False
while "interactive or at least once":
for fixed in lockdown:
if fixed == "input":
interactive = True
continue
if fixed.startswith("-") and fixed[1:] in obj.partnames:
# Specify "-delta" to have nothing in slot delta
pieces[obj.partnames.index(fixed[1:])] = [None]
continue
if "-" in fixed:
# Hack: Directly change the basis object
attr, val = fixed.split("-")
if hasattr(obj, attr):
setattr(obj, attr, val)
continue
for n, p in enumerate(pieces):
if fixed in p:
pieces[n] = [fixed]
break
else:
print("Couldn't find %r to lock down" % fixed)
# Show the available options and which one is in the basis object
total = 1
for i, (n, opts) in enumerate(zip(obj.partnames, pieces)):
for p in opts:
if p and obj.pieces[i] and p.endswith(obj.pieces[i]): p = "\x1b[1m%s\x1b[0m" % p
elif not obj.pieces[i] and p is None: p = "\x1b[1mNone\x1b[0m"
print(n, p)
n = " " * len(n)
if not obj.pieces[i] and None not in opts:
print(n, "\x1b[1mNone\x1b[0m")
total *= len(opts)
print("Will create", total, "objects.")
lockdown = []
fixme = interactive and input()
if fixme == "give" or fixme == "gr" or not fixme:
for pp in itertools.product(*pieces):
obj.seed = random.randrange(1<<31)
obj.grade = obj.stage = savefile.level
obj.pieces = [piece and strip_prefix(piece) for piece in pp]
if total < 10: print(">", obj)
savefile.add_inventory(obj)
if not fixme: break
if fixme == "gr": pieces = get_piece_options(obj) # Give and reset
elif fixme == "q": break # Hitting Enter gives those items and breaks; hitting "q" breaks without.
elif fixme == "reset": pieces = get_piece_options(obj)
else: lockdown = [fixme]
@synthesizer
def tweak(savefile, baseid):
if "_" in baseid: obj = item(50, baseid) # Looks like a balance name, not an item ID
else: obj = Asset.decode_asset_library(unarmor_serial(baseid))
obj.grade = obj.stage = savefile.level
info = get_balance_info(obj.is_weapon, obj.balance)
weap_item = "Weapon" if obj.is_weapon else "Item"
config = get_asset_library_manager()
setid, sublib, asset, cat = config["_find_asset"][weap_item + "Types"][obj.type]
typeinfo = get_asset(weap_item + " Types")[cat + "." + obj.type]
get_balance_options = { }
def list_parts(part):
b = info.get(part)
if b: return b
p = info.get("parts", {}).get(part)
if p: return p
t = typeinfo.get(part + "_parts")
if t: return t
return [None]
def opt(f): get_balance_options[f.__name__] = f
@opt
def type(info):
if "types" in info: return info["types"]
return list(filter(None, (info.get(x) for x in "type item weapon_type item_type".split())))
@opt
def brand(info): return info["manufacturers"]
@opt
def material(info): return list_parts("material")
@opt
def pfx(info): return typeinfo.get("prefixes", [None])
@opt
def title(info): return typeinfo.get("titles", [None])
import curses
@curses.wrapper
def _tweak(stdscr):
curses.set_escdelay(10)
filter = ""
scroll = sel = 0
while "interactive":
line = need = maxsel = selectme = 0
def printf(str="", *args, attr=curses.A_NORMAL, keep=3):
if args: str = str % tuple(args)
nonlocal line
if line - scroll > stdscr.getmaxyx()[0] - keep: # Need scrolling
nonlocal need
need += 1
return
if line >= scroll:
stdscr.addstr(line - scroll, 0, str, attr)
stdscr.clrtoeol()
line += 1
printf("Balance: %s", obj.balance, attr=curses.A_BOLD)
def show_piece(key, active, options):
printf("%s: %s", key, active, attr=curses.A_BOLD)
if len(options) == 1 and str(options[0]).endswith(str(active)):
# The only option is the selected one. Don't bother
# showing additional options. Note that this is checked
# before the filter is, so filtering down to just the
# selected one will still maintain consistency.
return
for opt in options:
opt = strip_prefix(opt) if opt else "None"
if filter in opt.lower():
nonlocal maxsel
maxsel += 1
printf("%s\t%s", "->" if maxsel == sel else "", opt)
if maxsel == sel:
nonlocal selectme
selectme = (key, opt)
for attr, func in get_balance_options.items():
show_piece(attr, getattr(obj, attr), func(info))
printf()
for n, piece in zip(obj.partnames, obj.pieces):
show_piece(n, piece, list_parts(n))
printf(keep=2)
for l in range(line, stdscr.getmaxyx()[0]):
stdscr.move(l, 0)
stdscr.clrtoeol()
if need: printf("(+%d)> %s", need, filter, attr=curses.A_BOLD, keep=1)
else: printf("> %s", filter, attr=curses.A_BOLD, keep=1)
stdscr.refresh()
key = stdscr.getkey()
# Filter, select, enter to change item. Example: Typing "maliwan" will let
# you go "enter, down, enter, down, enter" to make an all-Maliwan item.
if key == "\x1b":
if filter: filter = ""
else: break
# Scroll with shift-up and shift-down (or other keys if they've been redefined)
# or with ctrl-up and ctrl-down, assuming they get reported this way
elif key in ("KEY_SF", "kDN5") and need: scroll += 1
elif key in ("KEY_SR", "kUP5") and scroll: scroll -= 1
elif key == "KEY_DOWN":
if sel < maxsel: sel += 1
else: sel = 1
elif key == "KEY_UP":
if sel > 1: sel -= 1
else: sel = maxsel
elif len(key) == 1 and ('A' <= key <= 'Z' or 'a' <= key <= 'z' or '0' <= key <= '9'):
filter += key.lower()
elif key == "KEY_BACKSPACE" and filter:
filter = filter[:-1]
elif key == "\n" and selectme:
# Ugh, don't like this.
if selectme[0] in obj.partnames:
obj.pieces[obj.partnames.index(selectme[0])] = selectme[1] if selectme[1] != "None" else None
else:
setattr(obj, selectme[0], selectme[1] if selectme[1] != "None" else None)
elif key == "KEY_ENTER" or key == "+": # Keypad enter/plus to take the item
obj.seed = random.randrange(1<<31)
savefile.add_inventory(obj)
elif key == "KEY_IC": filter = repr(stdscr.getkey()) # Debug - hit Insert then a key to see its name
parser = argparse.ArgumentParser(description="Borderlands 2/Pre-Sequel save file reader")
parser.add_argument("-2", "--bl2", help="Read Borderlands 2 savefiles",
action="store_const", const="borderlands 2", dest="game")
parser.add_argument("-p", "--tps", help="Read Borderlands The Pre-Sequel savefiles",
action="store_const", const="borderlands the pre-sequel", dest="game")
parser.set_defaults(game="borderlands 2")
parser.add_argument("--proton", help="Read savefiles from Proton installation",
action="store_const", const="proton", dest="platform")
parser.add_argument("--native", help="Read savefiles from native Linux installation",
action="store_const", const="native", dest="platform")
parser.set_defaults(platform="native")
parser.add_argument("--player", help="Choose which player (by Steam ID) to view savefiles of")
parser.add_argument("--verify", help="Verify code internals by attempting to back-encode", action="store_true")
parser.add_argument("--pieces", help="Show the individual pieces inside weapons/items", action="store_true")
parser.add_argument("--raw", help="Show the raw details of weapons/items (spammy - use loot filters)", action="store_true")
parser.add_argument("--itemids", help="Show the IDs of weapons/items", action="store_true")
parser.add_argument("--synth", help="Synthesize a modified save file", type=synthesizer, nargs="*")
parser.add_argument("-l", "--loot-filter", help="Show loot, optionally filtered to only what's interesting", type=loot_filter, nargs="*")
parser.add_argument("-f", "--file", help="Process only one save file")
parser.add_argument("--dir", help="Specify the savefile directory explicitly (ignores --proton/--native and --player)")
parser.add_argument("--library", help="Add an item ID to the library")
parser.add_argument("--compare", nargs=2, help="Compare two library items (or potential library items)")
parser.add_argument("--cd", help="Print out the directory to save files, based on other options", action="store_true")
args = parser.parse_args()
GAME = args.game
# Library of item IDs. Everything here is seed=grade=stage=50 for consistency.
# Picking and choosing is best done with "--synth give:ITEMID,ITEMID,ITEMID".
# TODO: Check variants of each of these and pick out the best. The "# V" tag means I haven't checked variants.
# TODO: Describe some unobvious tweak options and how to pick useful variants
library = {
"borderlands 2": {
# Weapons
"hwAAADLKv4T3Nj+nwWj5D93eEsI037K1X4yK8cYDK8sWhOzS7cRJ": "Lucid Florentine",
"hwAAADIKvoT3NjynwWgFbZDeYkkhn4u8XMwIu9UDK6MWhOxyZrFJ": "Bulets Go Fasterifed Slagga", # Florentine and Slagga are both slag-em-up SMGs
"hwAAADKKvoT5NjunwWhxDgXdssM0KLK9XOwK+tQDK6MWrOwC7KRJ": "Bladed Tattler",
"hwAAADIHS+32AtYAwGjhy0mUAdMnD5q8mOMOut0DK4+33ajR/fdK": "Rapid Infinity",
"hwAAADLClhT5FjHnwWg5bene4E4lbRm8nSIJxdMDKw8WpengZrVJ": "Onslaught Veruc", # V
"hwAAADLKvwT4Nj2nwWiJr3XdckI2/9u4XoyK89oDK8MWjOyybQZI": "Rightsizing Yellow Jacket",
"hwAAADIHS231AtYAwGgVywGYwdYnYey8nQMMutMDK4uw7aiB+fdK": "Corrosive Teapot", # Bandit grip and Jakobs sight - other options available
"hwAAADLKvwT5NtYAwGgtbVDfok4hoqu8XywJu8cDK8sWhOzSZrFJ": "Fervid Hellfire",
"hwAAADIHS20A5TXPwWjVy1Ge8dMnX1a8mQMPut8DK7+3zagx/fdK": "Crammed Unkempt Harold",
"hwAAADIKvgT4Nj2nwWj5L33dMkI07Nm1X2wK9sYDK8sWhOzybYRJ": "Consummate Crit",
# Grenades
"BwAAADLCuhHxmSU8wOjSDKEfogGraRu+EzziescQoXr3uq5NbvU": "Longbow Bonus Package",
"BwAAADLCudH52S+8IhTTDKHfpQHpUxu2E0jiWscQ4X33uq5N7vY": "Longbow Breath of Terramorphous",
"BwAAADLCuhH52daAIhTTDKEfpQHy0xu5E0biWscQ4Xr3uq5N7v8": "Longbow Pandemic",
"BwAAADJCvDLxmSU8wOjSDKFfgyJbuBi6EQnKascQIW/Uuq5QbvU": "Longbow Meteor Shower",
"BwAAADLCshH52daAN1TfzH4jgiDqZBK8CQ+q6sYQYZkLt49NLv4": "Fire Storm",
"BwAAADLCstH52daAN9TfzH6jgCBJjxK8CQeq6sYQYZkLtI9Nrv8": "Lightning Bolt",
"BwAAADLCshH52daAN9TfzH4jgCCRihK8CQWq6sYQYZkLtY9NLv4": "Fireball",
"BwAAADLCstH5WS68N9TfzH7jgCDRYRK8CQGq6sYQYZlLtI9Nbv8": "Magic Missile",
"BwAAADLCstH5WS68NxTfzH7jgCB58xK8CQOq6sYQYZnLt49Nbv8": "Magic Missile (rare)", # Synthesized, unconfirmed.
"BwAAADLCstH52daAN1TfzH7jgSBNXhK8CQ2q6sYQYZnLtI9Nrv8": "Chain Lightning",
"BwAAADLCuhH52daAIhTTDKFfpQEo1Ru5EzriWscQ4Xr3uq5NLv4": "Longbow Fire Bee",
"BwAAADLCshH5WS68N9TfTIsfpgHW0Bu9E1Ti+sYQYXo3pa5Nrv8": "Shock Kiss of Death",
"BwAAADLCuhHxWS48wOjSzH5jrwGGwRu1E0LiescQYZkLuq5QbvU": "Rolling Thunder",
"BwAAADLCutH52daAItTZDJ7fpAHhRBu/Ez7iuscQIX23uq5NLv4": "Sticky Longbow Bouncing Bonny",
"BwAAADICvTLxWSW8IhTfDJ5fhCJ0iRi7EQvK6sYQoX23uu5QbvU": "O-Negative",
"BwAAADJCs/L52daAItTZDKGfjSJ5qBi5EQ/KqscQ4W7Uuq5Q7v8": "Corrosive Crossfire",
# Shields
"BwAAADIiS20A5dYAwOjK7H6jgCEaBxK8Cgm6CsYQ4WQwsClY6fQ": "Blockade",
"BwAAADIFS20A5dYAwGicy37j2gbb3xuqDW6qOccQYWcwsClY6aM": "The Bee",
"BwAAADIFS20A5dYAwOiey36j2QbGjRuuDVaqWccQ4WcwsakEKaA": "The Transformer",
"BwAAADIFS20A5dYAwCjOb4E8gCJh9Ri8EQXKSscQ4WcwsSlXaew": "Sponge",
"BwAAADIFS20A5dYAwOjFy36jlwaOWhu9DQCq+cYQIWYwsKlW6fs": "Flame of the Firehawk",
"BwAAADIFS20A5dYAwOjWS7qYugbmPhu7DWqq+cUQYWcwsSlYaeg": "Chitinous Turtle Shield", # Massive capacity, slow recharge.
"BwAAADIFS20A5dYAwOjWy76YugZn0hu7DWqq+cUQ4WcwtqlWaeg": "Selected Turtle Shield", # A bit less capacity but better recharge
"BwAAADIFS20A5dYAwKjWS76YugbEWRu7DTSq+cUQ4WcwsalWaeg": "Fit Turtle Shield", # Blue rarity
"BwAAADIFS20A5dYAwGjoi8MYlgbWihu0DTaqGccQYWcwsalWqdc": "Supersized Shield", # Blue rarity, nice and vanilla
"BwAAADIFS20A5dYAwKjdy80Y0wa1HhuxDTqq+cYQ4WcwsalW6bU": "Devastating Fire Nova Shield", # Blue rarity
"BwAAADIFS20A5dYAwKjQS7WYrwYKFBu5DSqqGcYQ4WcwsalWae4": "Hippocratic Adaptive Shield", # Blue rarity
"BwAAADIFS20A5dYAwKjRy7gYtQYY9Ru6DTCqOccQoWQwselXae8": "Action Itemized Amplify Shield", # Blue rarity
"BwAAADIFS20A5dYAwGidy36j2ga/nRukDXaq+cYQ4WcwsalW6aw": "Black Hole", # Good capacity, moderate nova
"BwAAADIFS20A5dYAwGidy4yY2gbR0RukDXaq+cYQIWfwsWlX6aw": "Grounded Black Hole", # Great nova, lower capacity
"BwAAADIiS20A5dYAwGjK7H4jgSGEjRK9Cgu66sYQ4WcwsalWqfU": "Antagonist",
# Snipers
"hwAAADINsYrkKkqfwWh1jdAYI8ki6Ti8n8yJu8cDK+/25C2z5zJN": "Banbury Volcano", # Hot (obviously)
"hwAAADJNsQrkKkufwWiBjagY08siXTC8n8yLu8cDKzv21C1D5FJN": "Monstrous Chère-amie", # Elec
"hwAAADKNsQoA5dYAwGidjDhd4s8iQdS8mCyKu9EDK2P17C3D7zJN": "Kull Trespasser", # Hard
"hwAAADINsArjKkufwWgdL60Yg0A0ere9n6yL+cYDKzv21C2jbARN": "Monstrous Pimpernel", # Slag
"hwAAADLNsYrjKkufwWhFjagY08kiKwG8n0yJu8cDK+/21C3z5DJN": "Monstrous Railer", # Acid
"hwAAADINsArkKkufwWhljZgYA8kiXSS8nAyOu9sDKwv21C3z5xJN": "Resource Invader", # Burst-fire elec, good for bosses maybe
# Kabooms
"hwAAADJEr5j3Dj/XwWihrbFeUUcm76O8XCOIxdkDKwPWvW4Ba9ZI": "Bonus Launcher",
"hwAAADJErxj2Dj/XwWjRr6FeoUkmDs28XyOOxdUDKzvWvW6xZrZI": "Roket Pawket PRAZMA CANON",
"hwAAADJEr5j3DjXXwWi5rcldgUomp+u8XYOIxd8DKwvWlW7Ba7ZI": "derp Duuurp!",
"hwAAADIErpj3DjzXwWjRrylcUkUmgjS8XwOKxdUDK2fUlW7Ba9ZI": "hurty Zooka!",
"hwAAADKEr5j3DjzXwWgRrSlcsk4mr128X4OJxdUDK2fUlW7BZnZI": "hurty Roaster",
"hwAAADIErpj3DjLXwWhBreldMUgm5Qq8XOOIxdkDKwPWlW5BZ5ZI": "Bustling Bunny",
"hwAAADJEr5j3DjPXwWjRrwFdUUcmYiK8XwOKxdUDK2fUtW4Ba9ZI": "Speeedee Launcher",
"hwAAADLEr5j3DjHXwWi5rWld0UUmSgG8XYOIxd8DKwvWpW4xa5ZI": "dippity boom",
"hwAAADJErpj3DjPXwWixrQFdEUgmCTC8X6OIxdUDKzvWtW6BZNZI": "Speeedee Badaboom", # Synthesized, unconfirmed
# Relics
"BwAAADIBS20A5S1fPXd/xYkbgM3MtQqOBQSK/JcqOGg": "Heart of the Ancients", # SMG variant
"BwAAADI+S20AZS+/OldkWoEUi/wcxQqOBQTKBSjdR5k": "Proficiency Relic",
"BwAAADI+S20A5SO/OlcyAoEci+wcxQqOBQTKBSjdR5k": "Vitality Relic",
"BwAAADI+S20AJSK/O1f9WYEbi/4cpQqOxfu1/BfdR5k": "Tenacity Relic",
"BwAAADI+S20AZSO/Old8zIEdi/IcxQqOhQ3KBSjdR5k": "Stockpile Relic", # +4 Grenades, regardless of level
"BwAAADI+S20AZSg/Sc3KCYoQWOLV1wK83Pv1BSjd": "Elemental Relic", # Synthesized, unconfirmed (why doesn't it end R5k?)
"BwAAADI+S20AZS1fPXdS+IkdgMHMtQqOBQUK/BfdR5k": "Skin of the Ancients", # Vary the Alpha component to change the element
"BwAAADIBS20A5SK/O1cVT4ECi6gcxQqOBQSK/Bfdx24": "The Afterburner",
# Unconfirmed or uncategorized
"hwAAADLClhT3Fj/nwWgBbWHeQE4l+Eu8nIIOxdUDK6MWpekQYLVJ": "Wyld Asss BlASSter",
"hwAAADJKvgT5Nj+nwWh9bdjewksh0OW8X4wIu8cDK8sWjOxybfFJ": "Lucid SubMalevolent Grace",
"hwAAADLKv4T4Nj6nwWh9bQDeAkQh1k28X4wIu8cDK8sWhOwSbbFJ": "Apt Venom",
"hwAAADIKvoT3NjinwWjpbLDYEk8h0pa8X4wJu8cDKx8WrOwSYPFJ": "Feculent Chulainn",
"BwAAADIFS20A5dYAwOjQy7OYrwaMDxu5DWaqGcYQoWfwtulXae4": "Patent Adaptive Shield",
"hwAAADIKvgT4NjinwWj5L13YMkI0xaK1X2wK9sYDKx8WhOzybeRJ": "Miss Moxxi's Crit",
"hwAAADIClRT4FjvnwWjNLx3foEI031G/mAKK99wDK/cWnelQbWRJ": "Corrosive Kitten",
"BwAAADIFS20A5dYAwOjdi5GY0wbHwxuzDYqq+cYQoWcwsKlWabU": "Majestic Corrosive Spike Shield", # V
"hwAAADLLqAb1MievwWh9TTiZ4skhwIu8HeyIutUDK+82jK6S5/FJ": "Sledge's Shotgun",
"hwAAADLLqAbzMiavwWgFTVidIskhXYm8HwyJutkDK8c2/K5y5tFJ": "Original Deliverance", # V
"hwAAADKLr4bzMjuvwWj1L5WfIkI0CkG/HqyK99oDK/c27K6ybSRI": "Practicable Slow Hand", # Better damage and fire rate
"hwAAADKLr4bzMiSvwWj1L52eIkI0fK6/HqyK99oDK/c27K6ybSRI": "Scalable Slow Hand", # Better accuracy and mag
"hwAAADJClhQA5T7nwWjJbDHeAE4lcVO8n2IJxd8DK7MWlemgZtVJ": "Slippery KerBlaster",
"hwAAADJEr5j3DjPXwWhtLx1dAUA0ayS9XQOI+d4DK0vUlW4hb4RI": "fidle dee 12 Pounder",
"hwAAADIHS231AjTPwWg1zHGesdMnRfi8ncMOutMDK/+3xaix4jdJ": "Neutralizing Hornet",
# Mechromancer:
"hwAAADLLqAYA5dYAwGh9L3VcaEA0OkK9HIyI+dADK8s05K6Sb8RJ": "Captain Blade's Orphan Maker",
"BwAAADI+S20A5dYAwCjOToo8gAPythm2HQECKscQoWO1tGxQLAs": "Legendary Mechromancer",
"BwAAADI+S20A5dYAwOjJzogdgAPgyRm9HQcC6sYQoWO1tGxQLAs": "Slayer of Terramorphous",
"BwAAADI+S20ApS1fPXfwpIkegMfM1QqOBQSK/9coeJk": "Bone of the Ancients",
"BwAAADI+S20A5dYAwOjOjoI8gAM37hGpHAkK6sYQoWO1tGxQLAs": "Legendary Catalyst",
# Siren:
"BwAAADI+S20A5dYAwOjJzogdgAM/+xudEXKr+cYQoWO1tGxQLAs": "Slayer Of Terramorphous Class Mod",
"BwAAADI+S20A5dYAwKjODoE8gAN1wBG8HAcK6sYQoWO1tGxQLAs": "Legendary Binder",
"BwAAADI+S20A5dYAwCjJjpkdgAMXxxubEXyr+cYQoWO1tGxQLAs": "Legendary Siren",
"BwAAADI+S20A5dYAwKjOzpsdgAPwuBuUEX6r+cYQIWO1tGxQLAs": "Chrono Binder", # Left tree focus
"BwAAADI+S20A5dYAwKjODpodgAMyehuUEX6r+cYQoWG1tGxQLAs": "Hell Binder", # Right tree focus
# Assassin:
"BwAAADI+S20A5dYAwOjJzogdgAMXIBufEZarmccQoWO1tGxQLAs": "Slayer Of Terramorphous Class Mod",
# Merc:
"BwAAADI+S20A5dYAwKjOzogdgAM/jBueEYCr2ccQoWN1t6xQLAs": "Slayer Of Terramorphous",
"BwAAADI+S20A5dYAwKjOTqMdgANlvhugEYyrWccQoWG1tGxQLAs": "Lucky Hoarder",
"BwAAADI+S20A5dYAwKjOzoA8gAPF5RG0HA0KSscQoWO1tGxQLAs": "Legendary Hoarder", # Synthesized, unconfirmed.
"BwAAADI+S20A5dYAwCjJDoI8gAP3KRG7HA0KascQoWO1tGxQLAs": "Legendary Gunzerker",
"BwAAADI+S20AJSlfPXdifokcgMPMlQqOBQSK/RcqeJk": "Blood of the Ancients", # SMG and Launcher ammo - other options available
# Soldier:
"BwAAADI+S20A5dYAwOjJzqAdgAMAXhuVEYKr2ccQoWO1t6xQLAs": "Legendary Berserker Class Mod", # V
"BwAAADI+S20A5dYAwOjOzqAdgANdoxuVEYKr2ccQoWO1t6xRLAs": "Legendary Berserker Class Mod",
"BwAAADI+S20A5dYAwOjJzogdgAN14hu8EWyruccQoWO1tOxQLAs": "Slayer Of Terramorphous",
"BwAAADI+S20A5dYAwKjOTosdgAOj/Ru9EXSruccQIWB1tmxQLAs": "Diehard Veteran Class Mod",
# Psycho:
"BwAAADI+S20A5dYAwOjJDog8gAP0NR22HAUK6sYQoWO1tGxQLAs": "Slayer of Terramorphous Class Mod",
# Blue rarity items, for lower-powered loot:
"BwAAADI+S20A5dYAwCjOzpsdgAMCwxuUEXirGcYQ4WO1tGxQLAs": "Chrono Binder Class Mod", # Maya
"BwAAADI+S20A5dYAwCjOTqMdgAO6yBugEY6rGcYQYWC1tGxQLAs": "Lucky Hoarder Class Mod", # Sally
"BwAAADI+S20A5dYAwCjOzoodgAOw1hulEWqrGcYQoW+1tGxQLAs": "Front Line Tactician Class Mod", # Axton
"BwAAADI+S20A5dYAwCjOjok8gAPr9xm7HQ0CCsYQYWC1tGxQLAs": "Superior Prodigy Class Mod", # Gaige
"BwAAADI+S20A5dYAwCjOjq8dgAOE6BucEZyrGcYQYWC1tGxQLAs": "Tricky Infiltrator Class Mod", # Zer0
"BwAAADI+S20A5dYAwCjOToQ8gAMWbB25HAMKCsYQIWG1tGxQLAs": "Diesel Sickle Class Mod", # Krieg
"BwAAADLCstH5WSW8NxTCzJ7fowG24Ru9EwLi2scQ4Xy3u65NbvU": "Throw'n Stik Jumpin Bitty",
"BwAAADLCshH5GSW8N9TfDJ/frQH8HRu9Ewji+sYQoX13pa5NbvU": "Homing Transfusion",
"BwAAADLCstH52daAN5TTzImfoQHlyhu9Ew7iWscQYXw3te5NLv4": "Lobbed Fire Burst",
"BwAAADLCshH5GSW8N1TdDKFfpwH0ARu9EwDiescQoXz3uq5NbvU": "Longbow MIRV",
"hwAAADJNsQrkKkufwWipjagYQ8EiWeu8nwyIu8cDKzv21C1D7DJN": "Monstrous Snider", # Elec (blue rarity)
},
"borderlands the pre-sequel": {
# Lasers
"igAAADJ+ogDSPtYAPme6Lrha4k4gJV28ni4JxtkBKztVdG/CaBC3": "E-GUN",
"igAAADJIsoD8PtYAPmfiDRUd2ME0uay5nk6I/9gBK6dUPG1S7sRI": "Tannis' Laser of Enlightenment", # V
"igAAADJIsoD7PtYAPmcCL3BZUkkgN2G8ni4LxtkBK6dULG1CbrBI": "Miss Moxxi's Vibra-Pulse",
"igAAADLIr4D8PtYAwGh9LuhaQk4gIsK8nG4JxtcBK8dV/G7ya5BF": "The ZX-1",
"igAAADIIsgAA5dYAPmfmDU0dKME0sGS5no6I/9gBK6dUJG3S7oRI": "MINAC's Atonement", # V
"igAAADLIsgAA5dYAPmfmDU0dKME0nDO5no6I/9gBK6dUPG3S7sRI": "MINAC's Atonement", # V
"igAAADJIsgAA5dYAPmfmDU0dKME07Ve5no6I/9gBK6dUPG3S7sRI": "MINAC's Atonement", # V
"igAAADLIsgD7Pgq3P2eWLDBaQk8ggYK8nY4OxsUBK0NVNG3yYFBI": "Excalibastard",
"igAAADIIsoD7PtYAPmfCDcUd+MA0q1K5nq6L/9gBK1dVNG0C72RI": "Thunderfire", # V
"igAAADJIsoD8Pgu3P2e2LsBYEk4gHLa8nc4KxtUBKxNVPG2yaLBI": "Lensed Mining Laser", # V
"igAAADKIsoD8Pgu3wZeYD+1YQksgXUy5ne6K/9QBK0NVJG0ybLBI": "Heated Subdivided Splitter", # V
"igAAADKIsoD8Pgu3zZeYDxVYAksgRBy5ne6K/9QBK0NVLG0SbFBI": "Catalyzing Lancer Railgun", # V
"igAAADIIsgD7Pgu35ZeYDz1fokggSRW5ne6K/9QBK0NVJG0ibHBI": "Niveous Zero Beam", # V
"igAAADLIsoD7PtYAPmdiL3hfAkkg/NG8n24LxtEBK8dUJG3Cb5BI": "Alternating Vandergraffen",
"igAAADKIsoD8PtYAPmd6L1BZEkkgcgS8nO4KxtcBK1NUPG3ib3BI": "Firestarta",
"igAAADLIsoD7Pgu3P2fKDW1fksA0z4+5nW6L/9QBK0NVLG1y70RI": "Stimulating Longest Yard",
# SMGs
"igAAADKWt7z+RgtH6Zf0DQUYaMo0lK25XAiN/9wBK8eUj+0FbB9K": "%Cu+ie^_^ki||er",
"igAAADLWsDzyRgpHP2eeDf3YpcY0Et25WOiI/8QBK1OVl+1F6eRL": "Incorporated Cheat Code", # V
"igAAADKWtzzyRg1HP2eeDS3YpcY0kga5WOiI/8QBK1OVj+1F6QRK": "Accessible Cheat Code", # V
"igAAADKWsDzyRghHP2eeDY3YpcY0vJe5WOiI/8QBK1OVn+1F6aRL": "All-In Cheat Code", # V
"igAAADKWsDz/RglH0ZfkDjXfBUsvXJu5XmiJ/9YBK7OUj+0VbP9L": "Deft Fox", # V
# Launchers
"igAAADJTvrb0UiFvxZeQDh2flMstKU25nq+M/9ABK7s0t6wk7z1J": "Large Spread", # V
"igAAADJTvrb0UitvwWitDz2e9MM0zHi+XxSL+9IBK2s1v6zU7ARJ": "Berrigan", # V
"igAAADLTv7b0Ui9vyZeQDs2fRMEtgma5nq+M/9ABK7s0p6wU7x1J": "Rocket Speed Launcher", # V
"igAAADLTvzbzUihvP2fOT8CYRMIhAVi8ng+KxtEBKxc0p6xU7fFK": "Sparkling Volt Thrower", # V
"igAAADKTv7b0UihvP2fyzoteRcYtaPa8ny+LxdsBK4M0v6wE7z1J": "Verm dee boom", # a basic blue for grinding with
"igAAADLTv7b0UiFvP2eKTgufVMIhlWK8nE+Kxt0BK080T60U7VFJ": "Heap'd Badaboom",
"igAAADKTv7bzUitvP2fuTxiehMMhQIe8nM+Kxt0BK080p6zU7VFJ": "Deep Pokket Thingy",
"igAAADITvrb0Ui1vP2fmT2ueRMEtkiK8nA+Kxd0BK080r6wU711J": "Clever Launcher",
"igAAADKTv7b0Ui9vP2cWbtufZMYtcj68mE+KxdMBK2s1T60E791K": "Paritisan RPG",
"igAAADJTvrb0UiFvL2eaziOfdMIhIs68n4+KxtsBK2M1v6wk7RFJ": "derp Nukem",
"igAAADKTv7b0UipvP2dqa2OelMMhLLy8mK+KxtMBK2s1p6z07fFK": "Victorious Mongol",
# Snipers
"igAAADIPtCIA5Qs/P2fSz7AftsMjt7i8nWiKxMcBK6t0vi0W7VNI": "Tl'kope Razorback", # V
"igAAADIZsaLxeg0/P2fmzyicw8Mja3u8mKiKxNMBK0N1/q3z7VNK": "The Machine", # V
"igAAADIZtCLweg8/15dYDm0flscoKae5n6iO/9QBK2N11i2G7XhK": "Auditing Sniper Rifle", # V
"igAAADLZsqLveg8/P2eKDmofhskoPgu8nwiIu9UBK3907i226RhB": "Auditing Fremington's Edge", # V
"igAAADIZsSLweg0/P2eaDgof9sMj1hi8nyiLxNUBK2N17i2G7bNI": "Venture Invader",
"igAAADIZsaLvego/P2eaDtof9sMjQ568nyiLxNUBK2N17i2G7bNI": "Longitudinal Invader",
"igAAADIZtKLvei0/NRfPz4gfhsMj10W8mAiKxNMBK2d0vi1W7bNI": "Bolshy Longnail",
"igAAADIZtCIA5Qo/A2eeDuIfBsMjyoq8nQiLxMcBK7N05i227XNK": "Siah-siah Skullmasher", # Luneshine - 3% shields on kill
"igAAADJZsSLxeg0/BWeGDgIf5sMjUjG8nMiKxNcBK6N07i2W7bNK": "Night Pitchfork", # Luneshine - grenade damage
"igAAADKZsiLxego/P2eSDuofFsMj43m8nuiKxNkBK4t0vi2m7ZNK": "Dandy Magma",
"igAAADIZsSLxeiw/MWeSDoIfFsMjGYK8nuiKxNkBK4t05i2m7ZNK": "Monstrous Magma", # Luneshine - crit damage
"igAAADLZsiLxego/M2eaDtof9sMjrsC8nyiLxNUBK2N1/i2G7ZNK": "Longitudinal Invader", # Luneshine - chance to ignore shields
"igAAADKZsqLxeiw/P2f6DoofBsYoss28mMiKu9MBK2d0/i2G7XhK": "Bolshy Pooshka",
# Grenades
"CgAAADKHslTznCI5wCjTCaHagAGPLBu9DQRSO+cQoWZ3tKtRa/0": "Bonus Package",
"CgAAADIHp1T5nCI5wCjTCaHagAGJEhu9DQRSO+cQoWZ3tKtRa/0": "Longbow Bonus Package",
"CgAAADKHslTz3NaAONHRCZ4agQFt1Bu+DQZS2+cQ4Wb3tatRK/A": "Quasar", # V
"CgAAADLHoFT53Dx5OFHXCZFagQExsRu8CABSG+cQIWY3vKtRa/0": "Explosive Kiss of Death", # V
"CqEzNA7LswseiQITzKdx+ROZACnogyewqB+Gu/ISr/C2sJiy7FA": "Snowball", # V
"CgAAADIHp5Tt3NaAOJHXyX4juARJ3RuzCFp6m+cQYZkLjatRK/E": "Snowball", # V
# Shields
"CgAAADIJS20A5dYAwGjeh5HU1grlMBugC5qCGecQ4WF8sKVTJcQ": "Majestic Incendiary Spike Shield", # V
"CgAAADIjS20A5dYAwKjI7X6jgSDCqB6+HQESyucQIWF8sKVTJfU": "Shield of Ages",
"CgAAADIjS20A5dYAwKjJ7X7jgiA5wx6/HQMSiucQIW58s6VTZfY": "Naught",
"CgAAADIJS20A5dYAwOjBx36j0ArCAxupC1qCOecQoWH8sCVSZdo": "Asteroid Belt", # V
"CgAAADIJS20A5dYAwOjBx37j0ArAkhuqC2SCmeYQYWB8seVRJdo": "Miss Moxxi's Slammer", # V
"CgAAADIJS20A5dYAwCjtx37jsQqVaBuvC16C2ecQoWE8sKVRpdY": "Prismatic Bulwark",
"CgAAADIjS20A5dYAwGjJ7cNUgyDtQx64HQ0SiuYQIWF8kyVSZfc": "Hippocratic M0RQ",
"CgAAADIJS20A5dYAwGjix34jvQpY5RuyC1SCGecQIWB8sSVR5dE": "Avalanche", # Synth
"CgAAADIJS20A5dYAwOjtx36jsQrTGhutC1KCuecQIWF8sKV25dY": "Kala", # Synth
"CgAAADIJS20A5dYAwKjtx35jsQokxBuuC1yCuecQoWH8saVRJdU": "The Sham", # Synth
"CgAAADIJS20A5dYAwKjqx34jxAr3+Bu3C06CGecQIWF8sCVSZdA": "Supernova", # Synth
"CgAAADIjS20A5dYAwOjI7X4jgSDAXB69HQcSyucQoW58sCVSJfQ": "Rerouter", # Synth
# Oz Kits
"CgAAADI+S20ApSB4OlA1MJNbkwVxmhunClJy2+cQ4WezStKvEgs": "Voltaic Support Relay",
"CgAAADI+S20AZS74OlA18H7jjAWpDhu7CmJym+cQ4WazStKvEgs": "Tranquility Oz Kit",
# Gladiator
"CgAAADI+S20A5dYAwCjYD5VdgALTFhuoEziKuecQYXW0pG1ALQs": "Celestial Gladiator Title.Title_ClassMod",
"CgAAADI+S20A5dYAwOjYj5BdgALHthutEz6KmeYQoWu0pG1ALQs": "Eternal Protector Title.Title_ClassMod",
"CgAAADI+S20A5dYAwOjYT5VdgAJr0xupEzqK2ecQYXU0pC1fLQs": "Eridian Vanquisher Title.Title_ClassMod", # V
"CgAAADI+S20A5dYAwOjZD4F8gALFKhm9HT8q6ucQYXV0pG1ALQs": "Chronicler Of Elpis Title.Title_ClassMod", # V
# Prototype
"CgAAADI+S20A5dYAwCjYj4NegAJuiBu8DCqKueYQYW20pG1fLQs": "Stampeding Brotrap Title.Title_ClassMod", # V
"CgAAADI+S20A5dYAwKjYz4NegALL8Ru8DDSKueYQ4Wt0pG1fLQs": "Inspirational Brotrap Title.Title_ClassMod", # V
"CgAAADI+S20A5dYAwKjZT4degAIo+hu5DCqKWecQoW10pC1ALQs": "Loot Piñata Title.Title_ClassMod", # V
"CgAAADI+S20A5dYAwOjZD4F8gALIaBm5HTsq6ucQYXX0pa1fLQs": "Chronicler Of Elpis Title.Title_ClassMod", # V
# Baroness
"CgAAADI+S20A5dYAwOjZT4p8gAIwQh2yHAMCCucQIWp0pK1fLQs": "High-and-Mighty Gentry Title.Title_ClassMod", # only a blue, need a purple
"CgAAADI+S20A5dYAwKjYD4Z8gALIJB2wHA0C6ucQYWq0pG1ALQs": "Posh Blue Blood Title.Title_ClassMod",
"CgAAADI+S20A5dYAwKjYD4B8gAK3FB2+HA8CyucQYXW0pG1ALQs": "Celestial Baroness Class Mod", # Synth
"CgAAADI+S20A5dYAwKjYT4tegALZ1R29HAkCyucQYXW0pG1ALQs": "Eridian Vanquisher Class Mod", # Synth
# Uncategorized
"igAAADKIsoD8Pgu3wZeYD+1YQksgXUy5ne6K/9QBK0NVJG0ybLBI": "Heated Subdivided Splitter", # Error code O4L4M0A0
"igAAADISS+3rVgZnP2fuDa0e6MY0XmG53Q+J/9wBKy8U5+8U6YRI": "Party Popper",
"igAAADISS23qVhpnP2deDyXZ5MA0Bu++28+K+9QBK1cUv+0U7yRM": "Win-Win T4s-R",
"igAAADJZsaLvego/45dEDu0flssomQ25nsiO/9gBK4t0/i227XhK": "Dandy Rakehell", # Error code O2L1M0A4
"igAAADLIoQD8Pny3P2eSLqhaAk4gUfa8ng4JxtkBKydVPG9yaJBC": "Thorny Ol' Rosie",
"igAAADKWsDzyRjdHP2fiL4VcFUI0lZq5XEgK/NwBK/+Ul+0lbaRL": "Reddy Fast Talker",
"igAAADISS+3pVhtnA2fqzuvZpE8t0DC83m8JutEBK7cV7++UbH1M": "Jam Packed Biggun",
"igAAADIRpLL3WtYAwGjFL4Xb5EI00zi5XDQK/NwBK8N0Ryx0bWRJ": "Tangy Boss Nova",
"igAAADLXtz4A5Q1PP2eCDa2Yxcs0Vpe5HCiN/8YBK7O0p61l5MRL": "Doc's Flayer",
"igAAADKWsLzxRjdHwWiRD83ZxcM0w+2+WJSK+9ABKyOVh+0F7ORL": "Sparkling Boxxy Gunn",
"igAAADKTvjbzUipvP2fOT1CeRMIh0fe8ng+KxtEBKxc0p6xU7TFJ": "Ultraprecise Volt Thrower",
"igAAADISS+3qVgRnP2dub9gZFkEhksO83E8Ix9cBK38U/++kb5FI": "Shock Gwen's Other Head",
"CgAAADIJS20A5dYAwOjlx34j0QorbBurC2aC+ecQoWF8s6V/5ds": "Sunshine",
"CgAAADIJS20A5dYAwCjDx37jnwoTIBu9CwCC+ecQoWG8sGVRpfI": "Haymaker",
"igAAADKIsgAA5dYAPmfmDU0dKME00g+5no6I/9gBK6dULG3S7qRI": "MINAC's Atonement",
"igAAADJRpbL2mjB/fZfgDx0cWMM0W4i+3W6K+9wBK8N0py+U7ORJ": "Bloody Wiggly Cry Baby",
"igAAADKIsoD8PtYAPmfiDRUd2ME0d0S5nk6I/9gBK6dULG1S7sRI": "Tannis' Laser of Enlightenment",
"CgAAADLHoFT53NZAOJHL7n7jgCO3KB69HAcaCucQYZnLtYxR6/E": "Data Scrubber",
"igAAADIXtIryQtYAwGjZj1gcI8Mi6b28HAiLx8cBK2f0vC1z7dJJ": "Boomacorn",
"igAAADJ+tj4A5TZPP2eGj+hctcAihVu8HyiLx9sBK1O1v6117BK3": "Lumpy Jack-o'-Cannon",
"igAAADLXtz7yQjZPwWjND93fhcI0fVS+XRSK+9ABK5u0r6117cRJ": "Toasty Party Line",
},
}
# Requires access to the Gibbed data files. One version works on pre-Commander-Lilith game files,
# the other on post-Commander, since that update changed a bunch of stuff.
ASSET_PATH_OLD = "../GibbedBL2/Gibbed.Borderlands{game}/projects/Gibbed.Borderlands{game}.GameInfo/Resources/{fn}.json"
ASSET_PATH = "../Borderlands{game}Dumps/{fn}.json"
def get_asset(fn, cache={}):
if fn not in cache:
if GAME == "borderlands 2": path = ASSET_PATH.format(game="2", fn=fn)
else: path = ASSET_PATH_OLD.format(game="Oz", fn=fn)
with open(path, "rb") as f: cache[fn] = json.load(f)
return cache[fn]
def get_asset_library_manager():
config = get_asset("Asset Library Manager")
if "_sets_by_id" not in config:
# Remap the sets to be keyed by ID - it's more useful that way.
config["_sets_by_id"] = {set["id"]: set for set in config["sets"]}
# Build a mapping from item identifier to (set,subid,asset) triple.
if "_find_asset" not in config:
cfg = config["_find_asset"] = collections.defaultdict(dict)
for set in config["sets"]:
for field, libinfo in set["libraries"].items():
for sublib, info in enumerate(libinfo["sublibraries"]):
for asset, name in enumerate(info.get("assets", [])):
# HACK: Exclude everything from the Commander Lilith DLC
# I'm seeing a bunch of duplication that is causing issues.
if "Anemone" in info["package"]: continue
if args.verify and name in cfg[field]:
print("Got duplicate", field, name, cfg[field][name], (set["id"], sublib, asset))
cfg[field][name] = set["id"], sublib, asset, info["package"]
return config
def get_balance_info(is_weapon, balance):
cls = "Weapon" if is_weapon else "Item"
allbal = get_asset(cls + " Balance")
if balance not in allbal:
# If something goes wrong, this will probably KeyError either looking up the BalanceDef, or in the subsequent lookup in allbal.
config = get_asset_library_manager()
setid, sublib, asset, cat = config["_find_asset"]["BalanceDefs"][balance]
balance = cat + "." + balance
info = allbal[balance]
base = get_balance_info(is_weapon, info["base"]) if "base" in info else {"parts": { }}
ret = {k:v for k,v in base.items() if k != "parts"}
for k,v in info.items():
if k == "parts":
if isinstance(v, str):
# Instead of having all the parts here, they're a reference to another file
v = get_asset(cls + " Balance Part Lists")[v]
mode = v.get("mode")
# I *think* that Complete means to ignore the base? There never seems to be a base anyhow.
if mode == "Complete": ret["parts"] = { }
else: ret["parts"] = {k:v for k,v in base["parts"].items() if k != "mode"}
for part, opts in v.items():
if part == "mode": continue
# Don't understand Additive. It doesn't seem to be used with both a base and parts.
# Not sure if this is correct or if things are done in the right order or anything.
if mode == "Additive" and isinstance(ret["parts"].get(part), list) and isinstance(opts, list):
ret["parts"][part] = opts + ret["parts"][part]
else:
ret["parts"][part] = opts
elif k != "base": ret[k] = v # Don't copy in the base once it's rendered
return ret
@dataclass
class Asset:
seed: None
is_weapon: None
type: "*Types"
balance: "BalanceDefs"
brand: "Manufacturers"
# There are two fields, "Grade" and "Stage". Not sure what the diff
# is, as they seem to be equal (except when grade is 0 and stage is 1?).
grade: int
stage: int
pieces: ["*Parts"] * 8
material: "*Parts"
pfx: "*Parts"
title: "*Parts"
@property
def partnames(self): return partnames(self.is_weapon) # Convenience lookup property
@classmethod
def decode_asset_library(cls, data):
orig = data
seed = int.from_bytes(data[1:5], "big")
dec = data[:5] + bogocrypt(seed, data[5:], "decrypt")
if args.verify:
reconstructed = dec[:5] + bogocrypt(seed, dec[5:], "encrypt")
if data != reconstructed:
print("Imperfect reconstruction of weapon/item:")
print(data)
print(reconstructed)
raise AssertionError
data = dec + b"\xFF" * (40 - len(dec)) # Pad to 40 with 0xFF
crc16 = int.from_bytes(data[5:7], "big")
data = data[:5] + b"\xFF\xFF" + data[7:]
crc = binascii.crc32(data)
crc = (crc >> 16) ^ (crc & 65535)
if crc != crc16: raise ValueError("Checksum mismatch")
config = get_asset_library_manager()
# The first byte is a version number, with the high
# bit set if it's a weapon, or clear if it's an item.
is_weapon = data[0] >= 128
weap_item = "Weapon" if is_weapon else "Item"
if (data[0] & 127) != config["version"]: raise ValueError("Version number mismatch")
uid = int.from_bytes(data[1:5], "little")
if not uid: return None # For some reason, there are a couple of null items at the end of inventory. They decode fine but aren't items.
setid = data[7]
bits = ConsumableLE.from_bits(data[8:])
def _decode(field):
cfg = config["configs"][field]
asset = bits.get(cfg["asset_bits"])
sublib = bits.get(cfg["sublibrary_bits"] - 1)
useset = bits.get(1)
if "0" not in (useset+sublib+asset): return None # All -1 means "nothing here"
cfg = config["_sets_by_id"][setid if useset == "1" else 0]["libraries"][field]
# print(field, cfg["sublibraries"][int(sublib,2)]["assets"][int(asset,2)])
return cfg["sublibraries"][int(sublib,2)]["assets"][int(asset,2)]
ret = {"seed": seed, "is_weapon": is_weapon}
for field, typ in cls.__dataclass_fields__.items():
typ = typ.type
if typ is None:
continue # Not being decoded this way
if typ is int:
ret[field] = int(bits.get(7), 2)
elif isinstance(typ, str):
ret[field] = _decode(typ.replace("*", weap_item))
elif isinstance(typ, list):
ret[field] = [_decode(t.replace("*", weap_item)) for t in typ]
else:
raise AssertionError("Bad annotation %r" % typ)
ret = cls(**ret)
if args.verify:
if ret.encode_asset_library() != orig:
raise AssertionError("Weapon reconstruction does not match original: %r" % ret)
return ret
def encode_asset_library(self):
# NOTE: Assumes that at least one decode has been done previously.
bits = []
config = get_asset_library_manager()
fields = []
needsets = {0}
def _encode(field, item):
cfg = config["configs"][field]
fields.append("%s-%d-%d" % (field, cfg["asset_bits"], cfg["sublibrary_bits"]))
if item is None:
bits.append("1" * (cfg["asset_bits"] + cfg["sublibrary_bits"]))
return
setid, sublib, asset, cat = config["_find_asset"][field][item]
needsets.add(setid)
bits.append(format(asset, "0%db" % cfg["asset_bits"])[::-1])
bits.append(format(sublib, "0%db" % (cfg["sublibrary_bits"]-1))[::-1])
bits.append("1" if setid else "0")
weap_item = "Weapon" if self.is_weapon else "Item"
for field, typ in self.__dataclass_fields__.items():
typ = typ.type
if typ is None:
continue # Not being encoded this way
if typ is int:
bits.append(format(getattr(self, field), "07b")[::-1])
elif isinstance(typ, str):
_encode(typ.replace("*", weap_item), getattr(self, field))
elif isinstance(typ, list):
for t, piece in zip(typ, getattr(self, field)):
_encode(t.replace("*", weap_item), piece)
if len(needsets) > 2: print("Need multiple set IDs! Cannot encode.", needsets)
# Note that needsets might still be {0}, in which case we'll render a setid of 0.
bits = "".join(bits)
bits += "1" * (8 - (len(bits) % 8))
data = int(bits[::-1], 2).to_bytes(len(bits)//8, "little")
data = (
bytes([config["version"] | (128 if self.is_weapon else 0)]) +
self.seed.to_bytes(4, "big") + b"\xFF\xFF" + bytes([max(needsets)]) +
data
)
data = data + b"\xFF" * (40 - len(data)) # Pad for CRC calculation
crc = binascii.crc32(data)
crc = (crc >> 16) ^ (crc & 65535)
# data = (data[:5] + crc.to_bytes(2, "big") + data[7:]).rstrip(b"\xFF")
# print(' '.join(format(x, "08b")[::-1] for x in data))
# print(' '.join(format(x, "08b")[::-1] for x in (dec[:5] + b"\xFF\xFF" + dec[7:])))
return data[:5] + bogocrypt(self.seed, (crc.to_bytes(2, "big") + data[7:]).rstrip(b"\xFF"), "encrypt")
def get_title(self):
if self.type == "ItemDefs.ID_Ep4_FireHawkMessage": return "FireHawkMessage" # This isn't a real thing and doesn't work properly. (It'll be in the bank when you find out about the Firehawk.)
weap_item = "Weapon" if self.is_weapon else "Item"
config = get_asset_library_manager()
setid, sublib, asset, cat = config["_find_asset"][weap_item + "Types"][self.type]
typeinfo = get_asset(weap_item + " Types")[cat + "." + self.type]
if typeinfo.get("has_full_name"):
# The item's type fully defines its title. This happens with a number
# of unique and/or special items.
return typeinfo["name"]
# Otherwise, build a name from a prefix (possibly) and a title.
# The name parts have categories and I don't yet know how to reliably list them.
names = get_asset(weap_item + " Name Parts")
pfxinfo = None
if self.pfx:
setid, sublib, asset, cat = config["_find_asset"][weap_item + "Parts"][self.pfx]
pfxinfo = names.get(cat + "." + self.pfx)
# pfxinfo has a name (unless it's a null prefix), and a uniqueness flag. No idea what that one is for.
if self.title:
setid, sublib, asset, cat = config["_find_asset"][weap_item + "Parts"][self.title]
titinfo = names.get(cat + "." + self.title)
title = titinfo["name"] if titinfo else self.title
else: title = "<no title>"
if pfxinfo and "name" in pfxinfo: title = pfxinfo["name"] + " " + title
return title
def __repr__(self):
if self.grade == self.stage: lvl = "Lvl %d" % self.grade
else: lvl = "Level %d/%d" % (self.grade, self.stage)
type = self.type.split(".", 1)[1].replace("WT_", "").replace("WeaponType_", "").replace("_", " ")
ret = "%s %s (%s)" % (lvl, self.get_title(), type)
if args.itemids: ret += " {%s}" % armor_serial(self.encode_asset_library())
if args.pieces: ret += "\n" + " + ".join(filter(None, self.pieces))
if args.raw: ret += "\n" + ", ".join("%s=%r" % (f, getattr(self, f)) for f in self.__dataclass_fields__)
if args.library and "{}" in args.library: args.library += ",{%s}" % armor_serial(self.encode_asset_library())
return ret
#if args.raw: del __repr__ # For a truly-raw view (debugging mode).
def decode_tree(bits):
"""Decode a (sub)tree from the given sequence of bits
Returns either a length-one bytes, or a tuple of two trees (left
and right). Consumes either a 1 bit and then eight data bits, or
a 0 bit and then two subtrees.
"""
if bits.get(1) == "1": # Is it a leaf?
return int(bits.get(8), 2)
# Otherwise, it has subnodes.
return (decode_tree(bits), decode_tree(bits))
def huffman_decode(data, size):
bits = Consumable.from_bits(data)
root = decode_tree(bits)
global last_huffman_tree; last_huffman_tree = root
ret = []
while len(ret) < size:
cur = root
while isinstance(cur, tuple):
cur = cur[bits.get(1) == "1"]
ret.append(cur)
# The residue doesn't always consist solely of zero bits. I'm not sure
# why, and I have no idea how to replicate it. Hopefully it doesn't
# matter.
residue = bits.peek()
global last_huffman_residue; last_huffman_residue = residue
if len(residue) >= 8: raise ValueError("Too much compressed data - residue " + residue)
return bytes(ret)
def huffman_encode(data):
if not data: return data # Probably wrong but should never happen anyway
# First, build a Huffman tree by figuring out which bytes are most common.
counts = collections.Counter(data)
while len(counts) > 1:
# Pick the two least common and join them
(left, lfreq), (right, rfreq) = counts.most_common()[-2:]
del counts[left], counts[right]
counts[(left, right)] = lfreq + rfreq
[head] = counts # Grab the sole remaining key
if args.verify: head = last_huffman_tree # Hack: Reuse the tree from the last decode (gives bit-for-bit identical compression)
# We now should have a Huffman tree where every node is either a leaf
# (a single byte value) or a tuple of two nodes with approximately
# equal frequency. Next, we turn that tree into a bit sequence that
# decode_tree() can parse, and also (for convenience) flatten it into
# a lookup table mapping byte values to their bit sequences.
bits = {}
ret = []
def _flatten(node, seq):
if isinstance(node, tuple):
ret.append("0")
_flatten(node[0], seq + "0")
_flatten(node[1], seq + "1")
else:
ret.append("1" + format(node, "08b"))
bits[node] = seq
_flatten(head, "")
# Finally, the easy bit: turn every data byte into a bit sequence.
ret.extend(bits[char] for char in data)
ret = "".join(ret)
spare = len(ret) % 8
if spare:
# Hack: Reuse the residue from the last decode. I *think* this is just
# junk bits that are ignored on load.
if args.verify and len(last_huffman_residue) == 8-spare: ret += last_huffman_residue
else: ret += "0" * (8-spare)
return int(ret, 2).to_bytes(len(ret)//8, "big")
def get_varint(data):
"""Parse a protobuf varint out of the given data
It's like a little-endian version of MIDI's variable-length
integer. I don't know why Google couldn't just adopt what
already existed.
"""
scale = ret = 0
byte = 128
while byte > 127:
byte = data.get(1)[0]
ret |= (byte&127) << scale
scale += 7
return ret
def build_varint(val):
"""Build a protobuf varint for the given value"""
data = []
while val > 127:
data.append((val & 127) | 128)
val >>= 7
data.append(val)
return bytes(data)
# Handle protobuf wire types by destructively reading from data
protobuf_decoder = [get_varint] # Type 0 is varint
@protobuf_decoder.append
def protobuf_64bit(data):
return data.get(8)
@protobuf_decoder.append
def protobuf_length_delimited(data):
return data.get(get_varint(data))
@protobuf_decoder.append
def protobuf_start_group(data):
raise Exception("Unimplemented")
@protobuf_decoder.append
def protobuf_end_group(data):
raise Exception("Unimplemented")
@protobuf_decoder.append
def protobuf_32bit(data):
return data.get(4)
int32, int64 = object(), object() # Pseudo-types. On decode they become normal integers.
class ProtoBuf:
# These can be packed into arrays.
PACKABLE = {int: get_varint, int32: protobuf_decoder[1], int64: protobuf_decoder[5]}
@staticmethod
def decode_value(val, typ, where):
if isinstance(val, int): return val # Only for varints, which should always be ints
assert isinstance(val, bytes)
if isinstance(typ, type) and issubclass(typ, ProtoBuf): return typ.decode_protobuf(val)
if typ in (int32, int64): return int.from_bytes(val, "little")
if typ is float: return struct.unpack("<f", val) # TODO: Should this be subscripted [0]?
if typ is str: return val.decode("UTF-8")
if typ is bytes: return val
if typ in (list, dict): return val # TODO
raise ValueError("Unrecognized annotation %r in %s: data %r" % (typ, where, val[:64]))
@classmethod