Skip to content

EldenClient API

The EldenClient class provides game-specific functionality for Elden Ring.

EldenClient

EldenClient

Bases: SiphonClient

Client for the Elden Ring game.

Source code in eldengym/client/elden_client.py
class EldenClient(SiphonClient):
    """
    Client for the Elden Ring game.
    """

    def __init__(self, host="localhost:50051", **kwargs):
        super().__init__(host, **kwargs)
        self.scenarios = {
            "margit": {
                "boss_name": "Margit",
                "fog_wall_location": (
                    19.958229064941406,
                    -11.990748405456543,
                    -7.051832675933838,
                ),
            }
        }

    ## =========== Initialization methods ===========
    # def load_config_from_file(self, config_filepath):
    #     """
    #     Load process configuration from a TOML file and send to server.

    #     This is a convenience method that parses the config file and calls
    #     set_process_config with the extracted data.

    #     Args:
    #         config_filepath: str or Path, path to TOML config file

    #     Returns:
    #         SetProcessConfigResponse from server

    #     Raises:
    #         FileNotFoundError: If config file doesn't exist
    #         ValueError: If config file is malformed

    #     Example:
    #         >>> client = EldenClient()
    #         >>> response = client.load_config_from_file("config.toml")
    #         >>> if response.success:
    #         ...     print("Config loaded successfully!")
    #     """
    #     process_name, process_window_name, attributes = parse_config_file(config_filepath)
    #     return self.set_process_config(process_name, process_window_name, attributes)

    def _resolve_config_path(self, config_filepath):
        """
        Resolve config file path relative to package root.

        Args:
            config_filepath: str or Path, can be:
                - Absolute path: used as-is
                - Relative path: resolved relative to package root (eldengym/)
                - Filename only: looked up in eldengym/files/configs/

        Returns:
            Path: Resolved absolute path to config file
        """
        config_path = Path(config_filepath)

        # If absolute path, use it directly
        if config_path.is_absolute():
            return config_path

        # Get package root (eldengym/)
        package_root = Path(__file__).parent.parent

        # If it's just a filename (no directory parts), look in configs directory
        if len(config_path.parts) == 1:
            config_path = package_root / "files" / "configs" / config_path
        else:
            # Relative path - resolve from package root
            config_path = package_root / config_path

        return config_path.resolve()

    def load_config_from_file(self, config_filepath, wait_time=2):
        """
        Complete initialization sequence: load config, initialize memory, input, and capture.

        This is a convenience method that performs all initialization steps at once,
        mirroring the 'init' command from the C++ client.

        Args:
            config_filepath: str or Path, path to TOML config file. Can be:
                - Absolute path: /full/path/to/config.toml
                - Relative to package: files/configs/ER_1_16_1.toml
                - Filename only: ER_1_16_1.toml (searches in eldengym/files/configs/)
            wait_time: int, seconds to wait after loading config before initializing subsystems

        Returns:
            dict with keys 'config', 'memory', 'input', 'capture' containing the responses

        Raises:
            FileNotFoundError: If config file doesn't exist
            ValueError: If config file is malformed
            RuntimeError: If any initialization step fails

        Example:
            >>> client = EldenClient()
            >>> # All these work from any directory:
            >>> results = client.load_config_from_file("ER_1_16_1.toml")
            >>> results = client.load_config_from_file("files/configs/ER_1_16_1.toml")
            >>> results = client.load_config_from_file("/absolute/path/to/config.toml")
        """
        import time

        results = {}

        # Resolve config path
        resolved_path = self._resolve_config_path(config_filepath)

        # Parse config file
        print(f"Loading config from: {resolved_path}")
        process_name, process_window_name, attributes = parse_config_file(resolved_path)

        print(
            f"Config loaded - Process: {process_name}, Window: {process_window_name}, "
            f"Attributes: {len(attributes)}"
        )

        # Send config to server
        print("Sending configuration to server...")
        config_response = self.set_process_config(
            process_name, process_window_name, attributes
        )
        results["config"] = config_response

        if not config_response.success:
            raise RuntimeError(
                f"Failed to set process config: {config_response.message}"
            )

        print(f"Server response: {config_response.message}")

        # Wait for process to be ready
        if wait_time > 0:
            print(f"Waiting {wait_time} seconds for process to be ready...")
            time.sleep(wait_time)

        # Initialize memory
        print("Initializing memory subsystem...")
        memory_response = self.initialize_memory()
        results["memory"] = memory_response

        if not memory_response.success:
            raise RuntimeError(
                f"Failed to initialize memory: {memory_response.message}"
            )

        print(f"Server response: {memory_response.message}")
        if hasattr(memory_response, "process_id") and memory_response.process_id > 0:
            print(f"Process ID: {memory_response.process_id}")

        # Initialize input
        print("Initializing input subsystem...")
        input_response = self.initialize_input()
        results["input"] = input_response

        if not input_response.success:
            raise RuntimeError(f"Failed to initialize input: {input_response.message}")

        print(f"Server response: {input_response.message}")

        # Initialize capture
        print("Initializing capture subsystem...")
        capture_response = self.initialize_capture()
        results["capture"] = capture_response

        if not capture_response.success:
            raise RuntimeError(
                f"Failed to initialize capture: {capture_response.message}"
            )

        print(f"Server response: {capture_response.message}")
        if hasattr(capture_response, "window_width") and hasattr(
            capture_response, "window_height"
        ):
            if capture_response.window_width > 0 and capture_response.window_height > 0:
                print(
                    f"Window size: {capture_response.window_width}x{capture_response.window_height}"
                )

        print("\n=== Initialization Complete! ===")
        print("All subsystems initialized successfully.")

        return results

    def launch_game(self):
        """
        Launch the game.
        """
        launch_response = self.execute_command(
            "start_protected_game.exe",
            args=None,
            working_directory="C:\Program Files (x86)\Steam\steamapps\common\ELDEN RING\Game",
        )
        if not launch_response.success:
            raise RuntimeError(f"Failed to launch game: {launch_response.message}")

        print(f"Server response: {launch_response.message}")
        if hasattr(launch_response, "process_id") and launch_response.process_id > 0:
            print(f"Process ID: {launch_response.process_id}")

        return launch_response

    def bypass_menu(self):
        """
        Bypass the menu.
        """
        self.send_key(["ENTER"], 200, 0)
        sleep(1)
        self.send_key(["ENTER"], 200, 0)
        sleep(1)
        self.send_key(["ENTER"], 200, 0)

    ## =========== Player methods ===========
    @property
    def player_hp(self):
        """
        Get the health of the player.
        """
        return self.get_attribute("HeroHp")

    @property
    def player_max_hp(self):
        """
        Get the maximum health of the player.
        """
        return self.get_attribute("HeroMaxHp")

    def set_player_hp(self, hp):
        """
        Set the health of the player.
        """
        self.set_attribute("HeroHp", hp)

    @property
    def local_player_coords(self):
        """
        Get the location of the player.
        """
        local_x = self.get_attribute("HeroLocalPosX")
        local_y = self.get_attribute("HeroLocalPosY")
        local_z = self.get_attribute("HeroLocalPosZ")
        return local_x, local_y, local_z

    @property
    def global_player_coords(self):
        """
        Get the location of the player.
        """
        global_x = self.get_attribute("HeroGlobalPosX")
        global_y = self.get_attribute("HeroGlobalPosY")
        global_z = self.get_attribute("HeroGlobalPosZ")
        return global_x, global_y, global_z

    @property
    def player_animation_id(self):
        """
        Get the animation id of the player.
        """
        return self.get_attribute("HeroAnimId")

    ## =========== Target methods ===========
    @property
    def target_hp(self):
        """
        Get the health of the target.
        """
        return self.get_attribute("NpcHp")

    @property
    def target_max_hp(self):
        """
        Get the maximum health of the target.
        """
        return self.get_attribute("NpcMaxHp")

    def set_target_hp(self, hp):
        """
        Set the health of the target.
        """
        self.set_attribute("NpcHp", hp)

    @property
    def local_target_coords(self):
        """
        Get the location of the target.
        """
        local_x = self.get_attribute("NpcLocalPosX")
        local_y = self.get_attribute("NpcLocalPosY")
        local_z = self.get_attribute("NpcLocalPosZ")
        return local_x, local_y, local_z

    @property
    def global_target_coords(self):
        """
        Get the location of the target.
        """
        global_x = self.get_attribute("NpcGlobalPosX")
        global_y = self.get_attribute("NpcGlobalPosY")
        global_z = self.get_attribute("NpcGlobalPosZ")
        return global_x, global_y, global_z

    @property
    def target_animation_id(self):
        """
        Get the animation id of the target.
        """
        return self.get_attribute("NpcAnimId")

    ## =========== Helper methods ===========
    @property
    def target_player_distance(self):
        """
        Get the distance between the player and the target.
        """
        player_x, player_y, player_z = self.local_player_coords
        target_x, target_y, target_z = self.global_target_coords
        return np.linalg.norm(
            [player_x - target_x, player_y - target_y, player_z - target_z]
        )

    def teleport(self, x, y, z):
        """
        Teleport the player to the given coordinates.
        """
        # FIXME: Close range teleport, need to check MapId for long range teleport.
        local_x, local_y, local_z = self.local_player_coords
        global_x, global_y, global_z = self.global_player_coords
        self.set_attribute("HeroLocalPosX", local_x + (x - global_x))
        self.set_attribute("HeroLocalPosY", local_y + (y - global_y))
        self.set_attribute("HeroLocalPosZ", local_z + (z - global_z))

    def set_game_speed(self, speed):
        """
        Set the game speed.
        """
        self.set_attribute("gameSpeedFlag", True)
        self.set_attribute("gameSpeedVal", speed)

    def reset_game(self):
        """
        Reset the game by setting the player's hp to 0.
        """
        self.set_player_hp(0)
        sleep(
            20
        )  # FIXME: This is a hack to wait for the game to reset, doesn't work well.

    def start_scenario(self, scenario_name="Margit"):
        """
        Start the scenario with the given scenario name.
        """
        # FIXME: This is a hack to start boss fight. Need to check fogwall state. or use another method.
        x, y, z = self.scenarios[scenario_name]["fog_wall_location"]
        self.teleport(x, y, z)
        self.move_mouse(1000, 0, 1)
        sleep(2)
        self.send_key(["W", "E"], 200, 200)
        sleep(2)
        self.send_key(["B"], 200)

global_player_coords property

global_player_coords

Get the location of the player.

global_target_coords property

global_target_coords

Get the location of the target.

local_player_coords property

local_player_coords

Get the location of the player.

local_target_coords property

local_target_coords

Get the location of the target.

player_animation_id property

player_animation_id

Get the animation id of the player.

player_hp property

player_hp

Get the health of the player.

player_max_hp property

player_max_hp

Get the maximum health of the player.

target_animation_id property

target_animation_id

Get the animation id of the target.

target_hp property

target_hp

Get the health of the target.

target_max_hp property

target_max_hp

Get the maximum health of the target.

target_player_distance property

target_player_distance

Get the distance between the player and the target.

bypass_menu

bypass_menu()

Bypass the menu.

Source code in eldengym/client/elden_client.py
def bypass_menu(self):
    """
    Bypass the menu.
    """
    self.send_key(["ENTER"], 200, 0)
    sleep(1)
    self.send_key(["ENTER"], 200, 0)
    sleep(1)
    self.send_key(["ENTER"], 200, 0)

launch_game

launch_game()

Launch the game.

Source code in eldengym/client/elden_client.py
def launch_game(self):
    """
    Launch the game.
    """
    launch_response = self.execute_command(
        "start_protected_game.exe",
        args=None,
        working_directory="C:\Program Files (x86)\Steam\steamapps\common\ELDEN RING\Game",
    )
    if not launch_response.success:
        raise RuntimeError(f"Failed to launch game: {launch_response.message}")

    print(f"Server response: {launch_response.message}")
    if hasattr(launch_response, "process_id") and launch_response.process_id > 0:
        print(f"Process ID: {launch_response.process_id}")

    return launch_response

load_config_from_file

load_config_from_file(config_filepath, wait_time=2)

Complete initialization sequence: load config, initialize memory, input, and capture.

This is a convenience method that performs all initialization steps at once, mirroring the 'init' command from the C++ client.

Parameters:

Name Type Description Default
config_filepath

str or Path, path to TOML config file. Can be: - Absolute path: /full/path/to/config.toml - Relative to package: files/configs/ER_1_16_1.toml - Filename only: ER_1_16_1.toml (searches in eldengym/files/configs/)

required
wait_time

int, seconds to wait after loading config before initializing subsystems

2

Returns:

Type Description

dict with keys 'config', 'memory', 'input', 'capture' containing the responses

Raises:

Type Description
FileNotFoundError

If config file doesn't exist

ValueError

If config file is malformed

RuntimeError

If any initialization step fails

Example

client = EldenClient()

All these work from any directory:

results = client.load_config_from_file("ER_1_16_1.toml") results = client.load_config_from_file("files/configs/ER_1_16_1.toml") results = client.load_config_from_file("/absolute/path/to/config.toml")

Source code in eldengym/client/elden_client.py
def load_config_from_file(self, config_filepath, wait_time=2):
    """
    Complete initialization sequence: load config, initialize memory, input, and capture.

    This is a convenience method that performs all initialization steps at once,
    mirroring the 'init' command from the C++ client.

    Args:
        config_filepath: str or Path, path to TOML config file. Can be:
            - Absolute path: /full/path/to/config.toml
            - Relative to package: files/configs/ER_1_16_1.toml
            - Filename only: ER_1_16_1.toml (searches in eldengym/files/configs/)
        wait_time: int, seconds to wait after loading config before initializing subsystems

    Returns:
        dict with keys 'config', 'memory', 'input', 'capture' containing the responses

    Raises:
        FileNotFoundError: If config file doesn't exist
        ValueError: If config file is malformed
        RuntimeError: If any initialization step fails

    Example:
        >>> client = EldenClient()
        >>> # All these work from any directory:
        >>> results = client.load_config_from_file("ER_1_16_1.toml")
        >>> results = client.load_config_from_file("files/configs/ER_1_16_1.toml")
        >>> results = client.load_config_from_file("/absolute/path/to/config.toml")
    """
    import time

    results = {}

    # Resolve config path
    resolved_path = self._resolve_config_path(config_filepath)

    # Parse config file
    print(f"Loading config from: {resolved_path}")
    process_name, process_window_name, attributes = parse_config_file(resolved_path)

    print(
        f"Config loaded - Process: {process_name}, Window: {process_window_name}, "
        f"Attributes: {len(attributes)}"
    )

    # Send config to server
    print("Sending configuration to server...")
    config_response = self.set_process_config(
        process_name, process_window_name, attributes
    )
    results["config"] = config_response

    if not config_response.success:
        raise RuntimeError(
            f"Failed to set process config: {config_response.message}"
        )

    print(f"Server response: {config_response.message}")

    # Wait for process to be ready
    if wait_time > 0:
        print(f"Waiting {wait_time} seconds for process to be ready...")
        time.sleep(wait_time)

    # Initialize memory
    print("Initializing memory subsystem...")
    memory_response = self.initialize_memory()
    results["memory"] = memory_response

    if not memory_response.success:
        raise RuntimeError(
            f"Failed to initialize memory: {memory_response.message}"
        )

    print(f"Server response: {memory_response.message}")
    if hasattr(memory_response, "process_id") and memory_response.process_id > 0:
        print(f"Process ID: {memory_response.process_id}")

    # Initialize input
    print("Initializing input subsystem...")
    input_response = self.initialize_input()
    results["input"] = input_response

    if not input_response.success:
        raise RuntimeError(f"Failed to initialize input: {input_response.message}")

    print(f"Server response: {input_response.message}")

    # Initialize capture
    print("Initializing capture subsystem...")
    capture_response = self.initialize_capture()
    results["capture"] = capture_response

    if not capture_response.success:
        raise RuntimeError(
            f"Failed to initialize capture: {capture_response.message}"
        )

    print(f"Server response: {capture_response.message}")
    if hasattr(capture_response, "window_width") and hasattr(
        capture_response, "window_height"
    ):
        if capture_response.window_width > 0 and capture_response.window_height > 0:
            print(
                f"Window size: {capture_response.window_width}x{capture_response.window_height}"
            )

    print("\n=== Initialization Complete! ===")
    print("All subsystems initialized successfully.")

    return results

reset_game

reset_game()

Reset the game by setting the player's hp to 0.

Source code in eldengym/client/elden_client.py
def reset_game(self):
    """
    Reset the game by setting the player's hp to 0.
    """
    self.set_player_hp(0)
    sleep(
        20
    )  # FIXME: This is a hack to wait for the game to reset, doesn't work well.

set_game_speed

set_game_speed(speed)

Set the game speed.

Source code in eldengym/client/elden_client.py
def set_game_speed(self, speed):
    """
    Set the game speed.
    """
    self.set_attribute("gameSpeedFlag", True)
    self.set_attribute("gameSpeedVal", speed)

set_player_hp

set_player_hp(hp)

Set the health of the player.

Source code in eldengym/client/elden_client.py
def set_player_hp(self, hp):
    """
    Set the health of the player.
    """
    self.set_attribute("HeroHp", hp)

set_target_hp

set_target_hp(hp)

Set the health of the target.

Source code in eldengym/client/elden_client.py
def set_target_hp(self, hp):
    """
    Set the health of the target.
    """
    self.set_attribute("NpcHp", hp)

start_scenario

start_scenario(scenario_name='Margit')

Start the scenario with the given scenario name.

Source code in eldengym/client/elden_client.py
def start_scenario(self, scenario_name="Margit"):
    """
    Start the scenario with the given scenario name.
    """
    # FIXME: This is a hack to start boss fight. Need to check fogwall state. or use another method.
    x, y, z = self.scenarios[scenario_name]["fog_wall_location"]
    self.teleport(x, y, z)
    self.move_mouse(1000, 0, 1)
    sleep(2)
    self.send_key(["W", "E"], 200, 200)
    sleep(2)
    self.send_key(["B"], 200)

teleport

teleport(x, y, z)

Teleport the player to the given coordinates.

Source code in eldengym/client/elden_client.py
def teleport(self, x, y, z):
    """
    Teleport the player to the given coordinates.
    """
    # FIXME: Close range teleport, need to check MapId for long range teleport.
    local_x, local_y, local_z = self.local_player_coords
    global_x, global_y, global_z = self.global_player_coords
    self.set_attribute("HeroLocalPosX", local_x + (x - global_x))
    self.set_attribute("HeroLocalPosY", local_y + (y - global_y))
    self.set_attribute("HeroLocalPosZ", local_z + (z - global_z))

Initialization Methods

load_config_from_file(config_filepath, wait_time=2)

Complete initialization: load config, initialize memory, input, and capture subsystems.

Args: - config_filepath (str | Path): Path to TOML config file - Filename only: "ER_1_16_1.toml" (searches in eldengym/files/configs/) - Relative path: "files/configs/ER_1_16_1.toml" (from package root) - Absolute path: "/full/path/to/config.toml" - wait_time (int): Seconds to wait after loading config (default: 2)

Returns: - dict: Results with keys 'config', 'memory', 'input', 'capture'

Example:

from eldengym.client.elden_client import EldenClient

client = EldenClient(host="localhost:50051")
results = client.load_config_from_file("ER_1_16_1.toml")

print(f"Initialized: {results['memory'].success}")

launch_game()

Launch Elden Ring game executable.

Returns: - ExecuteCommandResponse: Command execution result

bypass_menu()

Automatically bypass the main menu to load into the game.

Player Methods

Properties

player_hp

Get player's current HP.

hp = client.player_hp
print(f"HP: {hp}")

player_max_hp

Get player's maximum HP.

local_player_coords

Get player's local coordinates (x, y, z).

x, y, z = client.local_player_coords

global_player_coords

Get player's global coordinates (x, y, z).

player_animation_id

Get current player animation ID.

Methods

set_player_hp(hp)

Set player's HP.

client.set_player_hp(1000)  # Set HP to 1000

teleport(x, y, z)

Teleport player to coordinates.

client.teleport(100.0, 200.0, 50.0)

Target/Boss Methods

Properties

target_hp

Get target's current HP.

target_max_hp

Get target's maximum HP.

local_target_coords

Get target's local coordinates (x, y, z).

global_target_coords

Get target's global coordinates (x, y, z).

target_animation_id

Get current target animation ID.

Methods

set_target_hp(hp)

Set target's HP.

client.set_target_hp(500)  # Set boss HP to 500

Helper Methods

target_player_distance

Get distance between player and target.

distance = client.target_player_distance
print(f"Distance to boss: {distance:.2f}")

set_game_speed(speed)

Set game speed multiplier.

client.set_game_speed(2.0)  # 2x speed
client.set_game_speed(0.5)  # Half speed

reset_game()

Reset the game (kills player, triggers death/respawn).

client.reset_game()

start_scenario(scenario_name)

Start a boss fight scenario.

Args: - scenario_name (str): Name of scenario (e.g., "margit")

client.start_scenario("margit")

Low-Level Methods

These methods are inherited from SiphonClient and provide direct game control:

  • send_key(keys, hold_time, delay_time) - Send keyboard input
  • move_mouse(delta_x, delta_y, steps) - Move mouse
  • toggle_key(key, toggle) - Press/release key
  • get_attribute(name) - Read memory value
  • set_attribute(name, value) - Write memory value
  • get_frame() - Capture game frame
  • execute_command(...) - Execute system command

See SiphonClient API for details.

Example: Complete Workflow

from eldengym.client.elden_client import EldenClient

# Create client
client = EldenClient(host="localhost:50051")

# Initialize everything
results = client.load_config_from_file("ER_1_16_1.toml", wait_time=2)

# Get player info
print(f"Player HP: {client.player_hp}/{client.player_max_hp}")
print(f"Boss HP: {client.target_hp}/{client.target_max_hp}")
print(f"Distance: {client.target_player_distance:.2f}")

# Control the game
client.send_key(["W"], 500)  # Move forward for 500ms
client.send_key(["SPACE"], 100)  # Jump

# Capture frame
frame = client.get_frame()
print(f"Frame shape: {frame.shape}")

# Clean up
client.close()