Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 76 additions & 25 deletions worlds/animal_well/bean_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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:
Expand Down
46 changes: 43 additions & 3 deletions worlds/animal_well/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 24 additions & 2 deletions worlds/animal_well/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down