def firstChild(obj): if obj == ffi.NULL: return ffi.NULL cv = ffi.cast('struct UIElementContainer *', obj).children if cv.finish == cv.start or cv.endOfStorage <= cv.start: return ffi.NULL return ffi.cast('struct UIElement **', cv.start)[0]
def kickstart(): global refs, util import os SYMFILE = os.environ['SBPE_SYMFILE'] with open(SYMFILE, 'rb') as f: offsets = json.loads(f.read().decode('utf-8')) refs.SCRIPTPATH = os.path.dirname(SYMFILE) sys.path.insert(1, refs.SCRIPTPATH) sys.path.insert(1, os.path.join(refs.SCRIPTPATH, 'pypy', 'site-packages')) logging.basicConfig( level=logging.INFO, filename=os.path.join(refs.SCRIPTPATH, LOGFILE), filemode='w', format='%(asctime)s %(module)s [%(levelname)s] %(message)s') logging.info('SBPE ' + refs.VERSION) logging.info('platform: ' + refs.SYSINFO) refs.CONFIGFILE = os.path.join(refs.SCRIPTPATH, CONFIGFILE) refs.config.read(refs.CONFIGFILE) # import native functions/objects for sname in offsets: offset = offsets[sname] if sname in SYMTYPES: # objects, variables refs[sname] = ffi.cast(SYMTYPES[sname], offset) elif sname.startswith('flags::'): # flags refs[sname[7:]] = ffi.cast('uint8_t *', offset) else: # functions refs[sname] = ffi.cast('p' + sname, offset) # import object inheritance info with open(os.path.join(refs.SCRIPTPATH, 'cdefs/proto.json'), 'r') as f: d = json.load(f) refs.CHAINS = d['chains'] refs.CASTABLE = d['castable'] # import util util = importlib.import_module('util') util.refs = refs # import plugins plugpath = os.path.join(refs.SCRIPTPATH, 'plugins') man = importlib.import_module('manager') refs.manager = man.Manager(path=plugpath, refs=refs) # init hooks initHooks() logging.info('startup ok') return 0
def getUITree(obj, depth=0): '''printable UI element tree starting from obj''' if obj == ffi.NULL: return ' ' * depth + 'NULL' cname = getClassName(obj) uiel = ffi.cast('struct UIElement*', obj) res = ' ' * depth + '{0} ({1.x}, {1.y}) {1.w}x{1.h}'.format(cname, uiel) if cname in refs.CASTABLE['UIElementContainer']: uiec = ffi.cast('struct UIElementContainer*', obj) for elem in vec2list(uiec.children): res += '\n' + getUITree(elem, depth + 2) return res
def getClassName(obj): ''' class name of a C++ object (assuming gcc memory layout). doesn't demangle complicated names ''' if obj == ffi.NULL: return 'NULL' # class pointer is always at [0] classptr = ffi.cast('void****', obj)[0] # vtable (1 up) -> type -> name (1 down) nameptr = classptr[-1][1] cname = ffi.string(ffi.cast('char*', nameptr), 100) return cname[1 if len(cname) < 11 else 2:].decode()
def vec2list(vector, itemtype='void*'): '''std::vector -> list''' if vector.start == ffi.NULL or vector.finish == ffi.NULL or\ vector.endOfStorage <= vector.start: return [] n = (vector.finish - vector.start) // ffi.sizeof(itemtype) return ffi.unpack(ffi.cast(itemtype + '*', vector.start), n)
def afterUpdate(self): self.draw = False wc = self.refs.WorldClient cw = self.refs.ClientWorld if wc == ffi.NULL or cw == ffi.NULL or cw.player == ffi.NULL: return if wc.hud != ffi.NULL and wc.hud.hudStatus != ffi.NULL: ffi.cast('struct UIElement *', wc.hud.hudStatus).show = False ptype = util.getClassName(cw.player) if ptype not in self.refs.CASTABLE['PlayerCharacter']: return wobj = ffi.cast('struct WorldObject *', cw.player) pc = ffi.cast('struct PlayerCharacter *', cw.player) self.txt_hp.text = '{}'.format(wobj.props.hitpoints) self.txt_hpmax.text = '/{}'.format(wobj.props.maxhitpoints) self.txt_ammo.text = '{}'.format(pc.ammo) maxammo = pc.maxAmmo.base + pc.maxAmmo.bonus if pc.ammoMult > 1: self.txt_ammomax.text = '/{} (x{})'.format(maxammo, pc.ammoMult) else: self.txt_ammomax.text = '/{}'.format(maxammo) self.txt_currency.text = '{} EC {} UC'.format(pc.ec, pc.uc) for name in ELEMS: el = getattr(self, 'txt_' + name) el.size = getattr(self.config, 'size_' + name) el.outlineSize = self.config.outline if wobj.props.hitpoints == wobj.props.maxhitpoints: hpcolor = self.config.color_hp_full else: hpcolor = self.config.color_hp self.txt_hp.color = self.txt_hpmax.color = hpcolor self.txt_ammo.color = self.txt_ammomax.color = self.config.color_ammo self.txt_currency.color = self.config.color_currency self.draw = True
def hook_LoadTextureFile(name, callback, userData): refs._tfname = ffi.string(name).decode() hook = lib.subhook_new(callback, lib.hook_textureCallback, 0) refs._orig_callback = ffi.cast('XDL_LoadTextureDoneCallback', lib.subhook_get_trampoline(hook)) lib.subhook_install(hook) ORIGS['XDL_LoadTextureFile'](name, callback, userData) lib.subhook_remove(hook) lib.subhook_free(hook)
def updateState(): '''make commonly used data more accessible to plugins''' if GLFUNCTIONS[0] not in refs: loadGLFunctions() if refs.stage[0] == ffi.NULL: return # top level children of stage tops = [] types = [] for t in vec2list(refs.stage[0].asUIElementContainer.children): clname = getClassName(t) types.append(clname) if clname in STRUCTTYPES: t = ffi.cast('struct {} *'.format(clname), t) tops.append(t) refs.tops = tops refs.topTypes = types # main menu if types[0] == 'MainMenu': refs.MainMenu = tops[0] else: refs.MainMenu = ffi.NULL # game client refs.GameClient = refs.WorldClient = refs.ClientWorld =\ refs.WorldView = ffi.NULL if types[0] == 'GameClient': refs.GameClient = tops[0] refs.WorldClient = refs.GameClient.worldClient if refs.WorldClient != ffi.NULL: refs.WorldView = refs.WorldClient.worldView refs.ClientWorld = refs.WorldClient.clientWorld if refs.ClientWorld == ffi.NULL or refs.WorldView == ffi.NULL: refs.ClientWorld = refs.WorldView = ffi.NULL # window size and scale ww_ = ffi.new('int *') wh_ = ffi.new('int *') lib.SDL_GetWindowSize(refs.window_[0], ww_, wh_) ww = ww_[0] or 1 # avoid potential division by zero wh = wh_[0] or 1 refs.windowW = ww refs.windowH = wh refs.scaleX = refs.canvasW_[0] / ww refs.scaleY = refs.canvasH_[0] / wh
def addhook(fname, hookfunc, ret=False): hook = lib.subhook_new(refs[fname], hookfunc, 1) orig = ffi.cast('p' + fname, lib.subhook_get_trampoline(hook)) if orig != ffi.NULL: ORIGS[fname] = orig else: logging.info('{}: no trampoline, using fallback'.format(fname)) def call_orig(*args): lib.subhook_remove(hook) res = refs[fname](*args) lib.subhook_install(hook) if ret: return res ORIGS[fname] = call_orig lib.subhook_install(hook) if not lib.subhook_is_installed(hook): logging.error('failed to hook {}'.format(fname))
def afterUpdate(self): gc = self.refs.GameClient if gc != ffi.NULL and gc.chatWindow != ffi.NULL: if self.config.hide_chat: ffi.cast('struct UIElement *', gc.chatWindow).x = -9000 else: ffi.cast('struct UIElement *', gc.chatWindow).x = 0 cw = self.refs.ClientWorld wv = self.refs.WorldView if cw == ffi.NULL or wv == ffi.NULL: return # other players allies = util.vec2list(cw.allies, 'struct WorldObject *') for obj in allies: props = obj.props if self.config.hide_shells: # fail: triggers game's "unknown object" box drawing ffi.cast('int *', props.vid.s)[-3] = 0 if self.config.hide_names: # remember and remove relevant bits; also hides factions bits = props._has_bits[0] & 0x300000 props._has_bits[0] ^= bits self.oldbits.append((obj, bits)) elif self.config.hide_factions: # hide only faction names by zeroing faction name length if props.playerdata != ffi.NULL: fname = props.playerdata.factionname ffi.cast('int *', fname.s)[-3] = 0 if self.config.hide_healthbars: # not sure if this breaks anything props.hitpoints = props.maxhitpoints if self.config.hide_ally_objects: worlds = util.sVecMap2list(cw.clientSubWorlds, 'struct ForeignSubWorld *') for csubworld in worlds: for obj in util.worldobjects(csubworld): if obj in allies: # shells are treated separately continue if obj.props.terraintype > 0: # anything that acts like terrain (macrons) continue if util.getstr(obj.props.vid)[:5] == 'timer': # macron timer continue self.hide(obj) # reduce damage flash time rt = round((1 - self.config.damage_flash_intensity) * 132) rt = max(0, min(132, rt)) if rt > 0: # server world time swt = ffi.cast('struct SubWorld *', cw.serverSubWorld).t # skip the beginning since lastDamageT is 0 by default if swt > 132: for obj in util.worldobjects(cw.serverSubWorld): dt = swt - obj.lastDamageT if dt < rt: obj.lastDamageT = swt - rt for graphic in util.vec2list(wv.dynamicGraphics): gtype = util.getClassName(graphic) if self.config.hide_effects and gtype == 'AnimationGraphic': g = ffi.cast('struct AnimationGraphic *', graphic) g.startTime = 0 if self.config.replace_shake: ct = time.perf_counter() if wv.shakePos < wv.shakeDuration and wv.shakeMagnitude > 0: self._shake = max(self._shake, ct + wv.shakeDuration / 1000) wv.shakeMagnitude = 0 flashduration = wv.flashStart + wv.flashHold + wv.flashEnd if wv.flashPos < flashduration and wv.flashColor != 0: self._flash = max(self._flash, ct + flashduration / 1000) wv.flashColor = 0
def afterUpdate(self): cw = self.refs.canvasW_ ch = self.refs.canvasH_ cscale = cw[0] / self.refs.windowW ctime = time.perf_counter() if not self.config.active: self.start = self.target = 1 nscale = 1 else: twidth = self.config.level targ = twidth / self.refs.windowW if targ <= 0: targ = 1 if self.target != targ: self.stime = ctime self.start = cscale self.target = targ if self.config.time > 0: nscale = ease(self.start, self.target, self.stime, ctime, self.config.time) else: nscale = self.target if nscale <= 0: nscale = 0.1 if nscale == cscale: return tw = round(nscale * self.refs.windowW) th = round(nscale * self.refs.windowH) cw[0] = self.refs.overrideW = tw ch[0] = self.refs.overrideH = th installed = lib.subhook_is_installed(self._hook) != 0 if self.config.fast and not installed: lib.subhook_install(self._hook) if not self.config.fast and installed: lib.subhook_remove(self._hook) # replacement handling of window resize event for WorldClient and # everything inside of it that doesn't have handlers wc = self.refs.WorldClient if self.config.fast and wc != ffi.NULL: setUIElementSize(wc, tw, th) setUIElementSize(wc.worldView, tw, th) setUIElementSize(wc.hud, tw, th) setUIElementSize(wc.overlay, tw, th) # equip if wc.hud != ffi.NULL and wc.hud.hudEquip != ffi.NULL: equip = toUIElement(wc.hud.hudEquip) equip.x = tw - equip.w - 28 # overlays overlay = wc.overlay if overlay != ffi.NULL: otype = util.getClassName(overlay) overlay = ffi.cast('struct {} *'.format(otype), overlay) if otype == 'InventoryOverlay': inv = toUIElement(overlay.inventoryWindow) inv.x = tw - inv.w - 24 exy = inv.y + inv.h + 24 stash = toUIElement(overlay.stashWindow) if stash != ffi.NULL: stash.x = tw - stash.w - 24 stash.y = exy + 6 exy += stash.h + 6 ex = toUIElement(overlay.playerWindowExitSprite) ex.x = tw - ex.w ex.y = exy tt = toUIElement(overlay.toolTip) if tt != ffi.NULL: tt.x = inv.x - tt.w ctt = toUIElement(overlay.comparisonToolTip) if ctt != ffi.NULL: ctt.x = tt.x - ctt.w if otype == 'ProgressOverlay': pw = toUIElement(overlay.progressWindow) pw.x = (tw - pw.w) // 2 pw.y = (th - pw.h) // 2 ex = toUIElement(overlay.playerWindowExitSprite) ex.x = tw - ex.w ex.y = pw.y + pw.h - ex.h if otype == 'ScoreOverlay': off = int(tw * 0.4) scw = toUIElement(overlay.scoreCharWindow) scw.x = (off - scw.w) // 2 ssw = toUIElement(overlay.scoreStatsWindow) ssw.x = off sbw = toUIElement(overlay.scoreBonusWindow) sbw.x = off + ssw.w + 30 fss = toUIElement(overlay.finalScoreSprite) fss.x = off fss.y = max(sbw.h + sbw.y, ssw.h + ssw.y) + 64 xpb = toUIElement(overlay.xpLevelBars) if xpb != ffi.NULL: xpb.x = off xpb.y = fss.h + fss.y + 24 ex = toUIElement(overlay.playerWindowExitSprite) ex.x = tw - ex.w ex.y = th - ex.h - 100 if overlay.scoreRankAdded: # the rank visual element is the last child oc = ffi.cast('struct UIElementContainer *', overlay) chl = util.vec2list(oc.children, 'struct UIElement *') if len(chl) > 0: sr = chl[-1] sr.x = scw.x + scw.w - 20 if otype == 'ZoneScoreOverlay': outro = toUIElement(overlay.outro) outro.x = int(tw * 0.3) outro.y = int(th * 0.5) hoff = int(tw * 0.3) voff = int(th * 0.7) kvl = util.vec2list(overlay.keyValLabels, 'struct LabelPair') for pair in kvl: first = toUIElement(pair.first) second = toUIElement(pair.second) first.x = hoff - 96 second.x = hoff + 96 - second.w first.y = second.y = voff voff += 20 if len(kvl) > 0: voff += 20 xpb = toUIElement(overlay.xpLevelBars) xpb.x = hoff - 140 xpb.y = voff + 20 ex = toUIElement(overlay.playerWindowExitSprite) ex.x = tw - ex.w ex.y = th - ex.h - 100 # re-center camera if self.refs.WorldView != ffi.NULL: self.refs.WorldView.offsetsInitialized = False self.refs.windowEventCallback(lib.XDL_WINDOWEVENT_SIZE_CHANGED, self.refs.userData_[0]) self.refs.overrideW = 0 self.refs.overrideH = 0 # hack to avoid 1 frame of wrong text scaling self.refs.scaleX = self.refs.scaleY = nscale
def toUIElement(obj): return ffi.cast('struct UIElement *', obj)
def sVecMap2list(svecmap, itemtype='void*'): '''struct SortedVecMap -> list''' lst = vec2list(svecmap.vec, 'struct SortedVecElement') for i in range(len(lst)): lst[i] = ffi.cast(itemtype, lst[i].obj) return lst
def loadGLFunctions(): for name in GLFUNCTIONS: refs[name] = ffi.cast('p' + name, lib.SDL_GL_GetProcAddress(bytes(name, 'utf-8')))
def onPresent(self): if not self.draw: return x = self.config.x y = self.config.y # hp self.txt_hp.draw(x, y, anchorX=1, anchorY=0.5) self.txt_hpmax.draw(x - 4, y, anchorY=0.5) # ammo y = y + self.txt_hp.h + self.config.spacing self.txt_ammo.draw(x, y, anchorX=1, anchorY=0.5) self.txt_ammomax.draw(x - 4, y, anchorY=0.5) # currency y = y + self.txt_ammo.h + self.config.spacing self.txt_currency.draw(x, y, anchorX=0.5) # hp bar wv = self.refs.WorldView cw = self.refs.ClientWorld player = ffi.cast('struct WorldObject *', cw.player) props = player.props hp = props.hitpoints maxhp = props.maxhitpoints width = self.config.bar_width if width < 0: bw = maxhp else: bw = width bh = self.config.bar_height if bw <= 0 or bh <= 0: return x = props.xmp // 256 + props.wmp // 512 - wv.offset.x y = props.ymp // 256 - wv.offset.y # window space coords x = round(x / self.refs.scaleX) y = round(y / self.refs.scaleY) bx = x - bw // 2 by = y - bh - self.config.bar_y cw = self.refs.canvasW_[0] ch = self.refs.canvasH_[0] self.refs.canvasW_[0] = self.refs.windowW self.refs.canvasH_[0] = self.refs.windowH # outline self.refs.XDL_FillRect(bx - 1, by - 1, bw + 2, bh + 2, self.config.bar_outline, lib.BLENDMODE_BLEND) # background self.refs.XDL_FillRect(bx, by, bw, bh, self.config.bar_background, lib.BLENDMODE_BLEND) # bar filled = math.ceil(hp * bw / maxhp) self.refs.XDL_FillRect(bx, by, filled, bh, self.config.bar_color, lib.BLENDMODE_BLEND) # notches st = 1 while True: cx = bx + round(bw * (maxhp - st * 25) / maxhp) if cx <= bx + filled: break self.refs.XDL_FillRect(cx, by, 1, bh, self.config.bar_notches, lib.BLENDMODE_BLEND) st += 1 # restore coords self.refs.canvasW_[0] = cw self.refs.canvasH_[0] = ch
def onPresent(self): if not self._initedopts: self._initedopts = True for k in TARGETS.keys(): for opt in OPTS: self.config.option( 'arrow_{}_{}'.format(k, opt[0]), self.config['arrows_' + opt[0]], opt[2]) cw = self.refs.ClientWorld wv = self.refs.WorldView if cw == ffi.NULL or wv == ffi.NULL: return plr = cw.player if plr == ffi.NULL: return if util.getClassName(plr) not in self.refs.CASTABLE['PlayerCharacter']: return plr = ffi.cast('struct PlayerCharacter *', plr) objects = util.worldobjects(cw.serverSubWorld) objects += util.worldobjects(cw.mySubWorld.asNativeSubWorld) objects += util.vec2list(cw.allies, 'struct WorldObject *') kinds = self.config.arrows.split() for obj in objects: p = obj.props vid = util.getstr(p.vid) # invisible if len(vid) == 0: # triggers if p.trigger != 0: self.drawFrame( obj, self.config.trigger_color, self.config.trigger_frame) # otherwise skip continue w2 = p.wmp // 512 h2 = p.hmp // 512 x = p.xmp // 256 + w2 - wv.offset.x y = p.ymp // 256 + h2 - wv.offset.y inbounds = x + w2 > 0 and y + h2 > 0 and\ x - w2 < self.refs.canvasW_[0] and\ y - h2 < self.refs.canvasH_[0] # object hp/armor if inbounds and self.config.show_hp: if p.hitpoints >= 0: self.numbers.draw( p.hitpoints, x, y, anchorX=0.5, anchorY=1) elif p.hitpoints != -1: self.negnumbers.draw( abs(p.hitpoints), x, y, anchorX=0.5, anchorY=1) if p.armor > 0: self.numbers.draw(p.armor, x, y, anchorX=0.5, anchorY=0) # use counts if inbounds and self.config.show_uses and p.interact != 0: idesc = p.interactdescription if idesc != ffi.NULL and idesc.numused > 0 and\ idesc.totaluses > 0: self.numbers.draw( idesc.numused, x, y, anchorX=0.5, anchorY=0.5) # boosts if 'boost' in kinds and vid[:-1] in BOOSTS: btype = BOOSTS[vid[:-1]] blevel = int(vid[-1]) # current value: StatVal from PlayerCharacter currvalue = getattr(plr, btype[0]).base # max values list: RepeatedField_int from CharacterDescription mvals = getattr(plr.charDesc, btype[1]) mvals = ffi.unpack(mvals.elements, mvals.current_size) for i in range(1, len(mvals)): if mvals[i] > currvalue and i <= blevel: self.drawArrow(plr, obj, 'boost') break continue # match vid name with target list try: k = TVIDMAP[vid] except KeyError: continue # consider only enabled types if k not in kinds: continue # do we need hp? if k == 'hp': plrprops = ffi.cast('struct WorldObject *', plr).props if plrprops.hitpoints == plrprops.maxhitpoints: continue # all checks passed, we are interested in this obj self.drawArrow(plr, obj, k) # zone/room id if self.config.show_room_id: cwprops = cw.asWorld.props txt = util.getstr(cwprops.zone) or util.getstr(cwprops.music) if cwprops.floor > 0: txt = '{} {}'.format(txt, cwprops.floor + 1) self.roomtxt.text = txt self.roomtxt.draw( self.refs.windowW - 4, self.refs.windowH - 4, anchorX=1, anchorY=1)
def drawArrow(self, src, dst, kind): optprefix = 'arrow_' + kind + '_' offset = self.refs.WorldView.offset cw = self.refs.canvasW_[0] ch = self.refs.canvasH_[0] sp = ffi.cast('struct WorldObject *', src).props dp = ffi.cast('struct WorldObject *', dst).props dw2 = dp.wmp // 512 dh2 = dp.hmp // 512 x1 = (dp.xmp) // 256 + dw2 - offset.x y1 = (dp.ymp) // 256 + dh2 - offset.y inbounds = (x1 + dw2 >= 0 and x1 - dw2 <= cw and y1 + dh2 >= 0 and y1 - dh2 <= ch) if inbounds and self.config[optprefix + 'frame'] == 0: return if inbounds: x0 = (sp.xmp + sp.wmp // 2) // 256 - offset.x y0 = (sp.ymp + sp.hmp // 2) // 256 - offset.y else: x0 = cw // 2 y0 = ch // 2 angle = math.atan2(y1 - y0, x1 - x0) # offset target position to the canvas edges if out of bounds t = 1 if x1 < 0 and x0 != x1: t = min(t, x0 / (x0 - x1)) if y1 < 0 and y0 != y1: t = min(t, y0 / (y0 - y1)) if x1 > cw and x0 != x1: t = min(t, (cw - x0) / (x1 - x0)) if y1 > ch and y0 != y1: t = min(t, (ch - y0) / (y1 - y0)) # arrow point coords ax = x0 + t * (x1 - x0) ay = y0 + t * (y1 - y0) # fading dist = math.sqrt((ax - x0) ** 2 + (ay - y0) ** 2) fade = self.config[optprefix + 'fade'] color = self.config[optprefix + 'color'] f = 1 if dist < fade and fade > 0: f = dist / fade # blinking bp = self.config[optprefix + 'blink'] if bp > 0: f = f * abs(time.perf_counter() % bp - bp / 2) * 2 / bp if f < 1: ca = math.floor((color >> 24) * f) & 0xff color = (color & 0xffffff) | (ca << 24) # draw frame if inbounds: self.drawFrame(dst, color, self.config[optprefix + 'frame']) return # draw triangle dx = self.config[optprefix + 'length'] * self.refs.scaleX dy = self.config[optprefix + 'width'] / 2 * self.refs.scaleX # outline out = self.config[optprefix + 'outline'] * self.refs.scaleX if out > 0: offx = out / math.tan(math.pi / 4 - math.atan2(dx, dy) / 2) offy = out / math.tan(math.pi / 4 - math.atan2(dy, dx) / 2) (oax, oay) = rotate(offx, 0, angle) (obx, oby) = rotate(-dx - out, dy + offy, angle) (ocx, ocy) = rotate(-dx - out, -dy - offy, angle) self.refs.XDL_FillTri( round(ax + oax), round(ay + oay), round(ax + obx), round(ay + oby), round(ax + ocx), round(ay + ocy), color & 0xff000000, lib.BLENDMODE_BLEND) # middle (bx, by) = rotate(-dx, dy, angle) (cx, cy) = rotate(-dx, -dy, angle) self.refs.XDL_FillTri( round(ax), round(ay), round(ax + bx), round(ay + by), round(ax + cx), round(ay + cy), color, lib.BLENDMODE_BLEND)