Beispiel #1
0
    async def perform_memory_operations(
            self, ops: List[MemoryOperation]) -> Dict[MemoryOperation, bytes]:
        if self._socket is None:
            raise MemoryOperationException("Not connected")

        requests = self._prepare_requests_for(ops)
        all_responses = await self._send_requests_to_socket(requests)

        result = {}

        for request, response in zip(requests, all_responses):
            read_index = request.num_validator_bytes
            for i, op in enumerate(request.ops):
                if op.read_byte_count is None:
                    continue

                if _was_invalid_address(response, i):
                    raise MemoryOperationException(
                        "Operation tried to read an invalid address")

                split = response[read_index:read_index + op.read_byte_count]
                if len(split) != op.read_byte_count:
                    raise MemoryOperationException(
                        f"Received {len(split)} bytes, expected {op.read_byte_count}"
                    )
                else:
                    assert op not in result
                    result[op] = split

                read_index += op.read_byte_count

        return result
    async def perform_memory_operations(
            self, ops: List[MemoryOperation]) -> Dict[MemoryOperation, bytes]:
        pointers_to_read = set()
        for op in ops:
            if op.offset is not None:
                pointers_to_read.add(op.address)

        pointers = {}
        for pointer in pointers_to_read:
            if not self.dolphin.is_hooked():
                raise MemoryOperationException("Lost connection do Dolphin")

            try:
                pointers[pointer] = self.dolphin.follow_pointers(pointer, [0])
            except RuntimeError:
                pointers[pointer] = None
                self.logger.debug(
                    f"Failed to read a valid pointer from {pointer:x}")
                self._test_still_hooked()

            if not self.dolphin.is_hooked():
                raise MemoryOperationException("Lost connection do Dolphin")

        result = {}
        for op in ops:
            op_result = self._memory_operation(op, pointers)
            if op_result is not None:
                result[op] = op_result
        return result
Beispiel #3
0
    async def _send_requests_to_socket(
            self, requests: List[RequestBatch]) -> List[bytes]:
        all_responses = []
        try:
            for request in requests:
                data = request.build_request_data()
                self._socket.writer.write(data)
                await self._socket.writer.drain()
                if request.output_bytes > 0:
                    response = await asyncio.wait_for(
                        self._socket.reader.read(1024), timeout=15)
                    all_responses.append(response)
                else:
                    all_responses.append(b"")

        except (OSError, asyncio.TimeoutError) as e:
            if isinstance(e, asyncio.TimeoutError):
                self.logger.warning(
                    f"Timeout when reading response from {self._ip}")
                self._socket_error = MemoryOperationException(
                    f"Timeout when reading response")
            else:
                self.logger.warning(
                    f"Unable to send {len(requests)} requests to {self._ip}:{self._port}: {e}"
                )
                self._socket_error = MemoryOperationException(
                    f"Unable to send {len(requests)} requests: {e}")

            await self.disconnect()
            raise self._socket_error from e

        return all_responses
    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:x} is not in pointers")

            if pointers[address] is None:
                return None
            address = pointers[address] + op.offset

        _validate_range(address, op.byte_count)

        if not self.dolphin.is_hooked():
            raise MemoryOperationException("Lost connection do Dolphin")

        try:
            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}")

        except RuntimeError as e:
            raise MemoryOperationException(f"Lost connection do Dolphin: {e}")

        return result
async def test_update_calls_interact_with_game(backend: ConnectionBackend, interact_fails,
                                               expected_game, connected_game):
    # Setup
    backend.connector = None
    backend._world = True
    backend.set_expected_game(expected_game)

    if connected_game is not None:
        connector = AsyncMock()
        connector.game_enum = connected_game
    else:
        connector = None

    backend._interact_with_game = AsyncMock(side_effect=MemoryOperationException("err") if interact_fails else None)
    backend._identify_game = AsyncMock(return_value=connector)

    should_call_interact = connected_game is not None
    if should_call_interact:
        should_call_interact = (expected_game == connected_game) or (expected_game is None)

    # Run
    await backend.update(1)

    # Assert
    backend._identify_game.assert_awaited_once_with()
    if should_call_interact:
        backend._interact_with_game.assert_awaited_once_with(1)
        if interact_fails:
            assert backend._world is None
        else:
            assert backend._world is not None
    else:
        backend._interact_with_game.assert_not_awaited()
async def test_interact_with_game(backend: ConnectionBackend, depth: int, failure_at: Optional[int]):
    # Setup
    backend.message_cooldown = 2
    backend.connector = AsyncMock()

    backend.connector.current_game_status.return_value = (
        depth <= 1,  # has pending op
        MagicMock() if depth > 0 else None,  # world
    )

    expectation = contextlib.nullcontext()
    if failure_at == 1:
        backend.connector.get_inventory.side_effect = MemoryOperationException("error at _get_inventory")
        expectation = pytest.raises(MemoryOperationException, match="error at _get_inventory")

    backend._multiworld_interaction = AsyncMock()
    if failure_at == 2:
        backend._multiworld_interaction.side_effect = MemoryOperationException("error at _check_for_collected_index")
        expectation = pytest.raises(MemoryOperationException, match="error at _check_for_collected_index")

    expected_depth = min(depth, failure_at) if failure_at is not None else depth
    if (failure_at or 999) > depth:
        expectation = contextlib.nullcontext()

    # Run
    with expectation:
        await backend._interact_with_game(1)

    # Assert
    assert backend.message_cooldown == (2 if expected_depth < 2 else 1)
    backend.connector.current_game_status.assert_awaited_once_with(backend.executor)

    if expected_depth > 0:
        backend.connector.get_inventory.assert_awaited_once_with(backend.executor)
    else:
        backend.connector.get_inventory.assert_not_awaited()

    if expected_depth > 1:
        backend._multiworld_interaction.assert_awaited_once_with()
    else:
        backend._multiworld_interaction.assert_not_awaited()

    if 0 < depth:
        assert backend._world is not None
    else:
        assert backend._world is None
Beispiel #7
0
    async def get_inventory(self,
                            executor: MemoryOperationExecutor) -> Inventory:
        """Fetches the inventory represented by the given game memory."""

        memory_ops = await self._memory_op_for_items(executor, [
            item for item in self.game.resource_database.item
            if item.extra["item_id"] < 1000
        ])
        ops_result = await executor.perform_memory_operations(memory_ops)

        inventory = {}
        for item, memory_op in zip(self.game.resource_database.item,
                                   memory_ops):
            inv = InventoryItem(*struct.unpack(">II", ops_result[memory_op]))
            if (inv.amount > inv.capacity or inv.capacity > item.max_capacity
                ) and (item !=
                       self.game.resource_database.multiworld_magic_item):
                raise MemoryOperationException(
                    f"Received {inv} for {item.long_name}, which is an invalid state."
                )
            inventory[item] = inv

        return inventory
Beispiel #8
0
def _was_invalid_address(response: bytes, i: int) -> bool:
    try:
        return not response[i // 8] & (1 << (i % 8))
    except IndexError:
        raise MemoryOperationException(
            "Server response too short for validator bytes")
def _validate_range(address: int, size: int):
    if address < MEM1_START or address + size > MEM1_END:
        raise MemoryOperationException(
            f"Range {address:x} -> {address + size:x} is outside of the GameCube memory range."
        )