diff --git a/worlds/animal_well/bean_patcher.py b/worlds/animal_well/bean_patcher.py index 663fcbef5025..15466c27d41a 100644 --- a/worlds/animal_well/bean_patcher.py +++ b/worlds/animal_well/bean_patcher.py @@ -220,6 +220,7 @@ def update_player_state(self, player, state, save): get_an_item: int = 0x1400C15C0 warp: int = 0x140074DD0 update_player_state: int = 0x1400662F0 +play_sound: int = 0x140064480 # endregion # region OtherOffsets @@ -326,6 +327,8 @@ def __init__(self, process=None, logger=None): self.bean_has_died_address: int = 0 self.on_bean_death_function: Optional[Callable[[Any], Awaitable[Any]]] = None + self.trigger_sound_id_address = None + self.game_draw_routine_string_addr = None self.game_draw_routine_string_size = 256*MESSAGE_QUEUE_LENGTH self.game_draw_routine_default_string = "Connected to the Well" @@ -410,6 +413,17 @@ def player_address(self): return self.application_state_address + 0x93670 + @property + def current_frame_address(self): + if not self.attached_to_process: + self.log_error("Can't get current frame address without being attached to a process.") + return None + if self.application_state_address is None or self.application_state_address == 0: + self.log_error("Can't get current frame address without knowing the application state's address.") + return None + + return self.application_state_address + 0x9360c + @property def stamps_address(self): if not self.attached_to_process: @@ -481,7 +495,7 @@ def apply_patches(self): self.generate_room_palette_override_patch() - # self.apply_input_reader_patch() + self.apply_every_frame_patch() self.apply_pause_menu_patch() @@ -748,11 +762,10 @@ def apply_pause_menu_patch(self): if pause_menu_patch_on_confirm_trampoline_patch.apply(): self.revertable_patches.append(pause_menu_patch_on_confirm_trampoline_patch) - def apply_input_reader_patch(self): + def apply_every_frame_patch(self): """ - This patch enables watching for additional input beyond just the default controls and triggers functions - when the expected button is pressed. - Originally used as a Warp To Hub command before the Pause Menu patch was implemented. + This patch provides a place to run additional every-frame code from within the loop that runs every frame. + Useful for reading input or triggering things when the patch sees a watched value change. """ # input_reader_patch = (Patch("input_reader_patch", 0x140133c00, self.process) # .push_r15().push_r14().push_rsi().push_rdi().push_rbp().push_rbx()#.mov_to_rax(self.application_state_address + 0x93670) @@ -783,34 +796,65 @@ def apply_input_reader_patch(self): # Equipment: Top 0x27a, Ball 0x27d, Wheel 0x283, Remote 0x1d2, Slink 0x1a1, Yoyo 0x14e, UV 0x143, Bubble 0xa2, Flute 0xa9, Lantern 0x6d # Quest: Egg65 0x2c7, OfficeKey 0x269, QuestionKey 0x26a, MDisc 0x17e # Egg: 0x5a - input_reader_patch = (Patch("input_reader_patch", self.custom_memory_current_offset, self.process) - .get_key_pressed(0x48) - .cmp_al1_byte(0) - .je_near(0x80) - .push_rcx().push_rdx().push_r8().push_r9() - .mov_from_absolute_address_to_eax(self.player_address + 0x5D) # get player state - .cmp_eax(5) # only allow warp while idle, walking, jumping, falling, or climbing a ladder - .jnl_short(56) - .warp(self.player_address, self.unstuck_room_x, self.unstuck_room_y, self.unstuck_pos_x, self.unstuck_pos_y, self.unstuck_map) + + self.trigger_sound_id_address = self.custom_memory_current_offset + self.custom_memory_current_offset += 0x4 + self.process.write_bytes(self.trigger_sound_id_address, b'\x00\x00\x00\x00', 4) + frame_patch = (Patch("frame_patch", self.custom_memory_current_offset, self.process) + # .get_key_pressed(0x48) + # .cmp_al1_byte(0) + # .je_near(0x80) + # .push_rcx().push_rdx().push_r8().push_r9() + # .mov_from_absolute_address_to_eax(self.player_address + 0x5D) # get player state + # .cmp_eax(5) # only allow warp while idle, walking, jumping, falling, or climbing a ladder + # .jnl_short(56) + # .warp(self.player_address, self.unstuck_room_x, self.unstuck_room_y, self.unstuck_pos_x, self.unstuck_pos_y, self.unstuck_map) # .get_an_item(slot_address, 0x14c, 0x00, 0xff) - .pop_r9().pop_r8().pop_rdx().pop_rcx() - .nop(0x80) + # .pop_r9().pop_r8().pop_rdx().pop_rcx() + # .nop(0x80) + .mov_from_absolute_address_to_eax(self.trigger_sound_id_address) + .cmp_al1_byte(0) + .je_near(100) + # .je_near(81) + .mov_to_rax(self.trigger_sound_id_address) + .mov_rax_pointer_contents_to_rcx() # sound_id + .mov_rdx(1) # volume I think, but this particular sound function ignores this and always sends the same volume + .mov_to_rax(self.player_address) + .mov_rax_pointer_contents_to_r8() # position to play the sound at (we're playing it right on the bean) + .mov_r9(0x0) # not sure what this arg is, can't tell any obvious difference from playing around with it + .call_far(play_sound) + + .mov_rbx(self.trigger_sound_id_address) + .mov_to_eax(0x00000000) + .mov_eax_to_address_in_rbx() + + .nop(0x100) + # .mov_ecx(0x41) + # .mov_to_rax(self.player_address) + # .mov_rax_pointer_contents_to_rdx() + # .mov_r8(0x6) + # .mov_r9(0x0) + # .call_far(0x140064480) + .mov_edi(0x841c) .mov_from_absolute_address_to_rax(0x1420949D0).movq_rax_to_xmm6() .mov_from_absolute_address_to_rax(0x1420949F4).movq_rax_to_xmm7() .jmp_far(0x14003B7FD) ) - self.custom_memory_current_offset += len(input_reader_patch) + self.custom_memory_current_offset += len(frame_patch) if self.log_debug_info: - self.log_info(f"Applying input_reader_patch...\n{input_reader_patch}") - if input_reader_patch.apply(): - self.revertable_patches.append(input_reader_patch) - input_reader_trampoline = (Patch("input_reader_trampoline", 0x14003B7D1, self.process) - .jmp_far(input_reader_patch.base_address).nop(2)) + self.log_info(f"Applying frame_patch...\n{frame_patch}") + if frame_patch.apply(): + self.revertable_patches.append(frame_patch) + frame_trampoline = (Patch("frame_trampoline", 0x14003B7D1, self.process) + .jmp_far(frame_patch.base_address).nop(2)) + if self.log_debug_info: + self.log_info(f"Applying frame_trampoline...\n{frame_trampoline}") + if frame_trampoline.apply(): + self.revertable_patches.append(frame_trampoline) + if self.log_debug_info: - self.log_info(f"Applying input_reader_trampoline...\n{input_reader_trampoline}") - if input_reader_trampoline.apply(): - self.revertable_patches.append(input_reader_trampoline) + self.log_info(f"trigger_sound_id_address: {hex(self.trigger_sound_id_address)}") def apply_disable_anticheat_patch(self): """ @@ -1436,6 +1480,13 @@ def set_player_state(self, state: int): except Exception as e: self.log_error(f"Error while attempting to set player state: {e}") + def play_sound(self, sound: int): + try: + if self.attached_to_process and self.trigger_sound_id_address: + self.process.write_uint(self.trigger_sound_id_address, sound) + except Exception as e: + self.log_error(f"Error while attempting to set player state: {e}") + def set_title_text(self, text: str): try: if not self.attached_to_process or self.main_menu_draw_string_addr is None: diff --git a/worlds/animal_well/client.py b/worlds/animal_well/client.py index 3e3bb7d0b289..19e9f5ba195f 100644 --- a/worlds/animal_well/client.py +++ b/worlds/animal_well/client.py @@ -87,6 +87,18 @@ def _cmd_fullbright(self, val=""): logger.info(f"Enabling fullbright...") self.ctx.bean_patcher.enable_fullbright() + def _cmd_play_sound(self, sound_id="63"): + """ + Plays a sound. + """ + if isinstance(self.ctx, AnimalWellContext): + if sound_id.isnumeric(): + sound_id = int(sound_id) + else: + sound_id = 63 + + self.ctx.bean_patcher.play_sound(sound_id) + def _cmd_deathlink(self, val=""): """ Toggles deathlink. @@ -306,6 +318,10 @@ def display_text_in_client(self, text: str): if self.bean_patcher is not None and self.bean_patcher.attached_to_process: self.bean_patcher.display_to_client(text) + def play_sound(self, sound_id: int): + if self.bean_patcher is not None and self.bean_patcher.attached_to_process: + self.bean_patcher.play_sound(sound_id) + async def on_bean_death(self): death_link_key = f"{self.get_active_game_slot()}|death_link" if self.stored_data.get(death_link_key, None) is None: @@ -389,17 +405,21 @@ def on_package(self, cmd: str, args: dict): item_name = self.item_names.lookup_in_slot(args.get("item").item, self.slot) location_name = self.location_names.lookup_in_slot(args.get("item").location, player_slot) text = f"Hint: Your {item_name} is at {location_name}." + self.play_sound(68) self.display_text_in_client(text) elif msg_type == "Join": self.display_text_in_client(args.get("data")[0]["text"]) + self.play_sound(68) elif msg_type == "Part": self.display_text_in_client(args.get("data")[0]["text"]) + self.play_sound(68) elif msg_type == "ItemCheat": if args.get("receiving") != self.slot: return item_name = self.item_names.lookup_in_game(args.get("item").item) text = f"You received your {item_name}." self.display_text_in_client(text) + self.play_sound(68) elif msg_type == "ItemSend": destination_player_id = args["receiving"] source_player_id = args["item"][2] # it's a tuple, so we can't index by name @@ -424,6 +444,10 @@ def on_package(self, cmd: str, args: dict): self.display_text_in_client(text) elif msg_type == "Countdown": text = "".join(o["text"] for o in args.get("data")) + if text.find("GO") > -1: + self.play_sound(68) + else: + self.play_sound(56) self.display_text_in_client(text) elif msg_type == "CommandResult": pass @@ -475,13 +499,13 @@ def on_package(self, cmd: str, args: dict): # since we're setting our tags properly, we don't need to check our deathlink setting if "tags" in args: if self.last_death_link != args["data"]["time"]: - self.on_deathlink(args["data"]) + Utils.async_start(self.on_deathlink(args["data"])) except Exception as e: logger.error("Error while parsing Package from AP: %s", e) logger.info("Package details: {}".format(args)) - def on_deathlink(self, data: Dict[str, Any]) -> None: + async def on_deathlink(self, data: Dict[str, Any]) -> None: self.last_death_link = max(data["time"], self.last_death_link) text = DEATHLINK_RECEIVED_MESSAGE.replace("{name}", data.get("source", "A Player")) cause = data.get("cause", None) @@ -490,7 +514,17 @@ def on_deathlink(self, data: Dict[str, Any]) -> None: text = cause logger.info(text) + self.display_text_in_client("Deathlink incoming in 3...") + self.play_sound(56) + await asyncio.sleep(1) + self.display_text_in_client("Deathlink incoming in 2...") + self.play_sound(56) + await asyncio.sleep(1) + self.display_text_in_client("Deathlink incoming in 1...") + self.play_sound(56) + await asyncio.sleep(1) self.display_text_in_client(text) + self.play_sound(66) self.bean_patcher.set_player_state(5) def get_active_game_slot(self) -> int: @@ -1252,6 +1286,8 @@ def write_to_game(self, ctx): total_hearts = int.from_bytes(ctx.process_handle.read_bytes(slot_address + 0x1B4, 1), byteorder="little") # berries_to_use multiplied by 3 to always give you +3 hearts + if berries_to_use > 0: + ctx.play_sound(39) total_hearts = min(total_hearts + berries_to_use * 3, 255) buffer = bytes([total_hearts]) ctx.process_handle.write_bytes(slot_address + 0x1B4, buffer, 1) @@ -1278,6 +1314,8 @@ def write_to_game(self, ctx): total_firecrackers = int.from_bytes(ctx.process_handle.read_bytes(slot_address + 0x1B3, 1), byteorder="little") # multiply firecrackers to use by 6 so that it always fills up your inventory + if firecrackers_to_use > 0: + ctx.play_sound(42) total_firecrackers = min(total_firecrackers + firecrackers_to_use * 6, 6 if self.fanny_pack else 3) buffer = bytes([total_firecrackers]) ctx.process_handle.write_bytes(slot_address + 0x1B3, buffer, 1) @@ -1403,7 +1441,9 @@ async def get_animal_well_process_handle(ctx: AnimalWellContext): ctx.bean_patcher.apply_patches() - ctx.display_dialog("Connected to client!", "") + # ctx.display_text_in_client("Connected to client!") + # ctx.display_dialog("Connected to client!", "") + ctx.play_sound(68) else: raise NotImplementedError("Only Windows is implemented right now") except (pymem.exception.ProcessNotFound, pymem.exception.CouldNotOpenProcess, pymem.exception.ProcessError, diff --git a/worlds/animal_well/patch.py b/worlds/animal_well/patch.py index 3c724c6f3792..033fa1baa7c2 100644 --- a/worlds/animal_well/patch.py +++ b/worlds/animal_well/patch.py @@ -485,18 +485,33 @@ def mov_from_absolute_address_to_rax(self, value): def mov_eax_pointer_contents_to_ecx(self): """ - Moves a 32-bit value from the 32-bit address specified in EAX to ECX + Moves a 32-bit value from the 64-bit address specified in RAX to ECX 2 bytes """ return self.add_bytes(b'\x8b\x08') def mov_eax_pointer_contents_to_edx(self): """ - Moves a 32-bit value from the 32-bit address specified in EAX to EDX + Moves a 32-bit value from the 64-bit address specified in RAX to EDX 2 bytes """ return self.add_bytes(b'\x8b\x10') + def mov_eax_pointer_contents_to_r8d(self): + """ + Moves a 32-bit value from the 64-bit address specified in RAX to R8D + 3 bytes + """ + return self.add_bytes(b'\x44\x8b\x00') + + def mov_eax_pointer_contents_to_r9d(self): + """ + Moves a 32-bit value from the 64-bit address specified in RAX to R9D + 3 bytes + """ + return self.add_bytes(b'\x44\x8b\x08') + + def mov_rax_pointer_contents_to_rcx(self): """ Moves a 64-bit value from the 64-bit address specified in RAX to RCX @@ -511,6 +526,13 @@ def mov_rax_pointer_contents_to_rdx(self): """ return self.add_bytes(b'\x48\x8b\x10') + def mov_rax_pointer_contents_to_r8(self): + """ + Moves a 64-bit value from the 64-bit address specified in RAX to R8 + 3 bytes + """ + return self.add_bytes(b'\x48\x8b\x10') + def mov_al_to_address_in_rbx(self): """ Moves an 8-bit value from AL to the address specified in RBX