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
Example #6
0
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
Example #7
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'),
        ]),
    ])
Example #9
0
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)])