Skip to content

Instantly share code, notes, and snippets.

@richtan
Last active January 5, 2023 04:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save richtan/f686d63424c0537874f097bc35d292d1 to your computer and use it in GitHub Desktop.
Save richtan/f686d63424c0537874f097bc35d292d1 to your computer and use it in GitHub Desktop.
import time
import logging
from _remote import ffi, lib
import os
from manager import PluginBase, report_exception
import util
'''
By richtan
Based on playerlist.py and Alexrns's sc loot logger plugin
WIP
Last updated: 1/4/23
'''
'''
struct Chest {
struct ClientObject asClientObject;
struct RepeatedPtrField items; // ItemProperties
struct STDVector angles; // int,std::allocator<int>
int pos;
int untilThrow;
};
struct ItemProperties {
void *classptr;
int32_t type;
int32_t slotpos;
struct RepeatedPtrField statboosts; // StatBoost
int32_t _cached_size;
uint32_t _has_bits[1];
};
struct StatBoost {
void *classptr;
uint32_t stat;
int32_t val;
int32_t level;
bool increment;
struct STDString *subtypestr;
int32_t subtype;
int32_t _cached_size;
uint32_t _has_bits[1];
};
struct Loot {
struct ClientObject asClientObject;
struct ItemProperties itemProps;
struct ItemDescription *itemDesc;
int untilAutoApply;
};
struct ItemDescription {
void *classptr;
struct STDString *name;
uint32_t type;
uint32_t build;
int32_t tier;
uint32_t price;
uint32_t currency;
uint32_t itemclass;
int32_t itemsubclass;
uint32_t slot;
struct RepeatedField_int usableby;
struct STDString *lootvid;
struct STDString *lootdestroyvid;
struct STDString *lootsound;
int32_t lootlifetime;
uint32_t anim;
struct STDString *aaid;
struct STDString *imagemap;
struct STDString *sound;
struct STDString *altsound;
struct STDString *emitsound;
int32_t cooldown;
int32_t numsubs;
int32_t jitter;
bool autoapply;
bool candrop;
bool lootisglobalplayerevent;
bool unhitcancel;
int32_t maxheat;
int32_t speed;
int32_t secondarysubs;
int32_t simultaneous;
struct AnchoredImage *anchoredimage;
struct RepeatedPtrField statboosts; // StatBoost
struct RepeatedPtrField obj; // ItemObjDescription
int32_t bounces;
int32_t _cached_size;
uint32_t _has_bits[2];
};
'''
ITEMS = {
1: 'Aesc', 2: 'Frag', 4: 'Shotgun', 5: 'Arblast', 6: 'Sledge', 7: 'Kyeser', 9: 'Skein', 10: 'Warp',
11: 'Mantlet', 12: 'Macron', 14: 'Jerkin', 15: 'Trackers', 16: 'Empyreals', 18: 'Health Boost 1',
19: 'Max Health Boost 1', 20: 'Max Health Boost 2', 21: 'Ammo Boost', 22: 'Max Ammo Boost 1',
23: 'Max Ammo Boost 2', 31: 'Kantikoy Repeater', 32: 'Relentless Mantlet', 33: 'Plating', 34: 'Hyrst',
35: 'Nacre Skein', 36: 'Cruel Sledge', 38: 'Improved Frag', 39: 'Cluster', 40: 'Swarm Cluster',
41: 'Biting Aesc', 42: 'Acrotic Warp', 43: 'Sabatons', 44: 'Irascent Kyeser', 45: 'Swarmer',
46: 'Polychrome Skein', 47: 'Adjudicator', 48: 'Protracted Macron', 49: '1 EC', 50: '10 EC',
51: 'Implant Slot 1', 53: 'Dancer', 54: 'Antiphon', 55: 'Durable Jerkin', 58: 'Deepfire Dancer',
59: 'Defiant Antiphon', 60: 'Woven', 61: 'Valenki', 63: 'Blast', 66: 'Trinity', 67: 'Whispered Antiphon',
68: 'Mechanized Macron', 69: 'Empowered Blast', 70: 'Forceful Blast', 71: 'Scattered Blast',
72: 'Gnawing Aesc', 73: 'Inferior Bellites', 74: 'Bellites', 76: 'Inferior Salites', 77: 'Salites',
79: 'Inferior Delites', 80: 'Delites', 82: 'Inferior Calites', 83: 'Calites', 85: 'Inferior Armites',
86: 'Armites', 88: 'Exites', 89: 'Inferior Emites', 90: 'Emites', 92: 'Max Health Boost 3',
93: 'Max Health Boost 4', 94: 'Max Ammo Boost 3', 95: 'Max Ammo Boost 4', 96: '5 EC', 97: '25 EC',
98: 'Abiding Warp', 99: 'Fungus Cave Key', 100: 'Underground Base Key', 101: 'Graveyard Key',
102: 'Durable Plating', 103: 'Durable Hyrst', 104: 'Durable Woven', 105: 'Gliders', 106: 'Volatile Gliders',
107: 'Strands Key', 108: 'Soidal Repeater', 109: 'Bolt Thrower', 110: 'Proxies', 111: 'Kulbeda',
112: 'Mirrored Warp', 113: 'Gavotte', 114: 'Quickstep', 115: 'Burner', 116: 'Balefire Skein', 117: 'Baetyl',
118: 'Preceptor', 119: 'Baculus', 120: 'Armor Boost 1', 121: 'Armor Boost 2', 122: 'Armor Boost 3',
123: 'Armor Boost 4', 124: 'Movement Speed Boost 1', 125: 'Movement Speed Boost 2',
126: 'Movement Speed Boost 3', 127: 'Movement Speed Boost 4', 128: 'Jump Height Boost 1',
129: 'Jump Height Boost 2', 130: 'Jump Height Boost 3', 131: 'Jump Height Boost 4', 132: 'Damage Boost 1',
133: 'Damage Boost 2', 134: 'Damage Boost 3', 135: 'Damage Boost 4', 136: 'Crit Chance Boost 1',
137: 'Crit Chance Boost 2', 138: 'Crit Chance Boost 3', 139: 'Crit Chance Boost 4', 140: 'Crit Damage Boost 1',
141: 'Crit Damage Boost 2', 142: 'Crit Damage Boost 3', 143: 'Crit Damage Boost 4', 144: 'Stonebow',
145: 'Temple of the Lost Key', 146: 'Lightfoot', 147: 'Compound Repeater', 148: 'Redshift Repeater',
149: 'Tektite Repeater', 150: 'Dragoon', 151: 'Contrived Frag', 152: 'Synthetic Frag', 153: 'Reforged Frag',
154: 'Withering Blast', 155: 'Voracious Blast', 156: 'Zealous Blast', 157: 'Cobalt Skein',
158: 'Sapphire Skein', 159: 'Berylblade Skein', 160: 'Falcate Aesc', 161: 'Keen Aesc', 162: 'Violet Aesc',
163: 'Beloid Warp', 164: 'Daedal Warp', 165: 'Aeon Warp', 166: 'Vengeful Sledge', 167: 'Sinful Sledge',
168: 'Baleful Sledge', 169: 'Caustic Kyeser', 170: 'Insidious Kyeser', 171: 'Nefarious Kyeser',
172: 'Fortified Mantlet', 173: 'Banded Mantlet', 174: 'Annealed Mantlet', 175: 'Everdawn Dancer',
176: 'Fellwhip Dancer', 177: 'Darkstar Dancer', 178: 'Invidious Antiphon', 179: 'Mocking Antiphon',
180: 'Profane Antiphon', 181: 'Recalcitant Macron', 182: 'Inveterate Macron', 183: 'Composite Macron',
184: 'Augmented Trackers', 185: 'Augmented Sabatons', 186: 'Augmented Valenki', 187: 'Stompers',
188: 'Bandolier', 189: 'Double Bandolier', 190: 'Berserker', 191: 'Blockers', 192: 'Enraged Blockers',
193: 'Needlebow', 194: 'Tripwire', 195: 'Haunting Aesc', 196: 'Residual Warp', 197: 'Lancet', 198: 'Hotfoot',
199: 'Telson', 200: 'Mangonel', 201: 'Vanquished Key', 205: 'Unknown Key', 206: 'Mysterious Key',
207: 'Aerolith Repeater', 208: 'Bombard', 209: 'Jansky Repeater', 210: 'Unstable Frag', 212: 'Hydra',
213: 'Furious Blast', 214: 'Fireform', 215: 'Vitreous Skein', 216: 'Motley', 217: 'Skyfire Skein',
218: 'Radiant Aesc', 219: 'Kunai', 221: 'Tacent Warp', 222: 'Mimic', 223: 'Corrupt Sledge', 224: 'Quaestor',
226: 'Diabolical Kyeser', 227: 'Locust', 228: 'Eternal Mantlet', 229: 'Direfall Dancer', 230: 'Volta',
232: 'Blasphemous Antiphon', 233: 'Scorpion', 234: 'Devoted Macron', 235: 'Skiff', 236: 'Max Health Boost 5',
237: 'Max Ammo Boost 5', 238: 'Armor Boost 5', 239: 'Movement Speed Boost 5', 240: 'Jump Height Boost 5',
241: 'Damage Boost 5', 242: 'Crit Chance Boost 5', 243: 'Crit Damage Boost 5', 244: 'Augmented Plating',
245: 'Reinforced Plating', 246: 'Resilient Plating', 247: 'Augmented Jerkin', 248: 'Reinforced Jerkin',
249: 'Resilient Jerkin', 250: 'Augmented Hyrst', 251: 'Reinforced Hyrst', 252: 'Resilient Hyrst',
253: 'Augmented Woven', 254: 'Reinforced Woven', 255: 'Resilient Woven', 256: 'Frenzied Berserker',
257: 'Enlarged Blockers', 258: 'Durable Trackers', 259: 'Reinforced Trackers', 260: 'Resilient Trackers',
261: 'Talons', 262: 'Durable Talons', 263: 'Augmented Talons', 264: 'Reinforced Talons',
265: 'Resilient Talons', 266: 'Aeonic Empyreals', 267: 'Durable Sabatons', 268: 'Reinforced Sabatons',
269: 'Resilient Sabatons', 270: 'Spiked Stompers', 271: 'Durable Valenki', 272: 'Reinforced Valenki',
273: 'Resilient Valenki', 274: 'Overloaded Boosters', 277: 'Boosters', 291: '1 UC', 292: '5 UC',
299: 'Inferior Velites', 300: 'Velites', 302: 'Inferior Firites', 303: 'Firites', 305: 'Blink',
308: 'Inferior Ultrites', 309: 'Ultrites', 311: 'Silent Antiphon', 312: 'Triad'
}
STATS = {
4: 'max ammo',
10: 'armor',
11: 'mvs',
12: 'jh',
13: 'range',
14: 'precise',
18: 'dmg',
23: 'cc',
20: 'cd',
21: 'air',
22: 'kbr',
24: 'cc',
25: 'cd',
27: 'max ammo',
32: 'pen',
33: 'bounce',
34: 'exp ammo',
35: 'inc',
37: 'speed',
38: 'life',
39: 'seek',
42: 'acc',
43: 'ap',
45: 'sim',
46: 'shrap',
47: 'barb',
48: 'tri',
49: 'gravity',
50: 'leech',
51: 'exhit',
52: 'reflect',
53: 'spintime',
54: 'armor',
55: 'qs',
56: 'shield',
}
MODIFIER_PREFIXES = ["", "Modified ", "Custom ", "Experimental ", "Prototype "]
COLUMNS = 'modifiers name'.split()
def boostToModifier(item, stat, val):
try:
if stat == 57:
if item in [200,233]:
modName = "cooldown"
elif item in [113,199]:
modName = "300 ct"
else:
modName = "100 ct"
elif stat == 36:
if item in [69,70,71,154,155,156,213]:
modName = "#blasts"
elif item in [31,108,147,148,149,207,209, 5,150,208]:
modName = "#shots"
elif item in [113,199,230,66]:
modName = "#dances"
elif item in [39,40,212]:
modName = "#bombs"
elif item == 118:
modName = "height"
elif item in [59,67,178,179,180,232,311, 191,192,257]:
modName = "+1 blockers"
elif item in [47,224]:
modName = "#plinks"
else:
modName = "num subs"
else:
modName = STATS[stat]
# operator = "+" if val > 0 else ""
# value = str(val / 100 if "crit" in modName.lower() else val)
# value = value.rstrip('0').rstrip('.') if '.' in value else value
# return STATS[stat].format(o=operator, v=value)
return modName
except KeyError:
return str(stat) + " " + str(val)
def reFieldToList(rf, itemtype=None): # TODO: cast inside of `logChest`
'''struct RepeatedField_int -> list
struct RepeatedPtrField works too if there is a single struct to cast on all elements'''
if rf.elements == ffi.NULL:
return []
lst = ffi.unpack(rf.elements, rf.current_size)
if itemtype != None:
for e in range(len(lst)):
lst[e] = ffi.cast(itemtype, lst[e])
return lst
def item_sort(itemlist):
name = itemlist[1]
if name.endswith("ites"): return 0
if name.startswith("Prototype "): return 2
if name.startswith("Experimental "): return 3
if name.startswith("Custom "): return 4
if name.startswith("Modified "): return 5
if name.endswith(" Key"): return 7
if " Boost " in name: return 9
if name == "Implant Slot 1": return 10
if name.endswith(" UC") or name.endswith(" EC"): return 11
if name.endswith(" Boost"): return 12
return 6
def getBoosts(item):
boosts = list()
i = 0
for boost in reFieldToList(item.statboosts, 'struct StatBoost *'):
boosts.append(boostToModifier(item.type, boost.stat, boost.val))
if boosts[-1] == "300 ct":
i += 1
elif boosts[-1] == "100 ct":
i -= 1
if i > 1:
boosts.remove("300 ct")
boosts.remove("300 ct")
boosts.append("250 ct")
elif i < -1:
boosts.remove("100 ct")
boosts.remove("100 ct")
boosts.append("50 ct")
return boosts
def isEC(item):
return item in [49,50,96,97]
def isStat(item):
return item in [18,19,20,21,22,23, 92,93,94,95,96,97] + [i for i in range(120, 144)] + [i for i in range(236, 244)]
class Plugin(PluginBase):
def onInit(self):
self.config.option('visible', False, 'bool')
self.config.option('show_dropped', False, 'bool')
self.config.option('show_ec', False, 'bool')
self.config.option('show_stats', False, 'bool')
self.config.option('size', 14, 'int')
self.columns = {}
for cname in COLUMNS:
self.columns[cname] = util.MultilineText(
size=self.config.size, spacing=self.config.size)
self.chests = {}
def afterUpdate(self):
cw = self.refs.ClientWorld
if cw == ffi.NULL: return
objs = util.worldobjects(cw.mySubWorld.asNativeSubWorld)
dropped_loot = []
chestNum = 0
for obj in objs:
if util.getClassName(obj) == 'Chest':
chest = []
chestNum += 1
for item in reFieldToList(ffi.cast('struct Chest *', obj).items, 'struct ItemProperties *'):
if (isEC(item.type) and not self.config.show_ec) or (isStat(item.type) and not self.config.show_stats): continue
boosts = getBoosts(item)
item_name = ITEMS[item.type]
chest.append(list(map(str, (
', '.join(boosts),
MODIFIER_PREFIXES[len(boosts)] + item_name,
))))
self.chests["Chest {}: {} items".format(chestNum, len(chest))] = chest
elif util.getClassName(obj) == "Loot" and self.config.show_dropped:
loot = ffi.cast('struct Loot *', obj)
item = loot.itemProps
if (isEC(item.type) and not self.config.show_ec) or (isStat(item.type) and not self.config.show_stats): continue
boosts = getBoosts(item)
item_name = ITEMS[item.type]
dropped_loot.append(list(map(str, (
', '.join(boosts),
MODIFIER_PREFIXES[len(boosts)] + item_name,
))))
if dropped_loot and self.config.show_dropped:
self.chests["Dropped Loot: {} items".format(len(dropped_loot))] = dropped_loot
def onPresent(self):
chestlist = dict(self.chests)
self.chests = {}
if not chestlist:
return
for key in chestlist.keys():
chestlist[key].sort(key=item_sort)
txts = [''] * len(COLUMNS)
def addList(lst, num, name):
nonlocal txts
lst = list(lst)
if len(lst) == 0:
return
txts[0] += '{}\n'.format(name)
for i in range(1, len(txts)):
txts[i] += '\n'
for x in lst:
for i in range(len(txts)):
txts[i] += x[i] + '\n'
cw = self.refs.ClientWorld
if cw != ffi.NULL and cw.asWorld.props.safe:
# wipe old records in safe zones
self.items = {}
if not self.config.visible:
return
i = 0
for key in chestlist.keys():
i += 1
addList(chestlist[key], i, key)
self.columns['modifiers'].text = txts[0]
self.columns['name'].text = txts[1]
x = self.refs.windowW // 2
y = 10
spacing = 5
self.columns['modifiers'].draw(x - 1*spacing, y, anchorX=1)
self.columns['name'].draw(x + 1*spacing, y)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment