Source code for pyboy.api.memory_scanner

from enum import Enum

from pyboy.utils import bcd_to_dec


[docs] class StandardComparisonType(Enum): """Enumeration for defining types of comparisons that do not require a previous value.""" EXACT = 1 LESS_THAN = 2 GREATER_THAN = 3 LESS_THAN_OR_EQUAL = 4 GREATER_THAN_OR_EQUAL = 5
[docs] class DynamicComparisonType(Enum): """Comparisons between values found in the previous and current scans.""" UNCHANGED = 1 CHANGED = 2 INCREASED = 3 DECREASED = 4 MATCH = 5
[docs] class ScanMode(Enum): """Interpret scanned bytes as an unsigned integer or binary-coded decimal.""" INT = 1 BCD = 2
[docs] class MemoryScanner: """A class for scanning memory within a given range.""" def __init__(self, pyboy): self.pyboy = pyboy self._memory_cache = {} self._memory_cache_byte_width = 1 self._memory_cache_value_type = ScanMode.INT self._memory_cache_byteorder = "little"
[docs] def scan_memory( self, target_value=None, start_addr=0x0000, end_addr=0xFFFF, standard_comparison_type=StandardComparisonType.EXACT, value_type=ScanMode.INT, byte_width=1, byteorder="little", ): """ Scan the inclusive address range from `start_addr` to `end_addr` for a target value. Example: ```python >>> pyboy.memory[0xC000] = 4 >>> pyboy.memory_scanner.scan_memory(4, start_addr=0xC000, end_addr=0xC000) [49152] ``` Args: target_value (int or None): Value to search for. If None, every value matches. start_addr (int): First address to scan. Defaults to 0. end_addr (int): Last address to scan, inclusive. Defaults to 65535. standard_comparison_type (StandardComparisonType): Comparison to apply. Defaults to EXACT. value_type (ScanMode): Interpret values as INT or BCD. Defaults to INT. byte_width (int): Positive number of bytes per value. Defaults to 1. byteorder (str): "little" or "big" byte order for multi-byte values. Defaults to "little". Returns: list[int]: Addresses where the target value is found. Raises: ValueError: If `byte_width` is not positive. """ if byte_width <= 0: raise ValueError("byte_width must be positive") self._memory_cache = {} self._memory_cache_byte_width = byte_width self._memory_cache_value_type = value_type self._memory_cache_byteorder = byteorder for addr in range( start_addr, end_addr - (byte_width - 1) + 1 ): # Adjust the loop to prevent reading past end_addr # Read multiple bytes based on byte_width and byteorder value_bytes = self.pyboy.memory[addr : addr + byte_width] value = int.from_bytes(value_bytes, byteorder) if value_type == ScanMode.BCD: value = bcd_to_dec(value, byte_width, byteorder) if target_value is None or self._check_value(value, target_value, standard_comparison_type.value): self._memory_cache[addr] = value return list(self._memory_cache.keys())
[docs] def rescan_memory(self, new_value=None, dynamic_comparison_type=DynamicComparisonType.UNCHANGED, byteorder=None): """ Filter cached addresses using values read in the current scan. Example: ```python >>> pyboy.memory[0xC000] = 4 >>> pyboy.memory_scanner.scan_memory(4, start_addr=0xC000, end_addr=0xC000) [49152] >>> pyboy.memory[0xC000] = 8 >>> from pyboy.api.memory_scanner import DynamicComparisonType >>> addresses = pyboy.memory_scanner.rescan_memory(8, DynamicComparisonType.MATCH) >>> print(addresses) [49152] ``` Args: new_value (int or None): Target value used by MATCH; required for MATCH and ignored by other comparisons. dynamic_comparison_type (DynamicComparisonType): Comparison to apply. Defaults to UNCHANGED. byteorder (str or None): Byte order for reading values. If None, reuse the byte order from the previous `scan_memory` call. Returns: list[int]: Addresses remaining in the cache after filtering. """ if byteorder is None: byteorder = self._memory_cache_byteorder for addr, value in self._memory_cache.copy().items(): current_value = int.from_bytes( self.pyboy.memory[addr : addr + self._memory_cache_byte_width], byteorder=byteorder ) if self._memory_cache_value_type == ScanMode.BCD: current_value = bcd_to_dec(current_value, self._memory_cache_byte_width, byteorder) if dynamic_comparison_type == DynamicComparisonType.UNCHANGED: if value != current_value: self._memory_cache.pop(addr) else: self._memory_cache[addr] = current_value elif dynamic_comparison_type == DynamicComparisonType.CHANGED: if value == current_value: self._memory_cache.pop(addr) else: self._memory_cache[addr] = current_value elif dynamic_comparison_type == DynamicComparisonType.INCREASED: if value >= current_value: self._memory_cache.pop(addr) else: self._memory_cache[addr] = current_value elif dynamic_comparison_type == DynamicComparisonType.DECREASED: if value <= current_value: self._memory_cache.pop(addr) else: self._memory_cache[addr] = current_value elif dynamic_comparison_type == DynamicComparisonType.MATCH: if new_value is None: raise ValueError("new_value must be specified when using DynamicComparisonType.MATCH") if current_value != new_value: self._memory_cache.pop(addr) else: self._memory_cache[addr] = current_value else: raise ValueError("Invalid comparison type") return list(self._memory_cache.keys())
def _check_value(self, value, target_value, standard_comparison_type): """ Compares a value with the target value based on the specified compare type. Args: value (int): The value to compare. target_value (int or None): The target value to compare against. standard_comparison_type (StandardComparisonType): The type of comparison to use. Returns: bool: True if the comparison condition is met, False otherwise. """ if standard_comparison_type == StandardComparisonType.EXACT.value: return value == target_value elif standard_comparison_type == StandardComparisonType.LESS_THAN.value: return value < target_value elif standard_comparison_type == StandardComparisonType.GREATER_THAN.value: return value > target_value elif standard_comparison_type == StandardComparisonType.LESS_THAN_OR_EQUAL.value: return value <= target_value elif standard_comparison_type == StandardComparisonType.GREATER_THAN_OR_EQUAL.value: return value >= target_value else: raise ValueError("Invalid comparison type")