async def test_perform_memory_operations_success(backend: NintendontBackend): backend._socket = MagicMock() backend._socket.max_input = 120 backend._socket.max_output = 100 backend._socket.max_addresses = 8 backend._socket.writer.drain = AsyncMock() backend._socket.reader.read = AsyncMock(side_effect=[ b"\x03" + b"A" * 50 + b"B" * 30, b"\x01" + b"C" * 60, ]) ops = { MemoryOperation(0x1000, read_byte_count=50): b"A" * 50, MemoryOperation(0x1000, offset=10, read_byte_count=30, write_bytes=b"1" * 30): b"B" * 30, MemoryOperation(0x1000, read_byte_count=60): b"C" * 60, } # Run result = await backend._perform_memory_operations(list(ops.keys())) # Assert backend._socket.writer.drain.assert_has_awaits([call(), call()]) backend._socket.writer.write.assert_has_calls([ call(b'\x00\x02\x01\x01\x00\x00\x10\x00' + b'\x80\x32' + b'\xd0\x1e\x00\n' + (b"1" * 30)), call(b'\x00\x01\x01\x01\x00\x00\x10\x00\x80\x3c'), ]) assert result == ops backend._socket.reader.read.assert_has_awaits([call(1024), call(1024)])
async def test_perform_memory_operations_invalid(backend: NintendontBackend): backend._socket = MagicMock() backend._socket.max_input = 120 backend._socket.max_output = 100 backend._socket.max_addresses = 8 backend._socket.writer.drain = AsyncMock() backend._socket.reader.read = AsyncMock(side_effect=[ b"\x03" + b"A" * 50 + b"B" * 30, ]) # Run with pytest.raises(MemoryOperationException): await backend._perform_memory_operations([ MemoryOperation(0x1000, read_byte_count=50), MemoryOperation(0x2000, read_byte_count=10), MemoryOperation(0x2000, read_byte_count=10), ]) # Assert backend._socket.writer.drain.assert_has_awaits([call()]) backend._socket.writer.write.assert_has_calls([ call( b'\x00\x03\x02\x01\x00\x00\x10\x00\x00\x00 \x00\x802\x81\n\x81\n'), ]) backend._socket.reader.read.assert_has_awaits([call(1024)])
async def _ensure_initialized_game_memory(self): if self._game_memory_initialized: return try: self.patches = self._expected_patches world = self.game.world_list.worlds[0] await self._perform_memory_operations([ # Build String MemoryOperation( self.patches.build_string_address, write_bytes=self._expected_patches.build_string), # current CWorld MemoryOperation(self.patches.game_state_pointer, offset=4, write_bytes=world.world_asset_id.to_bytes( 4, "big")), # CPlayer VTable MemoryOperation( self.patches.cstate_manager_global + 0x14fc, offset=0, write_bytes=self.patches.cplayer_vtable.to_bytes(4, "big")), # CPlayerState MemoryOperation(self.patches.cstate_manager_global + 0x150c, write_bytes=0xA00000.to_bytes(4, "big")), ]) self._game_memory_initialized = True finally: self.patches = None
def _memory_operation( self, op: MemoryOperation, pointers: Dict[int, Optional[int]]) -> Optional[bytes]: op.validate_byte_sizes() address = op.address if op.offset is not None: if address not in pointers: raise MemoryOperationException( f"Invalid op: {address} is not in pointers") address = pointers[address] + op.offset _validate_range(address, op.byte_count) if not self.dolphin.is_hooked(): raise MemoryOperationException("Lost connection do Dolphin") result = None if op.read_byte_count is not None: result = self.dolphin.read_bytes(address, op.read_byte_count) if op.write_bytes is not None: self.dolphin.write_bytes(address, op.write_bytes) self.logger.debug(f"Wrote {op.write_bytes.hex()} to {address:x}") return result
def _memory_operation(self, op: MemoryOperation) -> Optional[bytes]: op.validate_byte_sizes() address = op.address if op.offset is not None: address = self._read_memory_format(">I", address)[0] address += op.offset result = None if op.read_byte_count is not None: result = self._read_memory(address, op.read_byte_count) if op.write_bytes is not None: self._write_memory(address, op.write_bytes) return result
async def test_execute_remote_patches(backend, mocker, has_message): # Setup backend.patches = dol_patcher.ALL_VERSIONS_PATCHES[0] backend._write_string_to_game_buffer = MagicMock() patch_address, patch_bytes = MagicMock(), MagicMock() mock_remote_execute: MagicMock = mocker.patch( "randovania.games.prime.all_prime_dol_patches.create_remote_execution_body", return_value=(patch_address, patch_bytes)) mock_hud_patch: MagicMock = mocker.patch( "randovania.games.prime.all_prime_dol_patches.call_display_hud_patch", return_value=([7, 8])) patches = [[1, 2, 3], [4, 5, 6]] instructions = [1, 2, 3, 4, 5, 6] message = None message_ops = [] if has_message: message = "A message to show!" instructions.extend([7, 8]) message_ops.append(backend._write_string_to_game_buffer.return_value) memory_operations = [ *message_ops, MemoryOperation(patch_address, write_bytes=patch_bytes), MemoryOperation(backend.patches.cstate_manager_global + 0x2, write_bytes=b"\x01"), ] # Run await backend.execute_remote_patches(patches, message) # Assert mock_remote_execute.assert_called_once_with(backend.patches.string_display, instructions) backend._perform_memory_operations.assert_awaited_once_with( memory_operations) if has_message: mock_hud_patch.assert_called_once_with(backend.patches.string_display) backend._write_string_to_game_buffer.assert_called_once_with(message) assert backend.message_cooldown == 4 else: backend._write_string_to_game_buffer.assert_not_called() assert backend.message_cooldown == 0
def _prepare_requests_for( self, ops: List[MemoryOperation]) -> List[RequestBatch]: requests: List[RequestBatch] = [] current_batch = RequestBatch() def _new_request(): nonlocal current_batch requests.append(current_batch) current_batch = RequestBatch() processes_ops = [] max_write_size = self._socket.max_input - 20 for op in ops: if op.byte_count == 0: continue op.validate_byte_sizes() if op.read_byte_count is None and ( op.write_bytes is not None and len(op.write_bytes) > max_write_size): self.logger.debug( f"Operation had {len(op.write_bytes)} bytes, " f"above the limit of {max_write_size}. Splitting.") for offset in range(0, len(op.write_bytes), max_write_size): if op.offset is None: address = op.address + offset op_offset = None else: address = op.address op_offset = op.offset + offset processes_ops.append( MemoryOperation( address=address, offset=op_offset, write_bytes=op.write_bytes[offset:min( offset + max_write_size, len(op.write_bytes))], )) else: processes_ops.append(op) for op in processes_ops: experimental = current_batch.copy() experimental.add_op(op) if not experimental.is_compatible_with(self._socket): _new_request() current_batch.add_op(op) if not current_batch.is_compatible_with(self._socket): raise ValueError( f"Request {op} is not compatible with current server.") # Finish the last batch _new_request() return requests
async def test_send_message(backend, message_original, message_encoded, previous_size): # Setup backend.patches = dol_patcher.ALL_VERSIONS_PATCHES[0] add_memory_op_result(backend, b"\x00") string_ref = backend.patches.string_display.message_receiver_string_ref has_message_address = backend.patches.string_display.cstate_manager_global + 0x2 backend._last_message_size = previous_size # Run backend.display_message(message_original) await backend._send_message_from_queue(1) # Assert backend._perform_memory_operations.assert_has_awaits([ call([MemoryOperation(has_message_address, read_byte_count=1)]), call([ MemoryOperation(string_ref, write_bytes=message_encoded), MemoryOperation(has_message_address, write_bytes=b'\x01'), ]), ])
async def test_write_string_to_game_buffer(backend, message_original, message_encoded, previous_size): # Setup backend.patches = dol_patcher.ALL_VERSIONS_PATCHES[0] backend._last_message_size = previous_size # Run result = backend._write_string_to_game_buffer(message_original) # Assert assert result == MemoryOperation(backend.patches.string_display.message_receiver_string_ref, write_bytes=message_encoded)
async def test_send_message_has_pending_message(backend): # Setup backend.patches = dol_patcher.ALL_VERSIONS_PATCHES[0] add_memory_op_result(backend, b"\x01") has_message_address = backend.patches.string_display.cstate_manager_global + 0x2 # Run backend.display_message("Magoo") await backend._send_message_from_queue(1) # Assert backend._perform_memory_operations.assert_has_awaits([ call([MemoryOperation(has_message_address, read_byte_count=1)]), ]) assert len(backend.message_queue) > 0
async def test_perform_memory_operations(backend: DolphinBackend): backend.dolphin.follow_pointers.return_value = 0x80003000 backend.dolphin.read_bytes.side_effect = [b"A" * 50, b"B" * 30, b"C" * 10] # Run result = await backend._perform_memory_operations([ MemoryOperation(0x80001000, offset=20, read_byte_count=50), MemoryOperation(0x80001000, offset=10, read_byte_count=30, write_bytes=b"1" * 30), MemoryOperation(0x80002000, read_byte_count=10), ]) # Assert assert list(result.values()) == [b"A" * 50, b"B" * 30, b"C" * 10] backend.dolphin.follow_pointers.assert_called_once_with(0x80001000, [0x0]) backend.dolphin.read_bytes.assert_has_calls([ call(0x80003000 + 20, 50), call(0x80003000 + 10, 30), call(0x80002000, 10), ]) backend.dolphin.write_bytes.assert_called_once_with( 0x80003000 + 10, b"1" * 30)
async def test_perform_write_inventory_dark_suit(backend, echoes_game_description, has_light_suit): # Setup backend.patches = dol_patcher.ALL_VERSIONS_PATCHES[0] backend._perform_memory_operations = AsyncMock() dark_suit = echoes_game_description.resource_database.get_item(13) light_suit = echoes_game_description.resource_database.get_item(14) changed_items = {dark_suit: InventoryItem(1, 1)} if has_light_suit: backend._inventory = {light_suit: InventoryItem(1, 1)} else: backend._inventory = {} # Run await backend._perform_write_inventory(changed_items) # Assert backend._perform_memory_operations.assert_awaited_once() write_op = backend._perform_memory_operations.mock_calls[0].args[0] assert write_op[1] == MemoryOperation( address=2151533548, offset=84, write_bytes=b"\x00\x00\x00\x02" if has_light_suit else b"\x00\x00\x00\x01", )
async def test_update_inventory_with_change(backend, item): # Setup backend.patches = dol_patcher.ALL_VERSIONS_PATCHES[0] backend._perform_memory_operations = AsyncMock() backend._inventory = { item: InventoryItem(0, 0) for item in backend.game.resource_database.item } new_inventory = copy.copy(backend._inventory) new_inventory[backend.game.resource_database.multiworld_magic_item] = InventoryItem(1, 15) # Run await backend._update_inventory(new_inventory) # Assert backend._perform_memory_operations.assert_awaited_once_with([ MemoryOperation( address=backend._get_player_state_pointer(), write_bytes=struct.pack(">II", 1, 15), read_byte_count=8, offset=connection_backend._powerup_offset(backend.game.resource_database.multiworld_magic_item.index), ) ])
async def test_perform_single_giant_memory_operation( backend: NintendontBackend): backend._socket = MagicMock() backend._socket.max_input = 120 backend._socket.max_output = 100 backend._socket.max_addresses = 8 backend._socket.writer.drain = AsyncMock() backend._socket.reader.read = AsyncMock(side_effect=[ b"\x01", b"\x01", ]) # Run result = await backend._perform_single_memory_operations( MemoryOperation(0x1000, write_bytes=b"1" * 200), ) # Assert backend._socket.writer.drain.assert_has_awaits([call(), call()]) backend._socket.writer.write.assert_has_calls([ call(b'\x00\x01\x01\x01\x00\x00\x10\x00' + b'\x40\x64' + (b"1" * 100)), call(b'\x00\x01\x01\x01\x00\x00\x10\x64' + b'\x40\x64' + (b"1" * 100)), ]) assert result is None backend._socket.reader.read.assert_has_awaits([call(1024), call(1024)])