plugin iSRO-R v2.0

Wheel of Fortune

by @CK3 · 2d ago · updated 6h ago · 24 views

WheelOfFortune - plugin for phBot (iSRO-R)

  • Automatically uses "Wheel of Fortune" on an item and stops when YOURconditions (line count per attribute) are met.
  • Example: STR >= 2 lines AND INT >= 2 lines (other lines do not matter).
  • Logic is AND: it stops only when ALL the conditions you set (> 0) are met.

Changelog

v2.0 latest 2026-08-02
  • Sound on condition met on item.
v1.9 2026-07-31
  • Added option STOP When one from multiple condition met.
v1.8 files only

Released with files, no release notes written yet.

WheelOfFortune.py v2.0 python · 644 lines · 19,241 B · 6h ago Download
# -*- coding: utf-8 -*-
#
# WheelOfFortune - plugin for phBot  (iSRO-R)
# -------------------------------------------
# Automatically uses "Wheel of Fortune" on an item and stops when YOUR
# conditions (line count per attribute) are met.
# Example: STR >= 2 lines AND INT >= 2 lines (other lines do not matter).
# Logic is AND by default: it stops only when ALL the conditions you set (> 0)
# are met. Tick "When one condition met" to stop as soon as ONE of them is met.
#
# It resolves each magic line's attribute from the phBot database
# (Data/*.db3, table magicoption). Separate from AutoWheels and WheelOfFate.
#
# NOTE: every spin consumes one Wheel of Fortune.
#
from phBot import *
from threading import Timer, Thread
import struct
import QtBind
import os
import json
import sqlite3

try:
	import winsound
except Exception:
	winsound = None

pName = 'WheelOfFortune'
pVersion = '2.0'

WHEEL_OPCODE = 0x7151
WHEEL_RESULT_OPCODE = 0xB151
MAGICLINES_OFFSET = 26          # number_of_magiclines in the 0xB151 response
WHEEL_ITEM_NAME = 'Wheel of Fortune'

INSTALL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(INSTALL_DIR, 'Data')
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), pName + '.json')

started = False
_timer = None
current_items = {}
attempts = 0
queue = []      # item slots in the queue (multi-item)
qi = 0          # index of the current item in the queue
_db = None
history = []    # result lines, newest first

# ============================================================================
# GUI
# ============================================================================
gui = QtBind.init(__name__, pName)

# ---- Title ----
QtBind.createLabel(gui, 'W H E E L   O F   F O R T U N E        v%s' % pVersion, 10, 6)

# ---- Item selection ----
QtBind.createLabel(gui, 'Item to spin:', 10, 32)
combo_items = QtBind.createCombobox(gui, 10, 52, 180, 22)
QtBind.createButton(gui, 'on_refresh', 'Refresh', 196, 51)
QtBind.createButton(gui, 'on_add_queue', '+Queue', 300, 51)
QtBind.createButton(gui, 'on_clear_queue', 'Clear', 404, 51)

lblMode = QtBind.createLabel(gui, 'Stop when lines >=  (0 = ignore).  ALL set conditions must be met:', 10, 82)

# column 1
QtBind.createLabel(gui, 'STR', 10, 106)
txtSTR = QtBind.createLineEdit(gui, '0', 70, 104, 35, 20)
QtBind.createLabel(gui, 'INT', 10, 130)
txtINT = QtBind.createLineEdit(gui, '0', 70, 128, 35, 20)
QtBind.createLabel(gui, 'Critical', 10, 154)
txtCRIT = QtBind.createLineEdit(gui, '0', 70, 152, 35, 20)
QtBind.createLabel(gui, 'Attack R.', 10, 178)
txtHR = QtBind.createLineEdit(gui, '0', 70, 176, 35, 20)

# column 2
QtBind.createLabel(gui, 'HP', 130, 106)
txtHP = QtBind.createLineEdit(gui, '0', 195, 104, 35, 20)
QtBind.createLabel(gui, 'MP', 130, 130)
txtMP = QtBind.createLineEdit(gui, '0', 195, 128, 35, 20)
QtBind.createLabel(gui, 'Block R.', 130, 154)
txtBLOCK = QtBind.createLineEdit(gui, '0', 195, 152, 35, 20)
QtBind.createLabel(gui, 'Parry R.', 130, 178)
txtER = QtBind.createLineEdit(gui, '0', 195, 176, 35, 20)

QtBind.createLabel(gui, 'Delay (ms):', 250, 106)
txtDelay = QtBind.createLineEdit(gui, '2000', 320, 104, 55, 20)
QtBind.createButton(gui, 'on_save', '  Save  ', 250, 130)
chkAny = QtBind.createCheckBox(gui, 'on_mode_changed', 'When one condition met', 250, 152)

btnStartStop = QtBind.createButton(gui, 'on_startstop', '     Start     ', 250, 172)
lblStatus = QtBind.createLabel(gui, 'stopped', 250, 204)

# ---- Sound alert ----
chkSound = QtBind.createCheckBox(gui, 'on_dummy', 'Sound on finish   (WAV below, blank = beep)', 410, 6)
txtWav = QtBind.createLineEdit(gui, '', 410, 28, 270, 20)
QtBind.createButton(gui, 'on_browse_wav', ' Browse ', 490, 51)
QtBind.createButton(gui, 'on_test_sound', ' Test ', 595, 51)

QtBind.createLabel(gui, 'History:', 10, 204)
lstResult = QtBind.createList(gui, 10, 222, 380, 40)

# right panel: current blue lines (updated every wheel)
BLUE_ORDER = [('STR', 'STR'), ('INT', 'INT'), ('HP', 'HP'), ('MP', 'MP'),
			  ('CRIT', 'Crit'), ('BLOCK', 'Block'), ('HR', 'AtkR'), ('ER', 'ParR')]
QtBind.createLabel(gui, 'Blue:', 410, 84)
lstBlue = QtBind.createList(gui, 410, 104, 85, 158)
QtBind.createLabel(gui, 'Queue (remaining):', 505, 84)
lstQueue = QtBind.createList(gui, 505, 104, 180, 158)

# ---- Footer ----
QtBind.createLabel(gui, 'Author : CK3 @ iLog1K.net', 10, 272)

# condition key -> GUI field mapping
COND_FIELDS = [
	('STR', 'txtSTR'), ('INT', 'txtINT'), ('CRIT', 'txtCRIT'), ('HR', 'txtHR'),
	('HP', 'txtHP'), ('MP', 'txtMP'), ('BLOCK', 'txtBLOCK'), ('ER', 'txtER'),
]


# ============================================================================
# Helpers
# ============================================================================
def set_status(txt):
	QtBind.setText(gui, lblStatus, 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 update_blues(values):
	try:
		QtBind.clear(gui, lstBlue)
		for _k, _d in BLUE_ORDER:
			QtBind.append(gui, lstBlue, '%s = %d' % (_d, values.get(_k, 0)))
	except Exception:
		pass


def delay_sec():
	try:
		return max(0.0, int(QtBind.text(gui, txtDelay).strip()) / 1000.0)
	except Exception:
		return 2.0


def on_dummy(*_):
	pass


def play_alert():
	"""Beep / play a WAV when an item is finished. 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 on finish" 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 any_mode():
	"""True = stop as soon as ONE condition is met (OR). False = ALL (AND)."""
	try:
		return bool(QtBind.isChecked(gui, chkAny))
	except Exception:
		return False


def on_mode_changed(*_):
	try:
		if any_mode():
			QtBind.setText(gui, lblMode, 'Stop when lines >=  (0 = ignore).  ANY set condition is enough:')
		else:
			QtBind.setText(gui, lblMode, 'Stop when lines >=  (0 = ignore).  ALL set conditions must be met:')
	except Exception:
		pass


def read_conditions():
	out = {}
	for key, fld in COND_FIELDS:
		try:
			out[key] = max(0, int(QtBind.text(gui, globals()[fld]).strip()))
		except Exception:
			out[key] = 0
	return out


def attr_key(name):
	"""magicoption servername -> tracked attribute key (or None)."""
	if not name:
		return None
	n = name.upper()
	if 'MATTR_STR' in n:
		return 'STR'
	if 'MATTR_INT' in n:
		return 'INT'
	if 'MATTR_REGENHPMP' in n:
		return None
	if 'MATTR_HP' in n:
		return 'HP'
	if 'MATTR_MP' in n:
		return 'MP'
	if 'EVADE_CRITICAL' in n:
		return None
	if 'MATTR_CRITICAL' in n:
		return 'CRIT'
	if 'MATTR_BLOCKRATE' in n:
		return 'BLOCK'
	if 'MATTR_HR' in n:
		return 'HR'
	if 'MATTR_ER' in n:
		return 'ER'
	return None


# ============================================================================
# Database (resolve magic line id -> servername)
# ============================================================================
def _db3_files():
	try:
		return [os.path.join(DATA_DIR, fn) for fn in os.listdir(DATA_DIR)
				if fn.lower().endswith('.db3')]
	except Exception:
		return []


def _lookup(con, mid):
	try:
		r = con.cursor().execute('SELECT name FROM magicoption WHERE id=?', (mid,)).fetchone()
		return r[0] if r else None
	except Exception:
		return None


def magic_name(mid):
	"""Pick AUTOMATICALLY the db3 that resolves the current server's ids."""
	global _db
	if _db is not None:
		n = _lookup(_db, mid)
		if n:
			return n
	for f in _db3_files():
		try:
			con = sqlite3.connect(f, check_same_thread=False)
		except Exception:
			continue
		n = _lookup(con, mid)
		if n:
			if _db is not None:
				try:
					_db.close()
				except Exception:
					pass
			_db = con
			log('[%s] Matched DB: %s' % (pName, os.path.basename(f)))
			return n
		try:
			con.close()
		except Exception:
			pass
	return None


# ============================================================================
# Config
# ============================================================================
def on_save(*_):
	try:
		data = {
			'delay': int(delay_sec() * 1000),
			'any_mode': any_mode(),
			'sound': bool(QtBind.isChecked(gui, chkSound)),
			'wav': QtBind.text(gui, txtWav),
		}
		for key, fld in COND_FIELDS:
			data[key] = QtBind.text(gui, globals()[fld])
		with open(CONFIG_FILE, 'w') as f:
			json.dump(data, 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, txtDelay, str(data.get('delay', 2000)))
			for key, fld in COND_FIELDS:
				QtBind.setText(gui, globals()[fld], str(data.get(key, '0')))
			try:
				QtBind.setChecked(gui, chkAny, bool(data.get('any_mode', False)))
				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))
	on_mode_changed()


# ============================================================================
# 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:
			continue
		try:
			ref = get_item(item['model'])
		except Exception:
			ref = None
		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.' % (pName, idx))


def selected_item_slot():
	if not current_items:
		return None
	return current_items.get(QtBind.currentIndex(gui, combo_items))


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_ITEM_NAME in (item.get('name') or ''):
			return slot
	return -1


# ============================================================================
# Start / Stop
# ============================================================================
def on_startstop():
	global started, qi
	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
		if all(v == 0 for v in read_conditions().values()):
			log('[%s] Set at least one condition (> 0).' % pName)
			return
		started = True
		qi = 0
		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
# ============================================================================
def use_wheel():
	global attempts
	if not started:
		return
	item_slot = current_target_slot()
	if item_slot is None:
		stop()
		return
	wheel_slot = get_wheel_slot()
	if wheel_slot == -1:
		log('[%s] No %s left in inventory. Stopping.' % (pName, WHEEL_ITEM_NAME))
		stop()
		return
	p = b'\x02\x19\x02' + struct.pack('B', item_slot) + struct.pack('B', wheel_slot)
	inject_joymax(WHEEL_OPCODE, p, False)
	attempts += 1
	log('[%s] %s #%d (item slot %d).' % (pName, WHEEL_ITEM_NAME, attempts, item_slot))


# ============================================================================
# Response: count lines per attribute and check the conditions
# ============================================================================
def eval_conditions(counts):
	"""Check the conditions set (> 0). Returns (met, progress_text).

	AND mode (checkbox off): met only when EVERY set condition has enough
	lines (e.g. STR 2 AND INT 2 -> both must reach the target).
	OR mode (checkbox 'When one condition met' on): met as soon as ONE set
	condition reaches its target."""
	conds = read_conditions()
	parts = []
	any_set = False
	all_met = True
	one_met = False
	for key, _fld in COND_FIELDS:
		needed = conds.get(key, 0)
		if needed > 0:
			any_set = True
			have = counts.get(key, 0)
			if have < needed:
				all_met = False
			else:
				one_met = True
			parts.append('%s %d/%d' % (key, have, needed))
	met = one_met if any_mode() else all_met
	progress = ('  '.join(parts) if parts else '(no conditions set)')
	if any_set and parts:
		progress += '  [%s]' % ('ANY' if any_mode() else 'ALL')
	return (any_set and met), progress


def handle_joymax(opcode, data):
	global _timer, qi
	if opcode == WHEEL_RESULT_OPCODE and started:
		try:
			is_success = data[2]
			if is_success == 1 and len(data) > MAGICLINES_OFFSET:
				n_lines = data[MAGICLINES_OFFSET]
				counts = {}
				values = {}
				idx = MAGICLINES_OFFSET + 1
				for x in range(n_lines):
					if idx + 8 > len(data):
						break
					mid = struct.unpack_from('<I', data, idx)[0]
					amount = struct.unpack_from('<I', data, idx + 4)[0]
					idx += 8  # id (4) + amount (4)
					if x == 0:
						continue  # first line is the base one
					sn = magic_name(mid)
					key = attr_key(sn)
					if key:
						counts[key] = counts.get(key, 0) + 1
						values[key] = values.get(key, 0) + amount

				summary = ' '.join('%s:%d (%dp)' % (k, counts[k], values.get(k, 0)) for k in counts) or '(no tracked attributes)'
				met, progress = eval_conditions(counts)
				add_history('Attempt %d:  %s' % (attempts, summary))
				set_status('attempt %d  |  %s' % (attempts, progress))
				log('[%s] Result #%d (raw lines=%d): %s  ->  %s' % (pName, attempts, n_lines, summary, progress))
				update_blues(values)

				if met:
					log('[%s] Item done: %s (%s).' % (pName, progress,
						'one condition met' if any_mode() else 'all conditions met'))
					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:
				if started:
					_timer = Timer(delay_sec(), use_wheel)
					_timer.start()
		except Exception as e:
			log('[%s] Parse error: %s' % (pName, e))
	return True


# ============================================================================
# Init
# ============================================================================
load_config()
log('[%s] v%s loaded OK.' % (pName, pVersion))
WheelOfFortune.py v1.9 python · 512 lines · 15,473 B · 2d ago Download
# -*- coding: utf-8 -*-
#
# WheelOfFortune - plugin for phBot  (iSRO-R)
# -------------------------------------------
# Automatically uses "Wheel of Fortune" on an item and stops when YOUR
# conditions (line count per attribute) are met.
# Example: STR >= 2 lines AND INT >= 2 lines (other lines do not matter).
# Logic is AND by default: it stops only when ALL the conditions you set (> 0)
# are met. Tick "When one condition met" to stop as soon as ONE of them is met.
#
# It resolves each magic line's attribute from the phBot database
# (Data/*.db3, table magicoption). Separate from  WheelOfFate.
#
# NOTE: every spin consumes one Wheel of Fortune.
#
from phBot import *
from threading import Timer
import struct
import QtBind
import os
import json
import sqlite3

pName = 'WheelOfFortune'
pVersion = '1.9'

WHEEL_OPCODE = 0x7151
WHEEL_RESULT_OPCODE = 0xB151
MAGICLINES_OFFSET = 26          # number_of_magiclines in the 0xB151 response
WHEEL_ITEM_NAME = 'Wheel of Fortune'

INSTALL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(INSTALL_DIR, 'Data')
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), pName + '.json')

started = False
_timer = None
current_items = {}
attempts = 0
queue = []      # item slots in the queue (multi-item)
qi = 0          # index of the current item in the queue
_db = None
history = []    # result lines, newest first

# ============================================================================
# GUI
# ============================================================================
gui = QtBind.init(__name__, pName)

# ---- Title ----
QtBind.createLabel(gui, 'W H E E L   O F   F O R T U N E        v%s' % pVersion, 10, 6)

# ---- Item selection ----
QtBind.createLabel(gui, 'Item to spin:', 10, 32)
combo_items = QtBind.createCombobox(gui, 10, 52, 180, 22)
QtBind.createButton(gui, 'on_refresh', 'Refresh', 196, 51)
QtBind.createButton(gui, 'on_add_queue', '+Queue', 300, 51)
QtBind.createButton(gui, 'on_clear_queue', 'Clear', 404, 51)

lblMode = QtBind.createLabel(gui, 'Stop when lines >=  (0 = ignore).  ALL set conditions must be met:', 10, 82)

# column 1
QtBind.createLabel(gui, 'STR', 10, 106)
txtSTR = QtBind.createLineEdit(gui, '0', 70, 104, 35, 20)
QtBind.createLabel(gui, 'INT', 10, 130)
txtINT = QtBind.createLineEdit(gui, '0', 70, 128, 35, 20)
QtBind.createLabel(gui, 'Critical', 10, 154)
txtCRIT = QtBind.createLineEdit(gui, '0', 70, 152, 35, 20)
QtBind.createLabel(gui, 'Attack R.', 10, 178)
txtHR = QtBind.createLineEdit(gui, '0', 70, 176, 35, 20)

# column 2
QtBind.createLabel(gui, 'HP', 130, 106)
txtHP = QtBind.createLineEdit(gui, '0', 195, 104, 35, 20)
QtBind.createLabel(gui, 'MP', 130, 130)
txtMP = QtBind.createLineEdit(gui, '0', 195, 128, 35, 20)
QtBind.createLabel(gui, 'Block R.', 130, 154)
txtBLOCK = QtBind.createLineEdit(gui, '0', 195, 152, 35, 20)
QtBind.createLabel(gui, 'Parry R.', 130, 178)
txtER = QtBind.createLineEdit(gui, '0', 195, 176, 35, 20)

QtBind.createLabel(gui, 'Delay (ms):', 250, 106)
txtDelay = QtBind.createLineEdit(gui, '2000', 320, 104, 55, 20)
QtBind.createButton(gui, 'on_save', '  Save  ', 250, 130)
chkAny = QtBind.createCheckBox(gui, 'on_mode_changed', 'When one condition met', 250, 152)

btnStartStop = QtBind.createButton(gui, 'on_startstop', '     Start     ', 250, 172)
lblStatus = QtBind.createLabel(gui, 'stopped', 250, 204)

QtBind.createLabel(gui, 'History:', 10, 204)
lstResult = QtBind.createList(gui, 10, 222, 380, 40)

# right panel: current blue lines (updated every wheel)
BLUE_ORDER = [('STR', 'STR'), ('INT', 'INT'), ('HP', 'HP'), ('MP', 'MP'),
			  ('CRIT', 'Crit'), ('BLOCK', 'Block'), ('HR', 'AtkR'), ('ER', 'ParR')]
QtBind.createLabel(gui, 'Blue:', 410, 84)
lstBlue = QtBind.createList(gui, 410, 104, 85, 158)
QtBind.createLabel(gui, 'Queue (remaining):', 505, 84)
lstQueue = QtBind.createList(gui, 505, 104, 180, 158)

# ---- Footer ----
QtBind.createLabel(gui, 'Author : CK3 @ iLog1K.net', 10, 272)

# condition key -> GUI field mapping
COND_FIELDS = [
	('STR', 'txtSTR'), ('INT', 'txtINT'), ('CRIT', 'txtCRIT'), ('HR', 'txtHR'),
	('HP', 'txtHP'), ('MP', 'txtMP'), ('BLOCK', 'txtBLOCK'), ('ER', 'txtER'),
]


# ============================================================================
# Helpers
# ============================================================================
def set_status(txt):
	QtBind.setText(gui, lblStatus, 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 update_blues(values):
	try:
		QtBind.clear(gui, lstBlue)
		for _k, _d in BLUE_ORDER:
			QtBind.append(gui, lstBlue, '%s = %d' % (_d, values.get(_k, 0)))
	except Exception:
		pass


def delay_sec():
	try:
		return max(0.0, int(QtBind.text(gui, txtDelay).strip()) / 1000.0)
	except Exception:
		return 2.0


def any_mode():
	"""True = stop as soon as ONE condition is met (OR). False = ALL (AND)."""
	try:
		return bool(QtBind.isChecked(gui, chkAny))
	except Exception:
		return False


def on_mode_changed(*_):
	try:
		if any_mode():
			QtBind.setText(gui, lblMode, 'Stop when lines >=  (0 = ignore).  ANY set condition is enough:')
		else:
			QtBind.setText(gui, lblMode, 'Stop when lines >=  (0 = ignore).  ALL set conditions must be met:')
	except Exception:
		pass


def read_conditions():
	out = {}
	for key, fld in COND_FIELDS:
		try:
			out[key] = max(0, int(QtBind.text(gui, globals()[fld]).strip()))
		except Exception:
			out[key] = 0
	return out


def attr_key(name):
	"""magicoption servername -> tracked attribute key (or None)."""
	if not name:
		return None
	n = name.upper()
	if 'MATTR_STR' in n:
		return 'STR'
	if 'MATTR_INT' in n:
		return 'INT'
	if 'MATTR_REGENHPMP' in n:
		return None
	if 'MATTR_HP' in n:
		return 'HP'
	if 'MATTR_MP' in n:
		return 'MP'
	if 'EVADE_CRITICAL' in n:
		return None
	if 'MATTR_CRITICAL' in n:
		return 'CRIT'
	if 'MATTR_BLOCKRATE' in n:
		return 'BLOCK'
	if 'MATTR_HR' in n:
		return 'HR'
	if 'MATTR_ER' in n:
		return 'ER'
	return None


# ============================================================================
# Database (resolve magic line id -> servername)
# ============================================================================
def _db3_files():
	try:
		return [os.path.join(DATA_DIR, fn) for fn in os.listdir(DATA_DIR)
				if fn.lower().endswith('.db3')]
	except Exception:
		return []


def _lookup(con, mid):
	try:
		r = con.cursor().execute('SELECT name FROM magicoption WHERE id=?', (mid,)).fetchone()
		return r[0] if r else None
	except Exception:
		return None


def magic_name(mid):
	"""Pick AUTOMATICALLY the db3 that resolves the current server's ids."""
	global _db
	if _db is not None:
		n = _lookup(_db, mid)
		if n:
			return n
	for f in _db3_files():
		try:
			con = sqlite3.connect(f, check_same_thread=False)
		except Exception:
			continue
		n = _lookup(con, mid)
		if n:
			if _db is not None:
				try:
					_db.close()
				except Exception:
					pass
			_db = con
			log('[%s] Matched DB: %s' % (pName, os.path.basename(f)))
			return n
		try:
			con.close()
		except Exception:
			pass
	return None


# ============================================================================
# Config
# ============================================================================
def on_save(*_):
	try:
		data = {'delay': int(delay_sec() * 1000), 'any_mode': any_mode()}
		for key, fld in COND_FIELDS:
			data[key] = QtBind.text(gui, globals()[fld])
		with open(CONFIG_FILE, 'w') as f:
			json.dump(data, 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, txtDelay, str(data.get('delay', 2000)))
			for key, fld in COND_FIELDS:
				QtBind.setText(gui, globals()[fld], str(data.get(key, '0')))
			try:
				QtBind.setChecked(gui, chkAny, bool(data.get('any_mode', False)))
			except Exception:
				pass
	except Exception as e:
		log('[%s] Config read error: %s' % (pName, e))
	on_mode_changed()


# ============================================================================
# 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:
			continue
		try:
			ref = get_item(item['model'])
		except Exception:
			ref = None
		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.' % (pName, idx))


def selected_item_slot():
	if not current_items:
		return None
	return current_items.get(QtBind.currentIndex(gui, combo_items))


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_ITEM_NAME in (item.get('name') or ''):
			return slot
	return -1


# ============================================================================
# Start / Stop
# ============================================================================
def on_startstop():
	global started, qi
	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
		if all(v == 0 for v in read_conditions().values()):
			log('[%s] Set at least one condition (> 0).' % pName)
			return
		started = True
		qi = 0
		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
# ============================================================================
def use_wheel():
	global attempts
	if not started:
		return
	item_slot = current_target_slot()
	if item_slot is None:
		stop()
		return
	wheel_slot = get_wheel_slot()
	if wheel_slot == -1:
		log('[%s] No %s left in inventory. Stopping.' % (pName, WHEEL_ITEM_NAME))
		stop()
		return
	p = b'\x02\x19\x02' + struct.pack('B', item_slot) + struct.pack('B', wheel_slot)
	inject_joymax(WHEEL_OPCODE, p, False)
	attempts += 1
	log('[%s] %s #%d (item slot %d).' % (pName, WHEEL_ITEM_NAME, attempts, item_slot))


# ============================================================================
# Response: count lines per attribute and check the conditions
# ============================================================================
def eval_conditions(counts):
	"""Check the conditions set (> 0). Returns (met, progress_text).

	AND mode (checkbox off): met only when EVERY set condition has enough
	lines (e.g. STR 2 AND INT 2 -> both must reach the target).
	OR mode (checkbox 'When one condition met' on): met as soon as ONE set
	condition reaches its target."""
	conds = read_conditions()
	parts = []
	any_set = False
	all_met = True
	one_met = False
	for key, _fld in COND_FIELDS:
		needed = conds.get(key, 0)
		if needed > 0:
			any_set = True
			have = counts.get(key, 0)
			if have < needed:
				all_met = False
			else:
				one_met = True
			parts.append('%s %d/%d' % (key, have, needed))
	met = one_met if any_mode() else all_met
	progress = ('  '.join(parts) if parts else '(no conditions set)')
	if any_set and parts:
		progress += '  [%s]' % ('ANY' if any_mode() else 'ALL')
	return (any_set and met), progress


def handle_joymax(opcode, data):
	global _timer, qi
	if opcode == WHEEL_RESULT_OPCODE and started:
		try:
			is_success = data[2]
			if is_success == 1 and len(data) > MAGICLINES_OFFSET:
				n_lines = data[MAGICLINES_OFFSET]
				counts = {}
				values = {}
				idx = MAGICLINES_OFFSET + 1
				for x in range(n_lines):
					if idx + 8 > len(data):
						break
					mid = struct.unpack_from('<I', data, idx)[0]
					amount = struct.unpack_from('<I', data, idx + 4)[0]
					idx += 8  # id (4) + amount (4)
					if x == 0:
						continue  # first line is the base one
					sn = magic_name(mid)
					key = attr_key(sn)
					if key:
						counts[key] = counts.get(key, 0) + 1
						values[key] = values.get(key, 0) + amount

				summary = ' '.join('%s:%d (%dp)' % (k, counts[k], values.get(k, 0)) for k in counts) or '(no tracked attributes)'
				met, progress = eval_conditions(counts)
				add_history('Attempt %d:  %s' % (attempts, summary))
				set_status('attempt %d  |  %s' % (attempts, progress))
				log('[%s] Result #%d (raw lines=%d): %s  ->  %s' % (pName, attempts, n_lines, summary, progress))
				update_blues(values)

				if met:
					log('[%s] Item done: %s (%s).' % (pName, progress,
						'one condition met' if any_mode() else 'all conditions met'))
					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:
				if started:
					_timer = Timer(delay_sec(), use_wheel)
					_timer.start()
		except Exception as e:
			log('[%s] Parse error: %s' % (pName, e))
	return True


# ============================================================================
# Init
# ============================================================================
load_config()
log('[%s] v%s loaded OK.' % (pName, pVersion))
WheelOfFortune.py v1.8 python · 477 lines · 14,366 B · 2d ago Download
# -*- coding: utf-8 -*-
#
# WheelOfFortune - plugin for phBot  (iSRO-R)
# -------------------------------------------
# Automatically uses "Wheel of Fortune" on an item and stops when YOUR
# conditions (line count per attribute) are met.
# Example: STR >= 2 lines AND INT >= 2 lines (other lines do not matter).
# Logic is AND: it stops only when ALL the conditions you set (> 0) are met.
#
# It resolves each magic line's attribute from the phBot database
# (Data/*.db3, table magicoption). Separate from AutoWheels and WheelOfFate.
#
# NOTE: every spin consumes one Wheel of Fortune.
#
from phBot import *
from threading import Timer
import struct
import QtBind
import os
import json
import sqlite3

pName = 'WheelOfFortune'
pVersion = '1.8'

WHEEL_OPCODE = 0x7151
WHEEL_RESULT_OPCODE = 0xB151
MAGICLINES_OFFSET = 26          # number_of_magiclines in the 0xB151 response
WHEEL_ITEM_NAME = 'Wheel of Fortune'

INSTALL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(INSTALL_DIR, 'Data')
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), pName + '.json')

started = False
_timer = None
current_items = {}
attempts = 0
queue = []      # item slots in the queue (multi-item)
qi = 0          # index of the current item in the queue
_db = None
history = []    # result lines, newest first

# ============================================================================
# GUI
# ============================================================================
gui = QtBind.init(__name__, pName)

# ---- Title ----
QtBind.createLabel(gui, 'W H E E L   O F   F O R T U N E        v%s' % pVersion, 10, 6)

# ---- Item selection ----
QtBind.createLabel(gui, 'Item to spin:', 10, 32)
combo_items = QtBind.createCombobox(gui, 10, 52, 180, 22)
QtBind.createButton(gui, 'on_refresh', 'Refresh', 196, 51)
QtBind.createButton(gui, 'on_add_queue', '+Queue', 300, 51)
QtBind.createButton(gui, 'on_clear_queue', 'Clear', 404, 51)

QtBind.createLabel(gui, 'Stop when lines >=  (0 = ignore).  ALL set conditions must be met:', 10, 82)

# column 1
QtBind.createLabel(gui, 'STR', 10, 106)
txtSTR = QtBind.createLineEdit(gui, '0', 70, 104, 35, 20)
QtBind.createLabel(gui, 'INT', 10, 130)
txtINT = QtBind.createLineEdit(gui, '0', 70, 128, 35, 20)
QtBind.createLabel(gui, 'Critical', 10, 154)
txtCRIT = QtBind.createLineEdit(gui, '0', 70, 152, 35, 20)
QtBind.createLabel(gui, 'Attack R.', 10, 178)
txtHR = QtBind.createLineEdit(gui, '0', 70, 176, 35, 20)

# column 2
QtBind.createLabel(gui, 'HP', 130, 106)
txtHP = QtBind.createLineEdit(gui, '0', 195, 104, 35, 20)
QtBind.createLabel(gui, 'MP', 130, 130)
txtMP = QtBind.createLineEdit(gui, '0', 195, 128, 35, 20)
QtBind.createLabel(gui, 'Block R.', 130, 154)
txtBLOCK = QtBind.createLineEdit(gui, '0', 195, 152, 35, 20)
QtBind.createLabel(gui, 'Parry R.', 130, 178)
txtER = QtBind.createLineEdit(gui, '0', 195, 176, 35, 20)

QtBind.createLabel(gui, 'Delay (ms):', 250, 106)
txtDelay = QtBind.createLineEdit(gui, '2000', 320, 104, 55, 20)
QtBind.createButton(gui, 'on_save', '  Save  ', 250, 130)

btnStartStop = QtBind.createButton(gui, 'on_startstop', '     Start     ', 250, 172)
lblStatus = QtBind.createLabel(gui, 'stopped', 250, 204)

QtBind.createLabel(gui, 'History:', 10, 204)
lstResult = QtBind.createList(gui, 10, 222, 380, 40)

# right panel: current blue lines (updated every wheel)
BLUE_ORDER = [('STR', 'STR'), ('INT', 'INT'), ('HP', 'HP'), ('MP', 'MP'),
			  ('CRIT', 'Crit'), ('BLOCK', 'Block'), ('HR', 'AtkR'), ('ER', 'ParR')]
QtBind.createLabel(gui, 'Blue:', 410, 84)
lstBlue = QtBind.createList(gui, 410, 104, 85, 158)
QtBind.createLabel(gui, 'Queue (remaining):', 505, 84)
lstQueue = QtBind.createList(gui, 505, 104, 180, 158)

# ---- Footer ----
QtBind.createLabel(gui, 'Author : CK3 @ iLog1K.net', 10, 272)

# condition key -> GUI field mapping
COND_FIELDS = [
	('STR', 'txtSTR'), ('INT', 'txtINT'), ('CRIT', 'txtCRIT'), ('HR', 'txtHR'),
	('HP', 'txtHP'), ('MP', 'txtMP'), ('BLOCK', 'txtBLOCK'), ('ER', 'txtER'),
]


# ============================================================================
# Helpers
# ============================================================================
def set_status(txt):
	QtBind.setText(gui, lblStatus, 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 update_blues(values):
	try:
		QtBind.clear(gui, lstBlue)
		for _k, _d in BLUE_ORDER:
			QtBind.append(gui, lstBlue, '%s = %d' % (_d, values.get(_k, 0)))
	except Exception:
		pass


def delay_sec():
	try:
		return max(0.0, int(QtBind.text(gui, txtDelay).strip()) / 1000.0)
	except Exception:
		return 2.0


def read_conditions():
	out = {}
	for key, fld in COND_FIELDS:
		try:
			out[key] = max(0, int(QtBind.text(gui, globals()[fld]).strip()))
		except Exception:
			out[key] = 0
	return out


def attr_key(name):
	"""magicoption servername -> tracked attribute key (or None)."""
	if not name:
		return None
	n = name.upper()
	if 'MATTR_STR' in n:
		return 'STR'
	if 'MATTR_INT' in n:
		return 'INT'
	if 'MATTR_REGENHPMP' in n:
		return None
	if 'MATTR_HP' in n:
		return 'HP'
	if 'MATTR_MP' in n:
		return 'MP'
	if 'EVADE_CRITICAL' in n:
		return None
	if 'MATTR_CRITICAL' in n:
		return 'CRIT'
	if 'MATTR_BLOCKRATE' in n:
		return 'BLOCK'
	if 'MATTR_HR' in n:
		return 'HR'
	if 'MATTR_ER' in n:
		return 'ER'
	return None


# ============================================================================
# Database (resolve magic line id -> servername)
# ============================================================================
def _db3_files():
	try:
		return [os.path.join(DATA_DIR, fn) for fn in os.listdir(DATA_DIR)
				if fn.lower().endswith('.db3')]
	except Exception:
		return []


def _lookup(con, mid):
	try:
		r = con.cursor().execute('SELECT name FROM magicoption WHERE id=?', (mid,)).fetchone()
		return r[0] if r else None
	except Exception:
		return None


def magic_name(mid):
	"""Pick AUTOMATICALLY the db3 that resolves the current server's ids."""
	global _db
	if _db is not None:
		n = _lookup(_db, mid)
		if n:
			return n
	for f in _db3_files():
		try:
			con = sqlite3.connect(f, check_same_thread=False)
		except Exception:
			continue
		n = _lookup(con, mid)
		if n:
			if _db is not None:
				try:
					_db.close()
				except Exception:
					pass
			_db = con
			log('[%s] Matched DB: %s' % (pName, os.path.basename(f)))
			return n
		try:
			con.close()
		except Exception:
			pass
	return None


# ============================================================================
# Config
# ============================================================================
def on_save(*_):
	try:
		data = {'delay': int(delay_sec() * 1000)}
		for key, fld in COND_FIELDS:
			data[key] = QtBind.text(gui, globals()[fld])
		with open(CONFIG_FILE, 'w') as f:
			json.dump(data, 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, txtDelay, str(data.get('delay', 2000)))
			for key, fld in COND_FIELDS:
				QtBind.setText(gui, globals()[fld], str(data.get(key, '0')))
	except Exception as e:
		log('[%s] Config read error: %s' % (pName, e))


# ============================================================================
# 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:
			continue
		try:
			ref = get_item(item['model'])
		except Exception:
			ref = None
		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.' % (pName, idx))


def selected_item_slot():
	if not current_items:
		return None
	return current_items.get(QtBind.currentIndex(gui, combo_items))


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_ITEM_NAME in (item.get('name') or ''):
			return slot
	return -1


# ============================================================================
# Start / Stop
# ============================================================================
def on_startstop():
	global started, qi
	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
		if all(v == 0 for v in read_conditions().values()):
			log('[%s] Set at least one condition (> 0).' % pName)
			return
		started = True
		qi = 0
		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
# ============================================================================
def use_wheel():
	global attempts
	if not started:
		return
	item_slot = current_target_slot()
	if item_slot is None:
		stop()
		return
	wheel_slot = get_wheel_slot()
	if wheel_slot == -1:
		log('[%s] No %s left in inventory. Stopping.' % (pName, WHEEL_ITEM_NAME))
		stop()
		return
	p = b'\x02\x19\x02' + struct.pack('B', item_slot) + struct.pack('B', wheel_slot)
	inject_joymax(WHEEL_OPCODE, p, False)
	attempts += 1
	log('[%s] %s #%d (item slot %d).' % (pName, WHEEL_ITEM_NAME, attempts, item_slot))


# ============================================================================
# Response: count lines per attribute and check the conditions
# ============================================================================
def eval_conditions(counts):
	"""AND over every condition set (> 0). Returns (all_met, progress_text).

	all_met is True only when at least one condition is set AND every set
	condition has enough lines (e.g. STR 2 AND INT 2 -> both must reach the
	target, not just one of them)."""
	conds = read_conditions()
	parts = []
	any_set = False
	all_met = True
	for key, _fld in COND_FIELDS:
		needed = conds.get(key, 0)
		if needed > 0:
			any_set = True
			have = counts.get(key, 0)
			if have < needed:
				all_met = False
			parts.append('%s %d/%d' % (key, have, needed))
	return (any_set and all_met), ('  '.join(parts) if parts else '(no conditions set)')


def handle_joymax(opcode, data):
	global _timer, qi
	if opcode == WHEEL_RESULT_OPCODE and started:
		try:
			is_success = data[2]
			if is_success == 1 and len(data) > MAGICLINES_OFFSET:
				n_lines = data[MAGICLINES_OFFSET]
				counts = {}
				values = {}
				idx = MAGICLINES_OFFSET + 1
				for x in range(n_lines):
					if idx + 8 > len(data):
						break
					mid = struct.unpack_from('<I', data, idx)[0]
					amount = struct.unpack_from('<I', data, idx + 4)[0]
					idx += 8  # id (4) + amount (4)
					if x == 0:
						continue  # first line is the base one
					sn = magic_name(mid)
					key = attr_key(sn)
					if key:
						counts[key] = counts.get(key, 0) + 1
						values[key] = values.get(key, 0) + amount

				summary = ' '.join('%s:%d (%dp)' % (k, counts[k], values.get(k, 0)) for k in counts) or '(no tracked attributes)'
				met, progress = eval_conditions(counts)
				add_history('Attempt %d:  %s' % (attempts, summary))
				set_status('attempt %d  |  %s' % (attempts, progress))
				log('[%s] Result #%d (raw lines=%d): %s  ->  %s' % (pName, attempts, n_lines, summary, progress))
				update_blues(values)

				if met:
					log('[%s] Item done: %s (all conditions met).' % (pName, progress))
					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:
				if started:
					_timer = Timer(delay_sec(), use_wheel)
					_timer.start()
		except Exception as e:
			log('[%s] 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.