Skip to content

Commit 41ab9a2

Browse files
committed
Add potion count check
- Read potion count from memory. Use potion only when count > 0. - Overlay: show "Potions:" value. - Console log: include remaining potions on each use. - Standalone script potion_count.py for testing.
1 parent e8d3cad commit 41ab9a2

File tree

11 files changed

+241
-15
lines changed

11 files changed

+241
-15
lines changed

build_release.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
pyinstaller \
33
--clean \
44
--onefile \
5-
--name="AutoPot-DR-v1.0.1" \
5+
--name="AutoPot-DR-v1.2.0" \
66
--icon=../imgs/icon.ico \
77
--add-data "../imgs;imgs" \
88
--distpath release \

imgs/OverlayOFF.png

319 Bytes
Loading

imgs/OverlayON.png

-8.07 KB
Loading

readme.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Dwarven Realms](https://img.shields.io/badge/Dwarven%20Realms%20Version-5.3.2.0-purple)]()
44
[![Windows](https://img.shields.io/badge/Platform-Windows-blue)]()
55

6-
A game tool for Dwarven Realms that automatically uses potions when HP drops below a configurable threshold. Attaches to the game process via pymem and reads player health data from memory using iterative pointer dereferencing with pointer chain resolution. Memory reading runs in a separate background thread with thread-safe operation. Features a PyQt5 overlay window with transparency and always-on-top behavior, and is also thread-safe for responsiveness.
6+
A game tool for Dwarven Realms that automatically uses potions when HP drops below a configurable threshold and the player has at least one potion. Attaches to the game process via pymem and reads player health and potion count from memory using iterative pointer dereferencing. Memory reading runs in a separate background thread with thread-safe operation. Features a PyQt5 overlay window with transparency and always-on-top behavior and is also thread-safe for responsiveness.
77

88
**⚠️ Warning**: This tool is intended for **offline use only**. While it may work online, use it at your own risk and discretion.
99

@@ -19,12 +19,13 @@ A game tool for Dwarven Realms that automatically uses potions when HP drops bel
1919

2020
<p align="center">
2121
<img src="imgs/OverlayOFF.png" alt="Overlay OFF" width="45%"/>
22-
<img src="imgs/OverlayON.png" alt="Overlay ON" width="43.5%"/>
22+
<img src="imgs/OverlayON.png" alt="Overlay ON" width="45%"/>
2323
</p>
2424

25-
* **Auto Potion**: Automatically triggers a potion when HP falls below a set percentage.
26-
* **Potion log**: The overlay displays a log of recent potion uses, showing HP values and timestamps for each use.
27-
* **Overlay UI**: Movable, lockable PyQt5 overlay showing status, HP, and logs.
25+
* **Auto Potion**: Automatically triggers a potion when HP falls below a set percentage and the player has at least one potion (prevents spamming when out of potions).
26+
* **Potion count display**: The overlay shows the current potion count.
27+
* **Potion log**: The overlay displays a log of recent potion uses, showing HP values and timestamps for each use. The console log for each use also shows remaining potion count.
28+
* **Overlay UI**: Movable, lockable PyQt5 overlay showing status, potions, HP, and logs.
2829
* **Customizable Hotkeys**: Easily change hotkeys for toggling ON/OFF, locking, and closing the overlay.
2930
* **Safe & Configurable**: All settings in a user-friendly config file.
3031

src/config.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class Config:
1717
DEFAULT_MAX_HEALTH_OFFSETS = "0x30,0x940,0x5D0,0x2F0,0x370"
1818
DEFAULT_CURRENT_HEALTH_BASE_OFFSET = "0x064D8FD0"
1919
DEFAULT_CURRENT_HEALTH_OFFSETS = "0x30,0x8C8,0xB0,0x2F0,0x368"
20+
DEFAULT_POTION_BASE_OFFSET = "0x064D8FD0"
21+
DEFAULT_POTION_OFFSETS = "0x30,0x8A8,0xAC"
2022
DEFAULT_HOTKEY_LOCK = "home"
2123
DEFAULT_HOTKEY_TOGGLE = "insert"
2224
DEFAULT_HOTKEY_CLOSE = "end"
@@ -55,7 +57,9 @@ def _create_default_config(self):
5557
'max_health_base_offset': self.DEFAULT_MAX_HEALTH_BASE_OFFSET,
5658
'max_health_offsets': self.DEFAULT_MAX_HEALTH_OFFSETS,
5759
'current_health_base_offset': self.DEFAULT_CURRENT_HEALTH_BASE_OFFSET,
58-
'current_health_offsets': self.DEFAULT_CURRENT_HEALTH_OFFSETS
60+
'current_health_offsets': self.DEFAULT_CURRENT_HEALTH_OFFSETS,
61+
'potion_base_offset': self.DEFAULT_POTION_BASE_OFFSET,
62+
'potion_offsets': self.DEFAULT_POTION_OFFSETS
5963
}
6064

6165
try:
@@ -108,6 +112,16 @@ def get_current_health_offsets(self):
108112
return self.config.get('POINTERCHAINS', 'current_health_offsets',
109113
fallback=self.DEFAULT_CURRENT_HEALTH_OFFSETS)
110114

115+
def get_potion_base_offset(self):
116+
"""Get potion base offset (hex string)."""
117+
return self.config.get('POINTERCHAINS', 'potion_base_offset',
118+
fallback=self.DEFAULT_POTION_BASE_OFFSET)
119+
120+
def get_potion_offsets(self):
121+
"""Get potion offsets as comma-separated hex string."""
122+
return self.config.get('POINTERCHAINS', 'potion_offsets',
123+
fallback=self.DEFAULT_POTION_OFFSETS)
124+
111125
def get_hotkey_lock(self):
112126
"""Get hotkey for lock/unlock overlay."""
113127
return self.config.get('KEYBINDS', 'hotkey_lock',

src/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ def set_enabled_wrapper(enabled):
317317
# Connect current health signal to overlay
318318
memory_reader.current_health_updated.connect(overlay.set_current_health)
319319

320+
# Connect potion count signal to overlay
321+
memory_reader.potion_count_updated.connect(overlay.set_potion_count)
322+
320323
# Register hotkeys (will print hotkeys inside register_hotkeys())
321324
hotkey_manager = HotkeyManager(overlay, app, config)
322325
hotkey_manager.register_hotkeys()

src/memory_reader.py

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ class MemoryReader(QObject):
223223
process_attached = pyqtSignal()
224224
# Signal emitted when process death is detected
225225
process_died = pyqtSignal()
226+
# Signal emitted when potion count is read (-1 for read failure)
227+
potion_count_updated = pyqtSignal(int)
226228

227229
def __init__(self, config, process_name, potion_key="r"):
228230
"""
@@ -252,6 +254,8 @@ def __init__(self, config, process_name, potion_key="r"):
252254
self._process_id = None
253255
self._last_max_health_chain = None
254256
self._last_current_health_chain = None
257+
self._potion_count_initialized = False
258+
self._last_potion_chain = None
255259
self._attachment_notified = False # Track if we've notified about current attachment
256260
self._last_chain_resolution_attempt = 0.0
257261
self._chain_resolution_cooldown = 1.0 # 1 second cooldown for chain resolution
@@ -273,9 +277,11 @@ def set_process_running(self, running: bool):
273277
self._module_base = None
274278
self._max_health_initialized = False
275279
self._current_health_initialized = False
280+
self._potion_count_initialized = False
276281
self._process_id = None
277282
self._last_max_health_chain = None
278283
self._last_current_health_chain = None
284+
self._last_potion_chain = None
279285
self._attachment_notified = False # Reset so we can notify again on next attachment
280286

281287
def start(self):
@@ -309,9 +315,11 @@ def _handle_process_death(self):
309315
self._module_base = None
310316
self._max_health_initialized = False
311317
self._current_health_initialized = False
318+
self._potion_count_initialized = False
312319
self._process_id = None
313320
self._last_max_health_chain = None
314321
self._last_current_health_chain = None
322+
self._last_potion_chain = None
315323
self._attachment_notified = False
316324
if self._process_running:
317325
self.process_died.emit()
@@ -551,6 +559,69 @@ def _initialize_current_health_pointer(self):
551559
except Exception as e:
552560
print(f"[ERROR] Error initializing current health pointer: {e}")
553561

562+
def _initialize_potion_pointer(self):
563+
"""
564+
Initialize and print potion pointer debug information once.
565+
Prints all debug info when first successfully reading potion count.
566+
"""
567+
if self._potion_count_initialized:
568+
return
569+
570+
current_time = time.time()
571+
if current_time - self._last_chain_resolution_attempt < self._chain_resolution_cooldown:
572+
return
573+
self._last_chain_resolution_attempt = current_time
574+
575+
try:
576+
if self._pm is None or self._module_base is None:
577+
return
578+
579+
print("Potion pointer:")
580+
print(f"[OK] Module: {self.process_name} | Base address: {hex(self._module_base)}")
581+
582+
base_offset_str = self.config.get_potion_base_offset()
583+
offsets_str = self.config.get_potion_offsets()
584+
585+
base_offset = parse_address(base_offset_str)
586+
if base_offset == 0:
587+
print("[ERROR] Invalid potion base offset")
588+
return
589+
590+
offsets = parse_offsets(offsets_str)
591+
if not offsets:
592+
print("[ERROR] Invalid potion offsets")
593+
return
594+
595+
base_address = self._module_base + base_offset
596+
597+
try:
598+
final_address, chain_addresses = read_pointer_chain(self._pm, base_address, offsets, return_chain=True)
599+
600+
current_pointer_path = tuple(chain_addresses[:-1])
601+
if self._last_potion_chain != current_pointer_path:
602+
print("[DEBUG] Potion pointer chain resolved:")
603+
print(f" Step 0: addr=0x{chain_addresses[0]:X}")
604+
for i, (addr, offset) in enumerate(zip(chain_addresses[1:], offsets), 1):
605+
print(f" Step {i}: addr=0x{addr:X} offset=0x{offset:X}")
606+
self._last_potion_chain = current_pointer_path
607+
608+
print(f"[OK] Final address: {hex(final_address)}")
609+
except RuntimeError as e:
610+
print(f"[ERROR] {e}")
611+
return
612+
613+
try:
614+
potion_count = read_memory_int(self._pm, final_address)
615+
if potion_count >= 0:
616+
print(f"[RESULT] Player Potion Count: {potion_count}")
617+
self._potion_count_initialized = True
618+
print("-------------")
619+
except Exception as e:
620+
print(f"[ERROR] Failed to read final int value: {e}")
621+
622+
except Exception as e:
623+
print(f"[ERROR] Error initializing potion pointer: {e}")
624+
554625
def _read_current_health(self) -> float:
555626
"""
556627
Read current health using pointer chain.
@@ -605,14 +676,46 @@ def _read_current_health(self) -> float:
605676

606677
def _read_potion_count(self) -> int:
607678
"""
608-
Placeholder for reading potion count.
609-
Currently returns 0 and does not affect the program.
679+
Read potion count using pointer chain.
610680
611681
Returns:
612-
Potion count (int)
682+
Potion count (int >= 0) on success, -1 on read failure
613683
"""
614-
# Placeholder - not implemented yet
615-
return 0
684+
try:
685+
if self._pm is None or self._module_base is None:
686+
return -1
687+
688+
if not self._potion_count_initialized:
689+
self._initialize_potion_pointer()
690+
691+
base_offset_str = self.config.get_potion_base_offset()
692+
offsets_str = self.config.get_potion_offsets()
693+
694+
base_offset = parse_address(base_offset_str)
695+
if base_offset == 0:
696+
return -1
697+
698+
offsets = parse_offsets(offsets_str)
699+
if not offsets:
700+
return -1
701+
702+
base_address = self._module_base + base_offset
703+
704+
final_address = read_pointer_chain(self._pm, base_address, offsets)
705+
706+
potion_count = read_memory_int(self._pm, final_address)
707+
if potion_count < 0:
708+
return -1
709+
return potion_count
710+
except RuntimeError:
711+
return -1
712+
except Exception as e:
713+
if not self._potion_count_initialized:
714+
current_time = time.time()
715+
if current_time - self._last_error_print_time >= self._error_print_cooldown:
716+
print(f"[ERROR] Error reading potion count: {e}")
717+
self._last_error_print_time = current_time
718+
return -1
616719

617720
def _use_potion(self):
618721
"""Send potion keypress. Ensures game window is focused first."""
@@ -661,14 +764,18 @@ def _reading_loop(self):
661764
# Emit signal for overlay to update display
662765
self.current_health_updated.emit(current_health)
663766

767+
# Read potion count using pointer chain
768+
potion_count = self._read_potion_count()
769+
self.potion_count_updated.emit(potion_count)
770+
664771
# Potion logic
665-
if max_health > 0 and current_health >= 0:
772+
if max_health > 0 and current_health >= 0 and potion_count > 0:
666773
threshold_percentage = self.config.get_health_threshold()
667774
threshold_value = (max_health * threshold_percentage) / 100.0
668775
current_time = time.time()
669776
time_since_last_potion = current_time - self._last_potion_time
670777

671-
# Check if health is below threshold
778+
# Check if health is below threshold and player has potions
672779
if current_health < threshold_value:
673780
# Potion logic: wait 500ms between potion drinks
674781
if time_since_last_potion >= self._potion_cooldown:
@@ -682,7 +789,8 @@ def _reading_loop(self):
682789
# Print to console with same format as overlay
683790
timestamp = datetime.now().strftime("%H:%M:%S")
684791
health_amount = int(current_health)
685-
print(f"[LOG] {timestamp} - {health_amount} - {health_percentage:.1f}%")
792+
remaining = potion_count - 1
793+
print(f"[LOG] {timestamp} - {health_amount} - {health_percentage:.1f}% - {remaining} potions remaining")
686794

687795
# Check every 10ms
688796
time.sleep(self.MEMORY_READ_INTERVAL)

src/overlay.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(self, config, parent=None):
3131
self._potion_log: List[Dict] = [] # List of {timestamp, health_amount, percentage}
3232
self._current_health = 0.0
3333
self._max_health = 0.0
34+
self._potion_count = -1 # Sentinel for unknown/failed read
3435

3536
self._init_ui()
3637
self._setup_window_properties()
@@ -48,6 +49,11 @@ def _init_ui(self):
4849
self._update_status_display()
4950
layout.addWidget(self.status_label)
5051

52+
# Potions label
53+
self.potions_label = QLabel("Potions: --")
54+
self.potions_label.setStyleSheet("font-size: 12px; color: #CCCCCC; background-color: transparent;")
55+
layout.addWidget(self.potions_label)
56+
5157
# Combined health label
5258
self.health_label = QLabel("Health: -- | --")
5359
self.health_label.setStyleSheet("font-size: 12px; color: #CCCCCC; background-color: transparent;")
@@ -207,6 +213,23 @@ def set_max_health(self, max_health: float):
207213
self._max_health = max_health if max_health > 0 else 0.0
208214
self._update_health_display()
209215

216+
def _update_potion_display(self):
217+
"""Update the potions label."""
218+
if self._potion_count < 0:
219+
self.potions_label.setText("Potions: --")
220+
else:
221+
self.potions_label.setText(f"Potions: {self._potion_count}")
222+
223+
def set_potion_count(self, value: int):
224+
"""
225+
Update potion count display.
226+
227+
Args:
228+
value: Potion count (>= 0) or sentinel (< 0) for read failure
229+
"""
230+
self._potion_count = value
231+
self._update_potion_display()
232+
210233
def add_potion_log_entry(self, health_amount: float, percentage: float):
211234
"""
212235
Add entry to potion log.

0 commit comments

Comments
 (0)