├── .gitignore ├── README.md ├── offsets.py ├── play.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | *.pyc 3 | *.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BrawlBot 2 | A quick hack made for brawlhalla that plays Queen Nai (at the moment). It can be improved a lot more however I'm losing interest. 3 | 4 | Written for Python 2 5 | 6 | [![Alt text](https://img.youtube.com/vi/CIVxv4JUefs/0.jpg)](https://www.youtube.com/watch?v=CIVxv4JUefs) 7 | 8 | 9 | # install requirments 10 | ``` 11 | pip install https://github.com/hrt/memorpy/archive/master.zip 12 | ``` 13 | 14 | # usage 15 | First adjust `calculate_actual_input` in `play.py` to match your ingame settings. 16 | 17 | 18 | You'll probably need to update `g_input_offsets` and `local_ptr_offsets` in `offsets.py` 19 | ``` 20 | python play.py 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /offsets.py: -------------------------------------------------------------------------------- 1 | PROCESS_NAME = 'Brawlhalla.exe' 2 | # search for double 568.99 on demon map - it is your standing y 3 | # ginput is just 0 ORed with whatever button you have pressed for example if you have up pressed then it'll be 17 4 | 5 | entity_sig_1 = b"\x00\x90\x64......\xA0.......\x00" # base entity -1 6 | entity_sig_2 = b"\x00.\x64..............\x00" # a looser variant 7 | # 90 AD 83 04 10 EF 57 13 50 78 68 07 90 64 C2 62 8 | # 90 AD 57 04 40 32 2C 12 50 58 46 07 90 64 C2 62 9 | # 90 AD E0 03 10 7F 9B 12 50 B8 B3 06 90 64 C2 62 10 | # 90 AD BC 03 10 FF 84 12 50 88 97 06 90 64 C2 62 11 | # 90 AD 2F 04 10 0F F3 12 50 78 17 07 90 64 C2 62 12 | # 90 AD EE 03 10 9F 8C 12 50 88 AA 06 90 64 A3 62 13 | ginput_sig = b"\x90\xAD......\x50...\x90\x64.\x62" 14 | g_input_base_offset = 0x34 # add to the base of ginput 15 | g_input_offsets = ('Adobe AIR.dll', [0x0131550C, 0x1DC, 0x18, 0x8, 0x98, 0x1F8, 0x4CC]) 16 | local_ptr_offsets = ('Adobe AIR.dll', [0x01315528, 0xA4, 0x44C, 0x14, 0x98, 0x98, 0x548]) 17 | 18 | # below are relative to base of entity 19 | increased_gravity_offset = 0xC4 # 1 if sprinting down 20 | dodge_offset = 0x154 # 0 if can dodge 21 | in_air_offset = 0x108 # 1 if in air 22 | not_grounded_offset = 0xF4 # 1 if in air 23 | in_attack_offset = 0x88 # 1 if attacking, also affected by dodge 24 | in_animation_offset = 0xA0 # 1 if in animation 25 | in_edging_offset = 0x118 # 2 if edging 26 | x_offset = 0x378 27 | y_offset = 0x370 # up is negative 28 | stun_offset = 0x184 # positive if stunned 29 | direction_offset = 0xD4 # 1 LEFT or 0 RIGHT 30 | jump_count_offset = 0x1F0 # 0, 1, 2 (2 means no jumps left) 31 | damage_taken_offset = 0x418 32 | y_vel_offset = 0x320 # negative up 33 | x_vel_offset = 0x328 # negative left 34 | recursive_ptr_offsets = [0x268, 0x4c] 35 | weapon_ptr_offsets = [0x2BC, 0x44, 0x8] 36 | 37 | QUICK_ATTACK = 640 38 | HEAVY_ATTACK = 64 39 | UP = 17 40 | DOWN = 2 41 | LEFT = 4 42 | RIGHT = 8 43 | DODGE = 256 44 | THROW = 516 45 | SPEAR = 0x00005F77 46 | SWORD = 0x00004222 47 | KATAR = 25411 48 | MELEE = 14969 -------------------------------------------------------------------------------- /play.py: -------------------------------------------------------------------------------- 1 | import time 2 | from memorpy import MemWorker, Process 3 | from offsets import * 4 | from utils import dereference_offsets, entities_aob_scan, fetch_entity, ginput_aob_scan 5 | import keyboard 6 | 7 | def main(): 8 | mem = MemWorker(name=PROCESS_NAME) 9 | g_input_ptr = dereference_offsets(mem, g_input_offsets) 10 | g_input = mem.Address(g_input_ptr + g_input_base_offset) 11 | local_ptr = dereference_offsets(mem, local_ptr_offsets) 12 | print('g_input: %s' % hex(g_input)) 13 | print('local_ptr: %s' % hex(local_ptr)) 14 | 15 | entity_pointers = entities_aob_scan(mem) 16 | 17 | print('Removing local entity from entity list') 18 | entity_pointers.remove(local_ptr) 19 | entity_pointers = [local_ptr]+entity_pointers 20 | 21 | # g_input = mem.Address(ginput_aob_scan(mem)) 22 | 23 | def calculate_actual_input(): 24 | # based on my settings for brawlhalla 25 | val = 0 26 | val |= LEFT * keyboard.is_pressed('LEFT') 27 | val |= RIGHT * keyboard.is_pressed('RIGHT') 28 | val |= UP * keyboard.is_pressed('UP') 29 | val |= DOWN * keyboard.is_pressed('DOWN') 30 | val |= DODGE * keyboard.is_pressed('x') 31 | val |= THROW * keyboard.is_pressed('e') 32 | val |= QUICK_ATTACK * keyboard.is_pressed('q') 33 | val |= HEAVY_ATTACK * keyboard.is_pressed('w') 34 | return val 35 | 36 | def reset_input(address, hard=False, u=0): 37 | if hard: 38 | address.write(0) 39 | time.sleep(0.03) 40 | time.sleep(0.03) 41 | address.write(calculate_actual_input() | u) 42 | 43 | def fetch_entity_from_index(i): 44 | return fetch_entity(mem, entity_pointers[i]) 45 | 46 | def down_quick(local, target, u=0): 47 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 48 | g_input.write(target_direction | DOWN | QUICK_ATTACK | u) 49 | 50 | def neutral_heavy(local, target): 51 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 52 | if target_direction != local.direction: 53 | g_input.write(target_direction) 54 | reset_input(g_input) 55 | g_input.write(HEAVY_ATTACK) 56 | 57 | def side_heavy(local, target): 58 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 59 | g_input.write(HEAVY_ATTACK | target_direction) 60 | 61 | 62 | def down_heavy(local, target): 63 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 64 | g_input.write(HEAVY_ATTACK | DOWN | target_direction) 65 | 66 | def neutral_quick(local, target): 67 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 68 | if target_direction != local.direction: 69 | g_input.write(target_direction) 70 | reset_input(g_input) 71 | g_input.write(QUICK_ATTACK) 72 | 73 | def side_air_quick(local, target, u=UP): 74 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 75 | g_input.write(target_direction | u | QUICK_ATTACK) 76 | 77 | def side_quick(local, target, u=UP): 78 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 79 | g_input.write(target_direction | QUICK_ATTACK) 80 | 81 | def air_quick(local, target, u=UP): 82 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 83 | if target_direction != local.direction: 84 | g_input.write(target_direction) 85 | reset_input(g_input) 86 | g_input.write(QUICK_ATTACK) 87 | 88 | def gravity_cancel(): 89 | print('gravity cancel') 90 | g_input.write(DODGE) 91 | reset_input(g_input) 92 | 93 | print('Entering script mode') 94 | while True: 95 | targets = [fetch_entity_from_index(i) for i in range(len(entity_pointers))] 96 | # assuming first entity found is local player which might not be true? 97 | local = targets.pop(0) 98 | if local.in_animation or local.in_stun: 99 | continue 100 | current_input = g_input.read() 101 | for target in targets: 102 | dx = target.x-local.x 103 | dy = target.y-local.y 104 | if target.in_attack and abs(dx+target.x_vel-local.x_vel) < 300 and abs(dy+target.y_vel-local.y_vel) < 300 and local.can_dodge: 105 | print('phase dodge') 106 | if local.not_grounded: 107 | g_input.write(DODGE | current_input) 108 | else: 109 | g_input.write(DODGE) 110 | time.sleep(0.05) 111 | reset_input(g_input) 112 | break 113 | 114 | if local.weapon == MELEE: 115 | # jump if above near 116 | if (150 < dy < 200) and (0 < abs(dx) < 250) and local.jump_count == 0 and target.in_stun: 117 | print('jump if above near') 118 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 119 | g_input.write(UP | target_direction) 120 | reset_input(g_input) 121 | break 122 | 123 | # neutral quick 124 | if (abs(dy) < 150) and (0 < abs(dx) < 150) and not local.not_grounded: 125 | print('neutral quick') 126 | neutral_quick(local, target) 127 | reset_input(g_input) 128 | break 129 | 130 | # side quick 131 | if (abs(dy) < 150) and (50 < abs(dx) < 350) and not local.not_grounded and target.in_stun: 132 | print('side quick') 133 | side_quick(local, target) 134 | reset_input(g_input) 135 | break 136 | 137 | # down quick 138 | if (abs(dy) < 150) and (100 < abs(dx) < 350) and not local.not_grounded: 139 | print('down quick') 140 | down_quick(local, target) 141 | reset_input(g_input) 142 | break 143 | 144 | # quick 145 | if (abs(dy) < 50) and (10 < abs(dx) < 250) and local.not_grounded: 146 | print('air quick') 147 | air_quick(local, target) 148 | reset_input(g_input) 149 | 150 | if -400 < dy < -100 and (abs(dy+abs(dx)) < 100) and local.not_grounded and local.jump_count == 0: 151 | print('air down quick') 152 | down_quick(local, target) 153 | reset_input(g_input) 154 | break 155 | 156 | 157 | if local.weapon == SPEAR: 158 | if 50 < dy < 350 and 400 > abs(dx) > 100: 159 | if not local.not_grounded: 160 | print('down quick') 161 | down_quick(local, target) 162 | reset_input(g_input) 163 | break 164 | 165 | 166 | if 100 < dy < 450 and 400 > abs(dx) > 50: 167 | if not local.not_grounded:# and target.damage_taken > 180: 168 | print('finish') 169 | neutral_heavy(local, target) 170 | reset_input(g_input) 171 | break 172 | elif local.not_grounded and local.can_dodge and target.in_stun: 173 | print('finish') 174 | gravity_cancel() 175 | neutral_heavy(local, target) 176 | reset_input(g_input) 177 | break 178 | 179 | if -150 < dy < 0 and 200 < abs(dx) and abs(dx) < 550: 180 | if not local.not_grounded: 181 | down_heavy(local, target) 182 | reset_input(g_input) 183 | break 184 | 185 | if local.not_grounded and local.can_dodge and local.jump_count == 0: 186 | gravity_cancel() 187 | down_heavy(local, target) 188 | reset_input(g_input) 189 | break 190 | 191 | # spear air side quick 192 | if 200 < dy < 400 and 400 > abs(dx) > 100 and local.not_grounded and local.jump_count == 0: 193 | print('air side quick') 194 | # can be better with jump count (can jump) 195 | side_air_quick(local, target) 196 | reset_input(g_input) 197 | break 198 | 199 | # spear air quick 200 | if abs(dy) < 300 and 300 > abs(dx) and local.not_grounded: 201 | print('air quick') 202 | # can be better with jump count (can jump) 203 | air_quick(local, target) 204 | reset_input(g_input) 205 | break 206 | 207 | # # quick 208 | # if (abs(dy) < 150) and (100 < abs(dx) < 600) and not local.not_grounded: 209 | # print('side quick') 210 | # side_quick(local, target) 211 | # reset_input(g_input) 212 | # break 213 | 214 | if local.weapon == KATAR: 215 | # if 50 < dy < 350 and 300 > abs(dx) > 100: 216 | if 400 > dy > 50 and (50 < abs(dx) < 400) and target.in_stun: 217 | if not local.not_grounded: 218 | print('down quick') 219 | down_quick(local, target) 220 | reset_input(g_input) 221 | break 222 | 223 | if -400 < dy < -100 and (abs(dy+abs(dx)) < 100) and local.not_grounded and local.jump_count == 0: 224 | print('air down quick') 225 | down_quick(local, target) 226 | reset_input(g_input) 227 | break 228 | 229 | if 100 < dy < 600 and (abs(abs(dx)-dy) < 100) and local.not_grounded and local.jump_count == 0: 230 | print('air side heavy') 231 | side_heavy(local, target) 232 | reset_input(g_input) 233 | break 234 | 235 | if (abs(dy) < 50) and (abs(dx) < 300 and target.in_stun) or (abs(dx) < 100) and not local.not_grounded: 236 | print('nquick') 237 | neutral_quick(local, target) 238 | reset_input(g_input) 239 | break 240 | 241 | if (abs(dy) < 100) and (70 < abs(dx) < 400) and not local.not_grounded: 242 | print('side quick') 243 | side_quick(local, target) 244 | reset_input(g_input) 245 | break 246 | 247 | if abs(dy) < 300 and 300 > abs(dx) and local.not_grounded: 248 | print('air quick') 249 | air_quick(local, target) 250 | reset_input(g_input) 251 | break 252 | 253 | if 200 < dy < 400 and 300 > abs(dx) > 100 and local.not_grounded and local.jump_count == 0: 254 | print('air side quick') 255 | # can be better with jump count (can jump) 256 | side_air_quick(local, target) 257 | reset_input(g_input) 258 | break 259 | 260 | 261 | if local.weapon == SWORD: 262 | if 300 < (dy+target.y_vel-local.y_vel) < 450 and 350 > abs(dx+target.x_vel-local.x_vel) > 200 and target.damage_taken > 180: 263 | print('sword finish') 264 | if local.not_grounded and local.can_dodge: 265 | gravity_cancel() 266 | neutral_heavy(local, target) 267 | break 268 | elif not local.not_grounded: 269 | neutral_heavy(local, target) 270 | break 271 | 272 | if 100 < dy < 350 and 350 > abs(dx) > 100 and target.in_stun and target.damage_taken > 180: 273 | print('sword stun finish') 274 | if local.not_grounded and local.can_dodge: 275 | gravity_cancel() 276 | neutral_heavy(local, target) 277 | reset_input(g_input) 278 | break 279 | elif not local.not_grounded: 280 | neutral_heavy(local, target) 281 | reset_input(g_input) 282 | break 283 | 284 | # jump if above near 285 | if (100 < dy < 200) and (0 < abs(dx) < 200) and local.jump_count == 0 and target.in_stun: 286 | print('jump if above near') 287 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 288 | g_input.write(UP | target_direction) 289 | reset_input(g_input) 290 | break 291 | 292 | # air down quick 293 | if (-250 < dy < -50) and (0 < abs(dx) < 150) and local.not_grounded and local.jump_count == 0: 294 | print('air down quick') 295 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 296 | down_quick(local, target) 297 | reset_input(g_input) 298 | break 299 | 300 | # air quick 301 | if (100 < dy < 350) and (0 < abs(dx) < 200) and local.not_grounded: 302 | print('air quick') 303 | air_quick(local, target) 304 | reset_input(g_input) 305 | break 306 | 307 | # air side quick 308 | if (abs(dy) < 100) and (70 < abs(dx) < 350) and local.not_grounded and local.jump_count == 0: 309 | print('air side quick') 310 | target_direction = LEFT if local.x-target.x > 0 else RIGHT 311 | side_air_quick(local, target, u=0) 312 | reset_input(g_input) 313 | break 314 | 315 | # down quick 316 | if (abs(dy) < 150) and (100 < abs(dx) < 350) and not local.not_grounded: 317 | print('down quick') 318 | down_quick(local, target) 319 | reset_input(g_input) 320 | break 321 | 322 | 323 | if (target.in_animation and not target.in_stun) and abs(dx+target.x_vel-local.x_vel) < 400 and abs(dy+target.y_vel-local.y_vel) < 400 and not local.not_grounded: 324 | print('jump dodge') 325 | if current_input & UP: 326 | reset_input(g_input, u=UP) 327 | else: 328 | g_input.write(UP) 329 | reset_input(g_input) 330 | break 331 | 332 | # if dy > 100 and not local.not_grounded and abs(dx) < 250: 333 | # print('jump up dude') 334 | # if current_input & UP: 335 | # reset_input(g_input, u=UP) 336 | # else: 337 | # g_input.write(UP) 338 | # reset_input(g_input) 339 | # break 340 | 341 | main() -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from offsets import * 2 | from memorpy.WinStructures import PAGE_READWRITE 3 | import re 4 | 5 | from collections import namedtuple 6 | Entity = namedtuple('Entity', 'x y can_dodge not_grounded in_animation in_stun direction weapon jump_count, damage_taken x_vel y_vel in_attack increased_gravity in_edging') 7 | 8 | def is_base_of_entity(mem, address): 9 | # verify entity by checking for recursive pointer 10 | try: 11 | ptr = address 12 | for offset in recursive_ptr_offsets: 13 | ptr = mem.Address(ptr + offset).read() 14 | if ptr != address: 15 | return False 16 | except: 17 | return False 18 | else: 19 | return True 20 | 21 | def aob_scan(mem, entity_pointers, start, size, pattern=None, offset=0, entity_check=False): 22 | all_the_bytes = mem.process.read(mem.Address(start), type='bytes', maxlen=size) 23 | matches = re.finditer(pattern, all_the_bytes) 24 | for match in matches: 25 | span = match.span() 26 | if span: 27 | address = start + span[0] + offset 28 | if not entity_check: 29 | entity_pointers.append(address) 30 | elif address not in entity_pointers and is_base_of_entity(mem, address): 31 | entity_pointers.append(address) 32 | 33 | def dereference_offsets(mem, name_and_offsets): 34 | modules = mem.process.list_modules() 35 | name, offsets = name_and_offsets 36 | ptr = modules[name] 37 | for offset in offsets: 38 | ptr = mem.process.read(mem.Address(ptr + offset)) 39 | return ptr 40 | 41 | def entities_aob_scan(mem): 42 | print('Scanning memory for entities') 43 | modules = mem.process.list_modules() 44 | regions = mem.process.iter_region(start_offset=modules[PROCESS_NAME], protec=PAGE_READWRITE) 45 | entity_pointers = [] 46 | print("Performing deep scan") 47 | for start, size in regions: 48 | if len(entity_pointers) >= 4: 49 | break 50 | aob_scan(mem, entity_pointers, start, size, pattern=entity_sig_2, offset=1, entity_check=True) 51 | 52 | print('Found %d entities : %s' % (len(entity_pointers), ', '.join([hex(e) for e in entity_pointers]))) 53 | return entity_pointers 54 | 55 | def ginput_aob_scan(mem): 56 | print('Scanning memory for ginput') 57 | modules = mem.process.list_modules() 58 | regions = mem.process.iter_region(start_offset=modules[PROCESS_NAME], protec=PAGE_READWRITE) 59 | ginput_pointers = [] 60 | print("Performing deep scan") 61 | for start, size in regions: 62 | if len(ginput_pointers) >= 1: 63 | break 64 | aob_scan(mem, ginput_pointers, start, size, pattern=ginput_sig, offset=-16) 65 | 66 | print('Found %d ginput : %s' % (len(ginput_pointers), ', '.join([hex(e) for e in ginput_pointers]))) 67 | assert len(ginput_pointers) == 1, "invalid number of ginput pointers found, find a better sig" 68 | ginput_pointer = ginput_pointers[0] 69 | print('g_input: %s' % hex(ginput_pointer)) 70 | return ginput_pointers[0] 71 | 72 | def fetch_entity(mem, address): 73 | x = mem.Address(address + x_offset).read(type='double') 74 | y =-mem.Address(address + y_offset).read(type='double') # invert it for brain convenience 75 | 76 | increased_gravity = mem.Address(address + increased_gravity_offset).read() 77 | can_dodge = 0 if mem.Address(address + dodge_offset).read() else 1 78 | # not_grounded = mem.Address(address + not_grounded_offset).read() 79 | in_edging = mem.Address(address + in_edging_offset).read() 80 | not_grounded = mem.Address(address + in_air_offset).read() 81 | in_animation = mem.Address(address + in_animation_offset).read() 82 | in_attack = mem.Address(address + in_attack_offset).read() 83 | in_stun = mem.Address(address + stun_offset).read() 84 | direction = LEFT if mem.Address(address + direction_offset).read() else RIGHT 85 | weapon_ptr = address 86 | for offset in weapon_ptr_offsets: 87 | weapon_ptr = mem.Address(weapon_ptr + offset).read() 88 | jump_count = mem.Address(address + jump_count_offset).read() 89 | damage_taken = mem.Address(address + damage_taken_offset).read(type='double') 90 | y_vel = mem.Address(address + y_vel_offset).read(type='double') 91 | x_vel = mem.Address(address + x_vel_offset).read(type='double') 92 | return Entity(x+(2*x_vel), y-(2*y_vel), can_dodge, not_grounded, in_animation, in_stun, direction, weapon_ptr, jump_count, damage_taken, x_vel, y_vel, in_attack, increased_gravity, in_edging) 93 | --------------------------------------------------------------------------------