Example #1
0
    async def execute_remote_patches(self, executor: MemoryOperationExecutor,
                                     patches: List[DolRemotePatch]) -> None:
        """
        Executes a given set of patches on the given memory operator. Should only be called if the bool returned by
        `current_game_status` is False, but validation of this fact is implementation-dependant.
        :param executor:
        :param patches: List of patches to execute
        :return:
        """
        memory_operations = []
        for patch in patches:
            memory_operations.extend(patch.memory_operations)

        patch_address, patch_bytes = all_prime_dol_patches.create_remote_execution_body(
            self.version.string_display,
            [
                instruction for patch in patches
                for instruction in patch.instructions
            ],
        )
        memory_operations.extend([
            MemoryOperation(patch_address, write_bytes=patch_bytes),
            MemoryOperation(self.version.cstate_manager_global + 0x2,
                            write_bytes=b"\x01"),
        ])
        self.logger.debug(
            f"Performing {len(memory_operations)} ops with {len(patches)} patches"
        )
        await executor.perform_memory_operations(memory_operations)
    async def _ensure_initialized_game_memory(self):
        if self._game_memory_initialized:
            return

        world = self.game.world_list.worlds[0]

        await self.perform_memory_operations([
            # Build String
            MemoryOperation(self._used_version.build_string_address,
                            write_bytes=self._used_version.build_string),

            # current CWorld
            MemoryOperation(self._used_version.game_state_pointer,
                            offset=4,
                            write_bytes=world.extra['asset_id'].to_bytes(
                                4, "big")),

            # CPlayer VTable
            MemoryOperation(
                self._used_version.cstate_manager_global + 0x14fc,
                offset=0,
                write_bytes=self._used_version.cplayer_vtable.to_bytes(
                    4, "big")),

            # CPlayerState
            MemoryOperation(self._used_version.cstate_manager_global + 0x150c,
                            write_bytes=0xA00000.to_bytes(4, "big")),
        ])

        self._game_memory_initialized = True
Example #3
0
    async def current_game_status(self, executor: MemoryOperationExecutor) -> Tuple[bool, Optional[World]]:
        """
        Fetches the world the player's currently at, or None if they're not in-game.
        :param executor:
        :return: bool indicating if there's a pending `execute_remote_patches` operation.
        """

        cstate_manager_global = self.version.cstate_manager_global

        mlvl_offset = 8
        asset_id_size = struct.calcsize(self._asset_id_format())

        # TODO: there's one extra pointer indirection
        cplayer_offset = 40
        player_offset = 0x2184

        memory_ops = [
            MemoryOperation(self.version.game_state_pointer, offset=mlvl_offset, read_byte_count=asset_id_size),
            MemoryOperation(cstate_manager_global + 0x2, read_byte_count=1),
            MemoryOperation(cstate_manager_global + cplayer_offset, offset=player_offset, read_byte_count=4),
        ]
        results = await executor.perform_memory_operations(memory_ops)
        player_pointer = results.get(memory_ops[2])
        player_vtable = None
        if player_pointer is not None:
            player_vtable = await executor.perform_single_memory_operation(MemoryOperation(
                struct.unpack(">I", player_pointer)[0], read_byte_count=4,
            ))

        pending_op_byte = results[memory_ops[1]]
        has_pending_op = pending_op_byte != b"\x00"
        return has_pending_op, self._current_status_world(results.get(memory_ops[0]), player_vtable)
Example #4
0
    async def current_game_status(
            self,
            executor: MemoryOperationExecutor) -> Tuple[bool, Optional[World]]:
        """
        Fetches the world the player's currently at, or None if they're not in-game.
        :param executor:
        :return: bool indicating if there's a pending `execute_remote_patches` operation.
        """

        cstate_manager_global = self.version.cstate_manager_global

        asset_id_size = struct.calcsize(self._asset_id_format())
        mlvl_offset = 0x84
        cplayer_offset = 0x84c

        memory_ops = [
            MemoryOperation(self.version.game_state_pointer,
                            offset=mlvl_offset,
                            read_byte_count=asset_id_size),
            MemoryOperation(cstate_manager_global + 0x2, read_byte_count=1),
            MemoryOperation(cstate_manager_global + cplayer_offset,
                            offset=0,
                            read_byte_count=4),
        ]
        results = await executor.perform_memory_operations(memory_ops)

        pending_op_byte = results[memory_ops[1]]

        has_pending_op = pending_op_byte != b"\x00"
        return has_pending_op, self._current_status_world(
            results.get(memory_ops[0]), results.get(memory_ops[2]))
async def test_execute_remote_patches(connector: EchoesRemoteConnector,
                                      version: EchoesDolVersion, mocker):
    # Setup
    patch_address, patch_bytes = MagicMock(), MagicMock()
    mock_remote_execute: MagicMock = mocker.patch(
        "randovania.patching.prime.all_prime_dol_patches.create_remote_execution_body",
        return_value=(patch_address, patch_bytes))

    executor = AsyncMock()

    memory_op_a = MemoryOperation(1234, write_bytes=b"1234")
    instructions = [BaseInstruction(), BaseInstruction()]
    patches = [
        DolRemotePatch([memory_op_a], instructions[:1]),
        DolRemotePatch([], instructions[1:]),
    ]
    memory_operations = [
        memory_op_a,
        MemoryOperation(patch_address, write_bytes=patch_bytes),
        MemoryOperation(version.cstate_manager_global + 0x2,
                        write_bytes=b"\x01"),
    ]

    # Run
    await connector.execute_remote_patches(executor, patches)

    # Assert
    mock_remote_execute.assert_called_once_with(version.string_display,
                                                instructions)
    executor.perform_memory_operations.assert_awaited_once_with(
        memory_operations)
    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
Example #7
0
async def test_perform_memory_operations_invalid(executor: NintendontExecutor):
    executor._socket = MagicMock()
    executor._socket.max_input = 120
    executor._socket.max_output = 100
    executor._socket.max_addresses = 8
    executor._socket.writer.drain = AsyncMock()
    executor._socket.reader.read = AsyncMock(side_effect=[
        b"\x03" + b"A" * 50 + b"B" * 30,
    ])

    # Run
    with pytest.raises(MemoryOperationException):
        await executor.perform_memory_operations([
            MemoryOperation(0x1000, read_byte_count=50),
            MemoryOperation(0x2000, read_byte_count=10),
            MemoryOperation(0x2000, read_byte_count=10),
        ])

    # Assert
    executor._socket.writer.drain.assert_has_awaits([call()])
    executor._socket.writer.write.assert_has_calls([
        call(
            b'\x00\x03\x02\x01\x00\x00\x10\x00\x00\x00 \x00\x802\x81\n\x81\n'),
    ])
    executor._socket.reader.read.assert_has_awaits([call(1024)])
Example #8
0
async def test_perform_memory_operations_success(executor: NintendontExecutor):
    executor._socket = MagicMock()
    executor._socket.max_input = 120
    executor._socket.max_output = 100
    executor._socket.max_addresses = 8
    executor._socket.writer.drain = AsyncMock()
    executor._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 executor.perform_memory_operations(list(ops.keys()))

    # Assert
    executor._socket.writer.drain.assert_has_awaits([call(), call()])
    executor._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
    executor._socket.reader.read.assert_has_awaits([call(1024), call(1024)])
    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 #10
0
 async def is_this_version(self, executor: MemoryOperationExecutor) -> bool:
     """Returns True if the accessible memory matches the version of this connector."""
     operation = MemoryOperation(self.version.build_string_address,
                                 read_byte_count=len(
                                     self.version.build_string))
     build_string = await executor.perform_single_memory_operation(operation
                                                                   )
     return build_string == self.version.build_string
Example #11
0
 async def _memory_op_for_items(
     self,
     executor: MemoryOperationExecutor,
     items: List[ItemResourceInfo],
 ) -> List[MemoryOperation]:
     player_state_pointer = int.from_bytes(
         await executor.perform_single_memory_operation(
             MemoryOperation(
                 address=self.version.cstate_manager_global + 0x8b8,
                 read_byte_count=4,
             )), "big")
     return [
         MemoryOperation(
             address=player_state_pointer,
             offset=_prime1_powerup_offset(item.extra["item_id"]),
             read_byte_count=8,
         ) for item in items
     ]
Example #12
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 i, op in enumerate(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 {i} 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
Example #13
0
async def test_perform_memory_operations(executor: DolphinExecutor):
    executor.dolphin.follow_pointers.return_value = 0x80003000
    executor.dolphin.read_bytes.side_effect = [b"A" * 50, b"B" * 30, b"C" * 10]

    # Run
    result = await executor.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]
    executor.dolphin.follow_pointers.assert_called_once_with(0x80001000, [0x0])
    executor.dolphin.read_bytes.assert_has_calls([
        call(0x80003000 + 20, 50),
        call(0x80003000 + 10, 30),
        call(0x80002000, 10),
    ])
    executor.dolphin.write_bytes.assert_called_once_with(0x80003000 + 10, b"1" * 30)
Example #14
0
 async def _memory_op_for_items(self, executor: MemoryOperationExecutor, items: List[ItemResourceInfo],
                                ) -> List[MemoryOperation]:
     player_state_pointer = self.version.cstate_manager_global + 0x150c
     return [
         MemoryOperation(
             address=player_state_pointer,
             offset=_echoes_powerup_offset(item.extra["item_id"]),
             read_byte_count=8,
         )
         for item in items
     ]
async def test_write_string_to_game_buffer(connector: EchoesRemoteConnector,
                                           version: EchoesDolVersion,
                                           message_original, message_encoded,
                                           previous_size):
    # Setup
    connector._last_message_size = previous_size

    # Run
    result = connector._write_string_to_game_buffer(message_original)

    # Assert
    assert result == MemoryOperation(
        version.string_display.message_receiver_string_ref,
        write_bytes=message_encoded)
Example #16
0
    async def _identify_game(self) -> Optional[RemoteConnector]:
        all_connectors: List[PrimeRemoteConnector] = [
            Prime1RemoteConnector(version)
            for version in prime1_dol_versions.ALL_VERSIONS
        ]
        all_connectors.extend([
            EchoesRemoteConnector(version)
            for version in echoes_dol_versions.ALL_VERSIONS
        ])
        all_connectors.extend([
            CorruptionRemoteConnector(version)
            for version in corruption_dol_versions.ALL_VERSIONS
        ])
        read_first_ops = [
            MemoryOperation(connectors.version.build_string_address,
                            read_byte_count=min(
                                len(connectors.version.build_string), 4))
            for connectors in all_connectors
        ]
        try:
            first_ops_result = await self.executor.perform_memory_operations(
                read_first_ops)
        except (RuntimeError, MemoryOperationException) as e:
            self.logger.debug(f"Unable to probe for game version: {e}")
            return None

        possible_connectors = [
            connectors
            for connectors, read_op in zip(all_connectors, read_first_ops)
            if first_ops_result.get(read_op) ==
            connectors.version.build_string[:4]
        ]

        for connector in possible_connectors:
            try:
                is_version = await connector.is_this_version(self.executor)
            except (RuntimeError, MemoryOperationException) as e:
                return None

            if is_version:
                self.logger.info(
                    f"identified game as {connector.game_enum.long_name}: {connector.version.description}"
                )
                return connector
Example #17
0
async def test_fetch_game_status(connector: CorruptionRemoteConnector,
                                 has_world, has_pending_op, has_cplayer,
                                 correct_vtable):
    # Setup
    expected_world = connector.game.world_list.worlds[1]

    cplayer_address = 0x8099FFAA

    executor = AsyncMock()
    executor.perform_memory_operations.side_effect = lambda ops: {
        ops[0]:
        expected_world.extra["asset_id"].to_bytes(8, "big")
        if has_world else b"DEADBEEF",
        ops[1]:
        b"\x01" if has_pending_op else b"\x00",
        ops[2]:
        cplayer_address.to_bytes(4, "big") if has_cplayer else None,
    }

    if correct_vtable:
        vtable_memory_return = connector.version.cplayer_vtable.to_bytes(
            4, "big")
    else:
        vtable_memory_return = b"CAFE"
    executor.perform_single_memory_operation.return_value = vtable_memory_return

    # Run
    actual_has_op, actual_world = await connector.current_game_status(executor)

    # Assert
    if has_world and has_cplayer and correct_vtable:
        assert actual_world is expected_world
    else:
        assert actual_world is None
    assert actual_has_op == has_pending_op
    if has_cplayer:
        executor.perform_single_memory_operation.assert_awaited_once_with(
            MemoryOperation(
                cplayer_address,
                read_byte_count=4,
            ))
    else:
        executor.perform_single_memory_operation.assert_not_awaited()
Example #18
0
    def _write_string_to_game_buffer(self, message: str) -> MemoryOperation:
        overhead_size = 6  # 2 bytes for an extra char to differentiate sizes
        encoded_message = message.encode(
            "utf-16_be")[:self.version.string_display.max_message_size -
                         overhead_size]

        # The game doesn't handle very well a string at the same address with same size being
        # displayed multiple times
        if len(encoded_message) == self._last_message_size:
            encoded_message += b'\x00 '
        self._last_message_size = len(encoded_message)

        # Add the null terminator
        encoded_message += b"\x00\x00"
        if len(encoded_message) & 3:
            # Ensure the size is a multiple of 4
            num_to_align = (len(encoded_message)
                            | 3) - len(encoded_message) + 1
            encoded_message += b"\x00" * num_to_align

        return MemoryOperation(
            self.version.string_display.message_receiver_string_ref,
            write_bytes=encoded_message)
Example #19
0
async def test_perform_single_giant_memory_operation(
        executor: NintendontExecutor):
    executor._socket = MagicMock()
    executor._socket.max_input = 120
    executor._socket.max_output = 100
    executor._socket.max_addresses = 8
    executor._socket.writer.drain = AsyncMock()
    executor._socket.reader.read = AsyncMock(side_effect=[
        b"\x01",
        b"\x01",
    ])

    # Run
    result = await executor.perform_single_memory_operation(
        MemoryOperation(0x1000, write_bytes=b"1" * 200), )

    # Assert
    executor._socket.writer.drain.assert_has_awaits([call(), call()])
    executor._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
    executor._socket.reader.read.assert_has_awaits([call(1024), call(1024)])