- Sound when condition met on item.
plugin
iSRO-R
v1.3
Wheel of Fate
by @CK3 · 2d ago · updated 6h ago · 12 views
WheelOfFate - plugin for phBot (iSRO-R)
- Automatically uses "Wheel of Fate" on an item and stops when the item has a number of magic (blue) lines >= the value you choose. The VALUES of the bluelines do not matter, only HOW MANY lines there are.
Changelog
v1.2
files only
Released with files, no release notes written yet.
▸ WheelOfFate.py v1.3 Download
# -*- coding: utf-8 -*-
#
# WheelOfFate - plugin for phBot (iSRO-R)
# ----------------------------------------
# Automatically uses "Wheel of Fate" on an item and stops when the item has a
# number of magic (blue) lines >= the value you choose. The VALUES of the blue
# lines do not matter, only HOW MANY lines there are.
#
# Separate from the original AutoWheels. No database (db3) needed.
#
# NOTE: every spin consumes one Wheel of Fate. The first spin on an item always
# happens (the plugin cannot read the current lines without spinning once).
#
from phBot import *
from threading import Timer, Thread
import struct
import QtBind
import os
import json
try:
import winsound
except Exception:
winsound = None
pName = 'WheelOfFate'
pVersion = '1.3'
# wheel use packet (same as AutoWheels): 0x7151
WHEEL_OPCODE = 0x7151
WHEEL_RESULT_OPCODE = 0xB151
# offset of number_of_magiclines inside the 0xB151 response
MAGICLINES_OFFSET = 26
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), pName + '.json')
started = False
_timer = None
current_items = {} # combo index -> inventory slot
attempts = 0
queue = [] # (slot, label) items waiting
history = [] # result lines, newest first
# ============================================================================
# GUI
# ============================================================================
gui = QtBind.init(__name__, pName)
# ---- Title ----
QtBind.createLabel(gui, 'W H E E L O F F A T E v%s' % pVersion, 10, 6)
# ---- Item selection ----
QtBind.createLabel(gui, 'Item to spin:', 10, 30)
combo_items = QtBind.createCombobox(gui, 10, 48, 240, 22)
QtBind.createButton(gui, 'on_refresh', ' Refresh items ', 258, 47)
QtBind.createButton(gui, 'on_add_queue', ' + Queue ', 258, 74)
QtBind.createButton(gui, 'on_clear_queue', ' Clear queue ', 360, 74)
# ---- Queue (right, under Clear queue) ----
QtBind.createLabel(gui, 'Queue (remaining):', 360, 104)
lstQueue = QtBind.createList(gui, 360, 124, 210, 176)
# ---- Settings ----
QtBind.createLabel(gui, 'Stop at blue lines >=:', 10, 102)
txtLines = QtBind.createLineEdit(gui, '4', 210, 100, 40, 20)
QtBind.createLabel(gui, 'Delay between wheels (ms):', 10, 126)
txtDelay = QtBind.createLineEdit(gui, '2000', 210, 124, 50, 20)
QtBind.createButton(gui, 'on_save', ' Save ', 280, 123)
btnStartStop = QtBind.createButton(gui, 'on_startstop', ' Start ', 10, 150)
lblStatus = QtBind.createLabel(gui, 'Status: stopped', 150, 154)
# ---- History (left) : newest line on top, scroll for the older ones ----
QtBind.createLabel(gui, 'History (attempt: lines):', 10, 182)
lstResult = QtBind.createList(gui, 10, 200, 240, 44)
# ---- Sound alert ----
chkSound = QtBind.createCheckBox(gui, 'on_dummy', 'Sound when target reached', 10, 248)
QtBind.createLabel(gui, 'WAV (blank = beep):', 10, 272)
txtWav = QtBind.createLineEdit(gui, '', 130, 270, 215, 20)
QtBind.createButton(gui, 'on_browse_wav', ' Browse ', 130, 296)
QtBind.createButton(gui, 'on_test_sound', ' Test ', 235, 296)
# ---- Footer ----
QtBind.createLabel(gui, 'Author : CK3 @ iLog1K.net', 10, 330)
def on_dummy(*_):
pass
def set_status(txt):
QtBind.setText(gui, lblStatus, 'Status: ' + txt)
def play_alert():
"""Beep / play a WAV when an item reaches the target. Never blocks the bot."""
try:
if not QtBind.isChecked(gui, chkSound):
return
path = QtBind.text(gui, txtWav).strip().strip('"')
except Exception:
return
if winsound is None:
return
def _run():
try:
if path and os.path.isfile(path):
winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC)
else:
for freq, dur in ((880, 150), (1175, 150), (1568, 350)):
winsound.Beep(freq, dur)
except Exception as e:
log('[%s] Sound error: %s' % (pName, e))
Thread(target=_run, daemon=True).start()
def on_test_sound(*_):
try:
if not QtBind.isChecked(gui, chkSound):
log('[%s] Tick "Sound when target reached" first.' % pName)
return
except Exception:
pass
play_alert()
def _ask_wav_file():
"""Native Windows "Open file" dialog - no extra dependencies."""
try:
import ctypes
from ctypes import wintypes
except Exception:
return None
class OPENFILENAMEW(ctypes.Structure):
_fields_ = [
('lStructSize', wintypes.DWORD),
('hwndOwner', wintypes.HWND),
('hInstance', wintypes.HINSTANCE),
('lpstrFilter', wintypes.LPCWSTR),
('lpstrCustomFilter', wintypes.LPWSTR),
('nMaxCustFilter', wintypes.DWORD),
('nFilterIndex', wintypes.DWORD),
('lpstrFile', wintypes.LPWSTR),
('nMaxFile', wintypes.DWORD),
('lpstrFileTitle', wintypes.LPWSTR),
('nMaxFileTitle', wintypes.DWORD),
('lpstrInitialDir', wintypes.LPCWSTR),
('lpstrTitle', wintypes.LPCWSTR),
('Flags', wintypes.DWORD),
('nFileOffset', wintypes.WORD),
('nFileExtension', wintypes.WORD),
('lpstrDefExt', wintypes.LPCWSTR),
('lCustData', wintypes.LPARAM),
('lpfnHook', ctypes.c_void_p),
('lpTemplateName', wintypes.LPCWSTR),
('pvReserved', ctypes.c_void_p),
('dwReserved', wintypes.DWORD),
('FlagsEx', wintypes.DWORD),
]
cur = ''
try:
cur = QtBind.text(gui, txtWav).strip().strip('"')
except Exception:
pass
buf = ctypes.create_unicode_buffer(1024)
if cur:
buf.value = cur
ofn = OPENFILENAMEW()
ofn.lStructSize = ctypes.sizeof(OPENFILENAMEW)
ofn.lpstrFilter = 'WAV files\0*.wav\0All files\0*.*\0\0'
ofn.lpstrFile = ctypes.cast(buf, wintypes.LPWSTR)
ofn.nMaxFile = 1024
ofn.lpstrTitle = 'Choose the alert sound (.wav)'
ofn.lpstrInitialDir = (os.path.dirname(cur) if cur else
os.path.join(os.environ.get('WINDIR', 'C:\\Windows'), 'Media'))
# NOCHANGEDIR | PATHMUSTEXIST | FILEMUSTEXIST | EXPLORER
ofn.Flags = 0x00000008 | 0x00000800 | 0x00001000 | 0x00080000
try:
ok = ctypes.windll.comdlg32.GetOpenFileNameW(ctypes.byref(ofn))
except Exception as e:
log('[%s] File dialog error: %s' % (pName, e))
return None
return buf.value if ok else None
def on_browse_wav(*_):
path = _ask_wav_file()
if not path:
return
QtBind.setText(gui, txtWav, path)
log('[%s] Sound file: %s' % (pName, path))
on_save()
def add_history(text):
"""Newest result on the first row; older ones below (scroll to see them)."""
history.insert(0, text)
try:
QtBind.clear(gui, lstResult)
for line in history:
QtBind.append(gui, lstResult, line)
except Exception:
pass
def clear_history():
history[:] = []
try:
QtBind.clear(gui, lstResult)
except Exception:
pass
def lines_target():
try:
return max(1, int(QtBind.text(gui, txtLines).strip()))
except Exception:
return 4
def delay_sec():
try:
return max(0.0, int(QtBind.text(gui, txtDelay).strip()) / 1000.0)
except Exception:
return 2.0
# ============================================================================
# Config
# ============================================================================
def on_save(*_):
try:
with open(CONFIG_FILE, 'w') as f:
json.dump({
'lines': lines_target(),
'delay': int(delay_sec() * 1000),
'sound': bool(QtBind.isChecked(gui, chkSound)),
'wav': QtBind.text(gui, txtWav),
}, f, indent=2)
log('[%s] Saved.' % pName)
except Exception as e:
log('[%s] Save error: %s' % (pName, e))
def load_config():
try:
QtBind.setChecked(gui, chkSound, True) # default: alert on
except Exception:
pass
try:
if os.path.isfile(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
data = json.load(f)
QtBind.setText(gui, txtLines, str(data.get('lines', 4)))
QtBind.setText(gui, txtDelay, str(data.get('delay', 2000)))
try:
QtBind.setChecked(gui, chkSound, bool(data.get('sound', True)))
QtBind.setText(gui, txtWav, str(data.get('wav', '')))
except Exception:
pass
except Exception as e:
log('[%s] Config read error: %s' % (pName, e))
# ============================================================================
# Inventory / items
# ============================================================================
def on_refresh():
current_items.clear()
QtBind.clear(gui, combo_items)
inv = get_inventory()
if not inv or 'items' not in inv:
log('[%s] You are not in game.' % pName)
return
idx = 0
for slot, item in enumerate(inv['items']):
if not item or slot <= 13: # skip equipment
continue
try:
ref = get_item(item['model'])
except Exception:
ref = None
# only equippable items (tid1 == 1)
if ref and ref.get('tid1') == 1:
current_items[idx] = slot
idx += 1
QtBind.append(gui, combo_items, '%s +%s' % (item['name'], item.get('plus', 0)))
log('[%s] %d equippable items in inventory.' % (pName, idx))
def selected_item_slot():
if not current_items:
return None
i = QtBind.currentIndex(gui, combo_items)
return current_items.get(i)
def current_target_slot():
if queue:
return queue[0][0]
return selected_item_slot()
def on_add_queue():
slot = selected_item_slot()
if slot is None:
log('[%s] Select an item (Refresh) before adding it to the queue.' % pName)
return
inv = get_inventory()
it = inv['items'][slot] if (inv and 'items' in inv and slot < len(inv['items'])) else None
label = ('%s +%s' % (it.get('name', '?'), it.get('plus', 0))) if isinstance(it, dict) else ('slot %d' % slot)
queue.append((slot, label))
QtBind.append(gui, lstQueue, label)
log('[%s] Added to queue: %s. Total: %d.' % (pName, label, len(queue)))
def on_clear_queue():
queue[:] = []
try:
QtBind.clear(gui, lstQueue)
except Exception:
pass
log('[%s] Queue cleared.' % pName)
def get_wheel_slot():
inv = get_inventory()
if not inv or 'items' not in inv:
return -1
for slot, item in enumerate(inv['items']):
if item and 'Wheel of Fate' in (item.get('name') or ''):
return slot
return -1
# ============================================================================
# Start / Stop
# ============================================================================
def on_startstop():
global started
if not started:
if not queue and selected_item_slot() is None:
log('[%s] Select an item (Refresh) or add one to the queue.' % pName)
return
started = True
QtBind.setText(gui, btnStartStop, ' Stop ')
clear_history()
set_status('running')
use_wheel()
else:
stop()
def stop():
global started, _timer
started = False
QtBind.setText(gui, btnStartStop, ' Start ')
if _timer:
try:
_timer.cancel()
except Exception:
pass
_timer = None
set_status('stopped')
# ============================================================================
# Use Wheel of Fate
# ============================================================================
def use_wheel():
global attempts
if not started:
return
item_slot = current_target_slot()
if item_slot is None:
log('[%s] No item selected.' % pName)
stop()
return
wheel_slot = get_wheel_slot()
if wheel_slot == -1:
log('[%s] No Wheel of Fate left in inventory. Stopping.' % pName)
stop()
return
p = b'\x02\x19\x02'
p += struct.pack('B', item_slot)
p += struct.pack('B', wheel_slot)
inject_joymax(WHEEL_OPCODE, p, False)
attempts += 1
log('[%s] Wheel of Fate #%d (item slot %d).' % (pName, attempts, item_slot))
# ============================================================================
# Wheel response: count the lines and stop at target
# ============================================================================
def handle_joymax(opcode, data):
global _timer
if opcode == WHEEL_RESULT_OPCODE and started:
try:
is_success = data[2]
if is_success == 1 and len(data) > MAGICLINES_OFFSET:
number_of_magiclines = data[MAGICLINES_OFFSET]
blue = max(0, number_of_magiclines - 1) # the first line is the base one
target = lines_target()
set_status('%d lines (target %d), attempt %d' % (blue, target, attempts))
add_history('Attempt %d: %d lines' % (attempts, blue))
log('[%s] Result: raw=%d, blue=%d (target %d).' % (pName, number_of_magiclines, blue, target))
if blue >= target:
log('[%s] Item done: %d lines >= %d.' % (pName, blue, target))
play_alert()
if queue:
done = queue.pop(0)
try:
QtBind.remove(gui, lstQueue, done[1])
except Exception:
pass
log('[%s] Removed from queue: %s. Remaining: %d.' % (pName, done[1], len(queue)))
if queue:
if started:
_timer = Timer(delay_sec(), use_wheel)
_timer.start()
else:
log('[%s] DONE! Queue finished.' % pName)
stop()
elif started:
_timer = Timer(delay_sec(), use_wheel)
_timer.start()
else:
# unsuccessful response -> retry after delay
if started:
_timer = Timer(delay_sec(), use_wheel)
_timer.start()
except Exception as e:
log('[%s] Response parse error: %s' % (pName, e))
return True
# ============================================================================
# Init
# ============================================================================
load_config()
log('[%s] v%s loaded OK.' % (pName, pVersion))
▸ WheelOfFate.py v1.2 Download
# -*- coding: utf-8 -*-
#
# WheelOfFate - plugin for phBot (iSRO-R)
# ----------------------------------------
# Automatically uses "Wheel of Fate" on an item and stops when the item has a
# number of magic (blue) lines >= the value you choose. The VALUES of the blue
# lines do not matter, only HOW MANY lines there are.
#
# NOTE: every spin consumes one Wheel of Fate. The first spin on an item always
# happens (the plugin cannot read the current lines without spinning once).
#
from phBot import *
from threading import Timer
import struct
import QtBind
import os
import json
pName = 'WheelOfFate'
pVersion = '1.2'
# wheel use packet (same as AutoWheels): 0x7151
WHEEL_OPCODE = 0x7151
WHEEL_RESULT_OPCODE = 0xB151
# offset of number_of_magiclines inside the 0xB151 response
MAGICLINES_OFFSET = 26
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), pName + '.json')
started = False
_timer = None
current_items = {} # combo index -> inventory slot
attempts = 0
queue = [] # (slot, label) items waiting
history = [] # result lines, newest first
# ============================================================================
# GUI
# ============================================================================
gui = QtBind.init(__name__, pName)
# ---- Title ----
QtBind.createLabel(gui, 'W H E E L O F F A T E v%s' % pVersion, 10, 6)
# ---- Item selection ----
QtBind.createLabel(gui, 'Item to spin:', 10, 30)
combo_items = QtBind.createCombobox(gui, 10, 48, 240, 22)
QtBind.createButton(gui, 'on_refresh', ' Refresh items ', 258, 47)
QtBind.createButton(gui, 'on_add_queue', ' + Queue ', 258, 74)
QtBind.createButton(gui, 'on_clear_queue', ' Clear queue ', 360, 74)
# ---- Queue (right, under Clear queue) ----
QtBind.createLabel(gui, 'Queue (remaining):', 360, 104)
lstQueue = QtBind.createList(gui, 360, 124, 210, 176)
# ---- Settings ----
QtBind.createLabel(gui, 'Stop at blue lines >=:', 10, 102)
txtLines = QtBind.createLineEdit(gui, '4', 210, 100, 40, 20)
QtBind.createLabel(gui, 'Delay between wheels (ms):', 10, 126)
txtDelay = QtBind.createLineEdit(gui, '2000', 210, 124, 50, 20)
QtBind.createButton(gui, 'on_save', ' Save ', 280, 123)
btnStartStop = QtBind.createButton(gui, 'on_startstop', ' Start ', 10, 150)
lblStatus = QtBind.createLabel(gui, 'Status: stopped', 150, 154)
# ---- History (left) : newest line on top, scroll for the older ones ----
QtBind.createLabel(gui, 'History (attempt: lines):', 10, 182)
lstResult = QtBind.createList(gui, 10, 200, 240, 44)
# ---- Footer ----
QtBind.createLabel(gui, 'Author : CK3 @ iLog1K.net', 10, 258)
def set_status(txt):
QtBind.setText(gui, lblStatus, 'Status: ' + txt)
def add_history(text):
"""Newest result on the first row; older ones below (scroll to see them)."""
history.insert(0, text)
try:
QtBind.clear(gui, lstResult)
for line in history:
QtBind.append(gui, lstResult, line)
except Exception:
pass
def clear_history():
history[:] = []
try:
QtBind.clear(gui, lstResult)
except Exception:
pass
def lines_target():
try:
return max(1, int(QtBind.text(gui, txtLines).strip()))
except Exception:
return 4
def delay_sec():
try:
return max(0.0, int(QtBind.text(gui, txtDelay).strip()) / 1000.0)
except Exception:
return 2.0
# ============================================================================
# Config
# ============================================================================
def on_save(*_):
try:
with open(CONFIG_FILE, 'w') as f:
json.dump({'lines': lines_target(), 'delay': int(delay_sec() * 1000)}, f, indent=2)
log('[%s] Saved.' % pName)
except Exception as e:
log('[%s] Save error: %s' % (pName, e))
def load_config():
try:
if os.path.isfile(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
data = json.load(f)
QtBind.setText(gui, txtLines, str(data.get('lines', 4)))
QtBind.setText(gui, txtDelay, str(data.get('delay', 2000)))
except Exception as e:
log('[%s] Config read error: %s' % (pName, e))
# ============================================================================
# Inventory / items
# ============================================================================
def on_refresh():
current_items.clear()
QtBind.clear(gui, combo_items)
inv = get_inventory()
if not inv or 'items' not in inv:
log('[%s] You are not in game.' % pName)
return
idx = 0
for slot, item in enumerate(inv['items']):
if not item or slot <= 13: # skip equipment
continue
try:
ref = get_item(item['model'])
except Exception:
ref = None
# only equippable items (tid1 == 1)
if ref and ref.get('tid1') == 1:
current_items[idx] = slot
idx += 1
QtBind.append(gui, combo_items, '%s +%s' % (item['name'], item.get('plus', 0)))
log('[%s] %d equippable items in inventory.' % (pName, idx))
def selected_item_slot():
if not current_items:
return None
i = QtBind.currentIndex(gui, combo_items)
return current_items.get(i)
def current_target_slot():
if queue:
return queue[0][0]
return selected_item_slot()
def on_add_queue():
slot = selected_item_slot()
if slot is None:
log('[%s] Select an item (Refresh) before adding it to the queue.' % pName)
return
inv = get_inventory()
it = inv['items'][slot] if (inv and 'items' in inv and slot < len(inv['items'])) else None
label = ('%s +%s' % (it.get('name', '?'), it.get('plus', 0))) if isinstance(it, dict) else ('slot %d' % slot)
queue.append((slot, label))
QtBind.append(gui, lstQueue, label)
log('[%s] Added to queue: %s. Total: %d.' % (pName, label, len(queue)))
def on_clear_queue():
queue[:] = []
try:
QtBind.clear(gui, lstQueue)
except Exception:
pass
log('[%s] Queue cleared.' % pName)
def get_wheel_slot():
inv = get_inventory()
if not inv or 'items' not in inv:
return -1
for slot, item in enumerate(inv['items']):
if item and 'Wheel of Fate' in (item.get('name') or ''):
return slot
return -1
# ============================================================================
# Start / Stop
# ============================================================================
def on_startstop():
global started
if not started:
if not queue and selected_item_slot() is None:
log('[%s] Select an item (Refresh) or add one to the queue.' % pName)
return
started = True
QtBind.setText(gui, btnStartStop, ' Stop ')
clear_history()
set_status('running')
use_wheel()
else:
stop()
def stop():
global started, _timer
started = False
QtBind.setText(gui, btnStartStop, ' Start ')
if _timer:
try:
_timer.cancel()
except Exception:
pass
_timer = None
set_status('stopped')
# ============================================================================
# Use Wheel of Fate
# ============================================================================
def use_wheel():
global attempts
if not started:
return
item_slot = current_target_slot()
if item_slot is None:
log('[%s] No item selected.' % pName)
stop()
return
wheel_slot = get_wheel_slot()
if wheel_slot == -1:
log('[%s] No Wheel of Fate left in inventory. Stopping.' % pName)
stop()
return
p = b'\x02\x19\x02'
p += struct.pack('B', item_slot)
p += struct.pack('B', wheel_slot)
inject_joymax(WHEEL_OPCODE, p, False)
attempts += 1
log('[%s] Wheel of Fate #%d (item slot %d).' % (pName, attempts, item_slot))
# ============================================================================
# Wheel response: count the lines and stop at target
# ============================================================================
def handle_joymax(opcode, data):
global _timer
if opcode == WHEEL_RESULT_OPCODE and started:
try:
is_success = data[2]
if is_success == 1 and len(data) > MAGICLINES_OFFSET:
number_of_magiclines = data[MAGICLINES_OFFSET]
blue = max(0, number_of_magiclines - 1) # the first line is the base one
target = lines_target()
set_status('%d lines (target %d), attempt %d' % (blue, target, attempts))
add_history('Attempt %d: %d lines' % (attempts, blue))
log('[%s] Result: raw=%d, blue=%d (target %d).' % (pName, number_of_magiclines, blue, target))
if blue >= target:
log('[%s] Item done: %d lines >= %d.' % (pName, blue, target))
if queue:
done = queue.pop(0)
try:
QtBind.remove(gui, lstQueue, done[1])
except Exception:
pass
log('[%s] Removed from queue: %s. Remaining: %d.' % (pName, done[1], len(queue)))
if queue:
if started:
_timer = Timer(delay_sec(), use_wheel)
_timer.start()
else:
log('[%s] DONE! Queue finished.' % pName)
stop()
elif started:
_timer = Timer(delay_sec(), use_wheel)
_timer.start()
else:
# unsuccessful response -> retry after delay
if started:
_timer = Timer(delay_sec(), use_wheel)
_timer.start()
except Exception as e:
log('[%s] Response parse error: %s' % (pName, e))
return True
# ============================================================================
# Init
# ============================================================================
load_config()
log('[%s] v%s loaded OK.' % (pName, pVersion))
Sign in to propose something or back an existing proposal.
No proposals yet. Be the first to suggest something.
Sign in to join the discussion.
No comments yet.
Sign in to report a bug.
No bug reports. Nice.