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
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
class EldenClient(SiphonClient):
    """
    Client for the Elden Ring game.
    """

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

    ## =========== Initialization methods ===========

    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 response dictionaries

        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)

        # pysiphon's set_process_config reads the TOML file directly
        print(f"Loading config from: {resolved_path}")
        print("Sending configuration to server...")
        config_response = self.set_process_config(str(resolved_path))
        results["config"] = config_response

        if not config_response.get("success", False):
            raise RuntimeError(
                f"Failed to set process config: {config_response.get('message', 'Unknown error')}"
            )

        print(f"Server response: {config_response.get('message', 'Success')}")

        # 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.get("success", False):
            raise RuntimeError(
                f"Failed to initialize memory: {memory_response.get('message', 'Unknown error')}"
            )

        print(f"Server response: {memory_response.get('message', 'Success')}")
        if memory_response.get("process_id", 0) > 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.get("success", False):
            raise RuntimeError(
                f"Failed to initialize input: {input_response.get('message', 'Unknown error')}"
            )

        print(f"Server response: {input_response.get('message', 'Success')}")

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

        if not capture_response.get("success", False):
            raise RuntimeError(
                f"Failed to initialize capture: {capture_response.get('message', 'Unknown error')}"
            )

        print(f"Server response: {capture_response.get('message', 'Success')}")
        window_width = capture_response.get("window_width", 0)
        window_height = capture_response.get("window_height", 0)
        if window_width > 0 and window_height > 0:
            print(f"Window size: {window_width}x{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=r"C:\Program Files (x86)\Steam\steamapps\common\ELDEN RING\Game",
        )
        if not launch_response.get("success", False):
            raise RuntimeError(
                f"Failed to launch game: {launch_response.get('message', 'Unknown error')}"
            )

        print(f"Server response: {launch_response.get('message', 'Success')}")
        if launch_response.get("process_id", 0) > 0:
            print(f"Process ID: {launch_response['process_id']}")

        return launch_response

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

        Returns:
            int: Current HP value
        """
        response = self.get_attribute("HeroHp")
        return response.get("value", 0) if isinstance(response, dict) else response

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

        Returns:
            int: Maximum HP value
        """
        response = self.get_attribute("HeroMaxHp")
        return response.get("value", 0) if isinstance(response, dict) else response

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

        Args:
            hp (int): HP value to set
        """
        self.set_attribute("HeroHp", hp, "int")

    @property
    def local_player_coords(self):
        """
        Get the local coordinates of the player.

        Returns:
            tuple: (x, y, z) local coordinates
        """
        local_x = self.get_attribute("HeroLocalPosX")
        local_y = self.get_attribute("HeroLocalPosY")
        local_z = self.get_attribute("HeroLocalPosZ")

        # Extract values from dict responses
        local_x = local_x.get("value", 0.0) if isinstance(local_x, dict) else local_x
        local_y = local_y.get("value", 0.0) if isinstance(local_y, dict) else local_y
        local_z = local_z.get("value", 0.0) if isinstance(local_z, dict) else local_z

        return local_x, local_y, local_z

    @property
    def global_player_coords(self):
        """
        Get the global coordinates of the player.

        Returns:
            tuple: (x, y, z) global coordinates
        """
        global_x = self.get_attribute("HeroGlobalPosX")
        global_y = self.get_attribute("HeroGlobalPosY")
        global_z = self.get_attribute("HeroGlobalPosZ")

        # Extract values from dict responses
        global_x = (
            global_x.get("value", 0.0) if isinstance(global_x, dict) else global_x
        )
        global_y = (
            global_y.get("value", 0.0) if isinstance(global_y, dict) else global_y
        )
        global_z = (
            global_z.get("value", 0.0) if isinstance(global_z, dict) else global_z
        )

        return global_x, global_y, global_z

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

        Returns:
            int: Animation ID
        """
        response = self.get_attribute("HeroAnimId")
        return response.get("value", 0) if isinstance(response, dict) else response

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

        Returns:
            int: Current target HP value
        """
        response = self.get_attribute("NpcHp")
        return response.get("value", 0) if isinstance(response, dict) else response

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

        Returns:
            int: Maximum target HP value
        """
        response = self.get_attribute("NpcMaxHp")
        return response.get("value", 0) if isinstance(response, dict) else response

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

        Args:
            hp (int): HP value to set
        """
        self.set_attribute("NpcHp", hp, "int")

    @property
    def local_target_coords(self):
        """
        Get the local coordinates of the target.

        Returns:
            tuple: (x, y, z) local coordinates
        """
        local_x = self.get_attribute("NpcLocalPosX")
        local_y = self.get_attribute("NpcLocalPosY")
        local_z = self.get_attribute("NpcLocalPosZ")

        # Extract values from dict responses
        local_x = local_x.get("value", 0.0) if isinstance(local_x, dict) else local_x
        local_y = local_y.get("value", 0.0) if isinstance(local_y, dict) else local_y
        local_z = local_z.get("value", 0.0) if isinstance(local_z, dict) else local_z

        return local_x, local_y, local_z

    @property
    def global_target_coords(self):
        """
        Get the global coordinates of the target.

        Returns:
            tuple: (x, y, z) global coordinates
        """
        global_x = self.get_attribute("NpcGlobalPosX")
        global_y = self.get_attribute("NpcGlobalPosY")
        global_z = self.get_attribute("NpcGlobalPosZ")

        # Extract values from dict responses
        global_x = (
            global_x.get("value", 0.0) if isinstance(global_x, dict) else global_x
        )
        global_y = (
            global_y.get("value", 0.0) if isinstance(global_y, dict) else global_y
        )
        global_z = (
            global_z.get("value", 0.0) if isinstance(global_z, dict) else global_z
        )

        return global_x, global_y, global_z

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

        Returns:
            int: Animation ID
        """
        response = self.get_attribute("NpcAnimId")
        return response.get("value", 0) if isinstance(response, dict) else response

    ## =========== 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 global coordinates.

        Takes target global coords, computes delta, writes to local coords.

        Args:
            x (float): Target global X coordinate
            y (float): Target global Y coordinate
            z (float): Target global Z coordinate
        """
        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), "float")
        self.set_attribute("HeroLocalPosY", local_y + (y - global_y), "float")
        self.set_attribute("HeroLocalPosZ", local_z + (z - global_z), "float")

    def teleport_to(self, x, y, z):
        """Alias for teleport(). Teleport player to global coordinates."""
        self.teleport(x, y, z)

    def set_game_speed(self, speed):
        """
        Set the game speed.

        Args:
            speed (float): Game speed multiplier (e.g., 0.5 for half speed, 2.0 for double speed)
        """
        self.set_attribute("gameSpeedFlag", True, "bool")
        self.set_attribute("gameSpeedVal", speed, "float")

    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.input_key_tap(["W", "E"], 200, 200)
        sleep(2)
        self.input_key_tap(["B"], 200)

    ## =========== Save File Management ===========
    def copy_save_file(self, save_file_name, save_file_dir=None, timeout_seconds=10):
        """
        Copy a backup save file to become the active save file.

        """
        import os

        # Use default Elden Ring save directory if not provided
        if save_file_dir is None:
            # Default path - user will need to replace with their Steam ID
            save_file_dir = os.path.join(
                os.getenv("APPDATA"),
                "EldenRing",
                # Note: Need to append the actual Steam ID subdirectory
            )
            print(f"[Warning] Using default save directory: {save_file_dir}")
            print(
                "[Warning] You may need to specify the full path including your Steam ID"
            )

        source_path = os.path.join(save_file_dir, save_file_name)
        dest_path = os.path.join(save_file_dir, "ER0000.sl2")

        print("Copying save file:")
        print(f"  Source: {source_path}")
        print(f"  Dest:   {dest_path}")

        # Use PowerShell to copy the file
        result = self.execute_command(
            "powershell",
            args=[
                "-Command",
                f'Copy-Item -Path "{source_path}" -Destination "{dest_path}" -Force',
            ],
            timeout_seconds=timeout_seconds,
            capture_output=True,
        )

        if not result.get("success", False):
            raise RuntimeError(
                f"Failed to copy save file: {result.get('message', 'Unknown error')}"
            )

        print("✓ Save file copied successfully")
        sleep(2.0)  # Wait for filesystem to sync

        return result

    ## =========== Menu methods ===========
    def enter_menu(self):
        """
        Enter the menu.
        """
        self.input_key_tap(["ENTER"], 200, 0)
        sleep(1)
        self.input_key_tap(["ENTER"], 200, 0)
        sleep(1)
        self.input_key_tap(["ENTER"], 200, 0)
        sleep(10)

    def quit_to_title(self):
        """
        Quit the game to the title screen.
        """
        self.input_key_tap(["ESC"])
        sleep(0.3)

        # Navigate menu (UP_ARROW, E)
        self.input_key_tap(["UP_ARROW"])
        sleep(0.3)
        self.input_key_tap(["E"])
        sleep(0.3)

        # Confirm quit (Z, E, LEFT_ARROW, E)
        self.input_key_tap(["Z"])
        sleep(0.3)
        self.input_key_tap(["E"])
        sleep(0.3)
        self.input_key_tap(["LEFT_ARROW"])
        sleep(0.3)
        self.input_key_tap(["E"])
        sleep(12.0)
        sleep(5.0)

global_player_coords property

global_player_coords

Get the global coordinates of the player.

Returns:

Name Type Description
tuple

(x, y, z) global coordinates

global_target_coords property

global_target_coords

Get the global coordinates of the target.

Returns:

Name Type Description
tuple

(x, y, z) global coordinates

local_player_coords property

local_player_coords

Get the local coordinates of the player.

Returns:

Name Type Description
tuple

(x, y, z) local coordinates

local_target_coords property

local_target_coords

Get the local coordinates of the target.

Returns:

Name Type Description
tuple

(x, y, z) local coordinates

player_animation_id property

player_animation_id

Get the animation id of the player.

Returns:

Name Type Description
int

Animation ID

player_hp property

player_hp

Get the health of the player.

Returns:

Name Type Description
int

Current HP value

player_max_hp property

player_max_hp

Get the maximum health of the player.

Returns:

Name Type Description
int

Maximum HP value

target_animation_id property

target_animation_id

Get the animation id of the target.

Returns:

Name Type Description
int

Animation ID

target_hp property

target_hp

Get the health of the target.

Returns:

Name Type Description
int

Current target HP value

target_max_hp property

target_max_hp

Get the maximum health of the target.

Returns:

Name Type Description
int

Maximum target HP value

target_player_distance property

target_player_distance

Get the distance between the player and the target.

copy_save_file

copy_save_file(
    save_file_name, save_file_dir=None, timeout_seconds=10
)

Copy a backup save file to become the active save file.

Source code in eldengym/client/elden_client.py
def copy_save_file(self, save_file_name, save_file_dir=None, timeout_seconds=10):
    """
    Copy a backup save file to become the active save file.

    """
    import os

    # Use default Elden Ring save directory if not provided
    if save_file_dir is None:
        # Default path - user will need to replace with their Steam ID
        save_file_dir = os.path.join(
            os.getenv("APPDATA"),
            "EldenRing",
            # Note: Need to append the actual Steam ID subdirectory
        )
        print(f"[Warning] Using default save directory: {save_file_dir}")
        print(
            "[Warning] You may need to specify the full path including your Steam ID"
        )

    source_path = os.path.join(save_file_dir, save_file_name)
    dest_path = os.path.join(save_file_dir, "ER0000.sl2")

    print("Copying save file:")
    print(f"  Source: {source_path}")
    print(f"  Dest:   {dest_path}")

    # Use PowerShell to copy the file
    result = self.execute_command(
        "powershell",
        args=[
            "-Command",
            f'Copy-Item -Path "{source_path}" -Destination "{dest_path}" -Force',
        ],
        timeout_seconds=timeout_seconds,
        capture_output=True,
    )

    if not result.get("success", False):
        raise RuntimeError(
            f"Failed to copy save file: {result.get('message', 'Unknown error')}"
        )

    print("✓ Save file copied successfully")
    sleep(2.0)  # Wait for filesystem to sync

    return result

enter_menu

enter_menu()

Enter the menu.

Source code in eldengym/client/elden_client.py
def enter_menu(self):
    """
    Enter the menu.
    """
    self.input_key_tap(["ENTER"], 200, 0)
    sleep(1)
    self.input_key_tap(["ENTER"], 200, 0)
    sleep(1)
    self.input_key_tap(["ENTER"], 200, 0)
    sleep(10)

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=r"C:\Program Files (x86)\Steam\steamapps\common\ELDEN RING\Game",
    )
    if not launch_response.get("success", False):
        raise RuntimeError(
            f"Failed to launch game: {launch_response.get('message', 'Unknown error')}"
        )

    print(f"Server response: {launch_response.get('message', 'Success')}")
    if launch_response.get("process_id", 0) > 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 response dictionaries

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 response dictionaries

    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)

    # pysiphon's set_process_config reads the TOML file directly
    print(f"Loading config from: {resolved_path}")
    print("Sending configuration to server...")
    config_response = self.set_process_config(str(resolved_path))
    results["config"] = config_response

    if not config_response.get("success", False):
        raise RuntimeError(
            f"Failed to set process config: {config_response.get('message', 'Unknown error')}"
        )

    print(f"Server response: {config_response.get('message', 'Success')}")

    # 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.get("success", False):
        raise RuntimeError(
            f"Failed to initialize memory: {memory_response.get('message', 'Unknown error')}"
        )

    print(f"Server response: {memory_response.get('message', 'Success')}")
    if memory_response.get("process_id", 0) > 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.get("success", False):
        raise RuntimeError(
            f"Failed to initialize input: {input_response.get('message', 'Unknown error')}"
        )

    print(f"Server response: {input_response.get('message', 'Success')}")

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

    if not capture_response.get("success", False):
        raise RuntimeError(
            f"Failed to initialize capture: {capture_response.get('message', 'Unknown error')}"
        )

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

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

    return results

quit_to_title

quit_to_title()

Quit the game to the title screen.

Source code in eldengym/client/elden_client.py
def quit_to_title(self):
    """
    Quit the game to the title screen.
    """
    self.input_key_tap(["ESC"])
    sleep(0.3)

    # Navigate menu (UP_ARROW, E)
    self.input_key_tap(["UP_ARROW"])
    sleep(0.3)
    self.input_key_tap(["E"])
    sleep(0.3)

    # Confirm quit (Z, E, LEFT_ARROW, E)
    self.input_key_tap(["Z"])
    sleep(0.3)
    self.input_key_tap(["E"])
    sleep(0.3)
    self.input_key_tap(["LEFT_ARROW"])
    sleep(0.3)
    self.input_key_tap(["E"])
    sleep(12.0)
    sleep(5.0)

set_game_speed

set_game_speed(speed)

Set the game speed.

Parameters:

Name Type Description Default
speed float

Game speed multiplier (e.g., 0.5 for half speed, 2.0 for double speed)

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

    Args:
        speed (float): Game speed multiplier (e.g., 0.5 for half speed, 2.0 for double speed)
    """
    self.set_attribute("gameSpeedFlag", True, "bool")
    self.set_attribute("gameSpeedVal", speed, "float")

set_player_hp

set_player_hp(hp)

Set the health of the player.

Parameters:

Name Type Description Default
hp int

HP value to set

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

    Args:
        hp (int): HP value to set
    """
    self.set_attribute("HeroHp", hp, "int")

set_target_hp

set_target_hp(hp)

Set the health of the target.

Parameters:

Name Type Description Default
hp int

HP value to set

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

    Args:
        hp (int): HP value to set
    """
    self.set_attribute("NpcHp", hp, "int")

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.input_key_tap(["W", "E"], 200, 200)
    sleep(2)
    self.input_key_tap(["B"], 200)

teleport

teleport(x, y, z)

Teleport the player to the given global coordinates.

Takes target global coords, computes delta, writes to local coords.

Parameters:

Name Type Description Default
x float

Target global X coordinate

required
y float

Target global Y coordinate

required
z float

Target global Z coordinate

required
Source code in eldengym/client/elden_client.py
def teleport(self, x, y, z):
    """
    Teleport the player to the given global coordinates.

    Takes target global coords, computes delta, writes to local coords.

    Args:
        x (float): Target global X coordinate
        y (float): Target global Y coordinate
        z (float): Target global Z coordinate
    """
    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), "float")
    self.set_attribute("HeroLocalPosY", local_y + (y - global_y), "float")
    self.set_attribute("HeroLocalPosZ", local_z + (z - global_z), "float")

teleport_to

teleport_to(x, y, z)

Alias for teleport(). Teleport player to global coordinates.

Source code in eldengym/client/elden_client.py
def teleport_to(self, x, y, z):
    """Alias for teleport(). Teleport player to global coordinates."""
    self.teleport(x, y, 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: - dict: Response from execute_command with keys 'success', 'message', 'process_id'

Player Methods

Properties

All properties now return clean values (not dict responses):

player_hp

Get player's current HP.

hp = client.player_hp  # Returns: int (e.g., 1317)
print(f"HP: {hp}")

player_max_hp

Get player's maximum HP.

Returns: int

local_player_coords

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

x, y, z = client.local_player_coords  # Returns: tuple[float, float, float]

global_player_coords

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

Returns: tuple[float, float, float]

player_animation_id

Get current player animation ID.

Returns: int

Methods

set_player_hp(hp)

Set player's HP. Note: set_attribute now requires a data type parameter.

client.set_player_hp(1000)  # Set HP to 1000
# Internally calls: set_attribute("HeroHp", 1000, "int")

teleport(x, y, z)

Teleport player to coordinates.

client.teleport(100.0, 200.0, 50.0)
# Internally uses: set_attribute("HeroLocalPosX", value, "float")

Target/Boss Methods

Properties

All properties now return clean values:

target_hp

Get target's current HP.

Returns: int (e.g., 167)

hp = client.target_hp  # Returns: 167 (not a dict)

target_max_hp

Get target's maximum HP.

Returns: int

local_target_coords

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

Returns: tuple[float, float, float]

global_target_coords

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

Returns: tuple[float, float, float]

target_animation_id

Get current target animation ID.

Returns: int

Methods

set_target_hp(hp)

Set target's HP.

client.set_target_hp(500)  # Set boss HP to 500
# Internally calls: set_attribute("NpcHp", 500, "int")

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.

Args: - speed (float): Game speed multiplier (e.g., 0.5 for half speed, 2.0 for double speed)

client.set_game_speed(2.0)  # 2x speed
client.set_game_speed(0.5)  # Half speed
# Internally uses: set_attribute("gameSpeedFlag", True, "bool")
#                  set_attribute("gameSpeedVal", speed, "float")

start_scenario(scenario_name)

Start a boss fight scenario by teleporting to fog wall and entering.

Args: - scenario_name (str): Name of scenario from client.scenarios dict (e.g., "Margit-v0")

client.start_scenario("Margit-v0")

Save File Management

copy_save_file(save_file_name, save_file_dir=None, timeout_seconds=10)

Copy a backup save file to become the active save file. Useful for resetting to a specific game state.

Args: - save_file_name (str): Name of backup save file (e.g., "margit_checkpoint.sl2") - save_file_dir (str, optional): Directory containing save files. Defaults to %APPDATA%/EldenRing/ - timeout_seconds (int): Timeout for copy operation (default: 10)

Returns: - dict: Response from execute_command

Example:

save_dir = r"C:\Users\YourName\AppData\Roaming\EldenRing\76561198012345678"
client.copy_save_file("margit_checkpoint.sl2", save_file_dir=save_dir)

enter_menu()

Enter the game from title screen.

client.enter_menu()  # Presses ENTER three times with delays

quit_to_title()

Quit to title screen from in-game.

client.quit_to_title()  # ESC → navigate menu → confirm quit

Low-Level Methods

These methods are inherited from SiphonClient (via pysiphon) and provide direct game control:

  • input_key_tap(keys, hold_ms=100, delay_ms=0) - Send keyboard input
  • move_mouse(delta_x, delta_y, steps=1) - Move mouse
  • input_key_toggle(key, pressed) - Press/release key (for persistent input)
  • get_attribute(name) - Read memory value (returns dict with 'value' key)
  • set_attribute(name, value, value_type) - Write memory value (requires type: "int", "float", "bool")
  • start_frame_stream(format, quality) - Start non-blocking frame stream
  • get_latest_frame(handle) - Poll latest frame from stream
  • execute_command(...) - Execute system command

Important: All memory read/write operations now use dictionary responses:

# Reading attributes (returns dict)
response = client.get_attribute("HeroHp")
hp = response["value"]  # Extract the value

# Writing attributes (requires type)
client.set_attribute("HeroHp", 1000, "int")
client.set_attribute("gameSpeedVal", 2.0, "float")
client.set_attribute("gameSpeedFlag", True, "bool")

See SiphonClient API for details.

Example: Complete Workflow

from eldengym.client.elden_client import EldenClient
import time

# 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 (properties now return clean values)
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.input_key_tap(["W"], 500)  # Move forward for 500ms
client.input_key_tap(["SPACE"], 100)  # Jump

# Start frame streaming
handle = client.start_frame_stream(format="jpeg", quality=85)
time.sleep(0.1)  # Wait for first frame

# Get latest frame
frame_data = client.get_latest_frame(handle)
if frame_data:
    print(f"Frame size: {len(frame_data.data)} bytes")
    print(f"Frame dimensions: {frame_data.width}x{frame_data.height}")

# Stop stream
client.stop_frame_stream(handle)

# Clean up
client.close()