plugin iSRO-R v2.6

Shadow Dungeon

by @CK3 · 2d ago · updated 2d ago · 15 views

Shadow Dungeon Automation Plugin
Take your dungeon farming efficiency to the next level. This specialized plugin fully automates your Shadow Dungeon runs, offering granular control over Unique monster targeting and smart party management.

Key Features

  • Built-in Script Engine
  • Integrated directly into the build, allowing for reliable execution and smooth automation without relying on external setups.
  • Custom Unique Selection
  • Choose exactly which Uniques you want to spawn during your run. Skip unwanted targets to optimize your time and resources.

Smart Party & Exit Automation

  • Auto-Party Leave: As soon as your targeted Uniques are defeated, the secondary character automatically drops out of the party.
  • Instant Dungeon Exit: The party drop immediately triggers the main character's exit from the Shadow Dungeon, readying both accounts for the next run seamlessly.

* NEED TWO CHARACTERS ON SAME PC *

Changelog

v2.6 latest files only

Released with files, no release notes written yet.

ShadowDundeon.py v2.6 python · 756 lines · 24,046 B · 2d ago Download
# -*- coding: utf-8 -*-
#
# ShadowDungeon - plugin pentru phBot  (iSRO-R)  [pe AMBELE conturi, acelasi PC]
# -----------------------------------------------------------------------------
# - Shadow Script: scripturile de start (7 orase) sunt INCLUSE in plugin si se
#   scriu automat in  <phBot>\Shadow Scripts\.  Alegi orasul -> Load Script.
# - UNIQUES TO SPAWN: cand NU folosesti un script custom, alegi cati unici de
#   fiecare tip sa fie spawnati (ex. 3 Uruchi + 2 Tigerwoman). Scriptul orasului
#   se reconstruieste la Load Script cu numarul ales.
# - GRIDING CHARACTER: cand toti unicii (rarity==3) au fost omorati -> semnal.
# - LEAVE CHARACTER : cand apare semnalul -> leave party (0x7061).
# Author : CK3 @ iLog1K.net
#
from phBot import *
import QtBind
import os
import json
import time
import tempfile
import sqlite3

pName = 'ShadowDungeon'
pVersion = '2.6'

SIGNAL_FILE = os.path.join(tempfile.gettempdir(), 'phbot_party_leave_signal.txt')
UNIQUE_RARITY = 3
LEAVE_OPCODE = 0x7061
INSTALL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(INSTALL_DIR, 'Data')
SHADOW_DIR = os.path.join(INSTALL_DIR, 'Shadow Scripts')
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), pName + '_common.json')
_mdb = None

# stare HUNTER
seen_unique = False
ready_since = 0.0
signaled = False
# stare LEAVER
_last_mtime = 0.0
# statistici
stat_used = 0     # tickete consumate de la pornire/reset
_tk_last = None   # ultimul numar de tickete vazut (pt. delta)
# limba + orase
lang = 'EN'
city_map = {}   # index combo -> (nume oras, cale script)
active_path = ''  # ultimul script incarcat in Training (per character)
_char = ''        # numele characterului curent (setari salvate per character)

# ============================================================================
# Scripturile Shadow (incluse in plugin) - se scriu in <phBot>\Shadow Scripts\
# ============================================================================
# Partea comuna DUPA mersul pana la Priest: teleport in dungeon, walk, si NPC-ul
# de start. Liniile care spawneaza unicii se adauga dinamic din UNIQUES/counts.
_TAIL_HEAD = (
	'teleport,Mysterious Priest,Sealed Dungeon of Vicious Shadows\n'
	'walk,-32742,-19486,98,-134\n'
	'npc,Sacrificed Slave\n'
)

# Unicii disponibili:  (nume scurt pentru GUI, linia npc din script)
# O linie "npc,Sealing Stone of X" = un spawn. Repetata de N ori = N spawn-uri.
UNIQUES = [
	('Tigerwoman',  'Sealing Stone of Tigerwoman'),
	('Cerberus',    'Sealing Stone of Cerberus'),
	('Captain Ivy', 'Sealing Stone of Captain Ivy'),
	('Uruchi',      'Sealing Stone of Uruchi'),
	('Isyutaru',    'Sealing Stone of Isyutaru'),
]
DEFAULT_COUNTS = {short: 1 for short, _npc in UNIQUES}

# Prefix de mers pana la Mysterious Priest, per oras.
CITY_WALK = {
	'Jangan':
		'walk,6438,1080,-32\nwalk,6444,1065,-32\nwalk,6443,1057,-32\n',
	'Donwhang':
		'walk,3551,2082,-106\nwalk,3555,2086,-106\nwalk,3560,2089,-106\n',
	'Hotan':
		'walk,105,25,244\nwalk,94,42,244\nwalk,92,57,244\nwalk,97,60,244\nwalk,103,59,244\n',
	'Samarkand':
		'walk,-5155,2838,180\nwalk,-5153,2850,180\nwalk,-5153,2859,180\nwalk,-5154,2869,180\n',
	'Constantinople':
		'walk,-10663,2608,83\nwalk,-10669,2612,83\nwalk,-10675,2611,83\nwalk,-10679,2611,83\n',
	'Alexandria South':
		'walk,-16645,-324,863\nwalk,-16647,-316,863\nwalk,-16649,-305,863\nwalk,-16649,-298,863\n',
	'Baghdad':
		'walk,-8526,-744,-236\nwalk,-8522,-756,-236\nwalk,-8515,-772,-236\nwalk,-8508,-780,-236\n'
		'walk,-8493,-786,-236\nwalk,-8478,-796,-236\nwalk,-8471,-805,-236\nwalk,-8472,-812,-235\n'
		'walk,-8472,-836,-319\nwalk,-8474,-855,-319\nwalk,-8477,-877,-319\nwalk,-8478,-883,-313\n'
		'walk,-8478,-907,-432\nwalk,-8479,-935,-433\nwalk,-8479,-963,-433\nwalk,-8482,-983,-433\n'
		'walk,-8474,-987,-433\n',
}


def build_body(city, counts):
	"""Construieste corpul scriptului pt. un oras cu numarul de unici ales."""
	walk = CITY_WALK.get(city, '')
	npc_lines = ''
	for short, npc in UNIQUES:
		try:
			n = int(counts.get(short, 0) or 0)
		except Exception:
			n = 0
		if n < 0:
			n = 0
		npc_lines += ('npc,%s\n' % npc) * n
	return walk + _TAIL_HEAD + npc_lines


def ensure_scripts():
	"""Scrie scripturile incluse in <phBot>\\Shadow Scripts\\ daca lipsesc."""
	try:
		if not os.path.isdir(SHADOW_DIR):
			os.makedirs(SHADOW_DIR)
	except Exception as e:
		log('[%s] Nu pot crea %s : %s' % (pName, SHADOW_DIR, e))
		return
	for city in CITY_WALK:
		path = os.path.join(SHADOW_DIR, 'Chamber %s.txt' % city)
		if not os.path.isfile(path):
			try:
				with open(path, 'w') as f:
					f.write(build_body(city, DEFAULT_COUNTS))
			except Exception as e:
				log('[%s] Nu pot scrie %s : %s' % (pName, path, e))


# ============================================================================
# Texte (EN / RO)
# ============================================================================
GUIDE = {
	'EN': [
		'=====  HOW TO USE  =====',
		'Load on BOTH accounts.',
		'',
		'SHADOW SCRIPT',
		'  Pick a city -> Load Script.',
		'  Own route? Custom Script.',
		'  Then start botting.',
		'',
		'UNIQUES TO SPAWN',
		'  City scripts only.',
		'  Set a count per unique,',
		'  e.g. 3 Uruchi + 2 Tiger.',
		'  Load Script rebuilds it.',
		'',
		'ROLE & SIGNAL',
		'  Grind acc: tick GRIDING.',
		'  Leaver acc: tick LEAVE.',
		'',
		'  Final Trigger = delay',
		'  after last unique dies.',
		'  All dead -> signal ->',
		'  other acc leaves party.',
	],
	'RO': [
		'===  CUM SE FOLOSESTE  ===',
		'Pune-l pe AMBELE conturi.',
		'',
		'SHADOW SCRIPT',
		'  Alegi oras -> Load Script.',
		'  Ruta ta? Custom Script.',
		'  Apoi pornesti botting.',
		'',
		'UNIQUES TO SPAWN',
		'  Doar scripturi de oras.',
		'  Numar pt. fiecare unic,',
		'  ex. 3 Uruchi + 2 Tiger.',
		'  Load Script reface ruta.',
		'',
		'ROLE & SIGNAL',
		'  Grind: bifeaza GRIDING.',
		'  Leaver: bifeaza LEAVE.',
		'',
		'  Final Trigger = pauza',
		'  dupa ultimul unic mort.',
		'  Toti morti -> semnal ->',
		'  celalalt iese din party.',
	],
}

MSG = {
	'wait_uniques': {'EN': 'waiting for uniques', 'RO': 'astept unicii'},
	'killing':      {'EN': 'uniques present - killing', 'RO': 'unici prezenti - se omoara'},
	'countdown':    {'EN': 'all killed -> signal in %ds', 'RO': 'omorati -> semnal in %ds'},
	'signaled':     {'EN': 'SIGNALED (all uniques dead)', 'RO': 'SEMNALAT (toti unicii morti)'},
	'wait_signal':  {'EN': 'waiting for signal', 'RO': 'astept semnal'},
	'left':         {'EN': 'left party (signal received)', 'RO': 'am iesit din party (semnal)'},
	'inactive':     {'EN': 'inactive (enable a role)', 'RO': 'inactiv (bifeaza un rol)'},
	'loaded':       {'EN': 'script loaded: %s', 'RO': 'script incarcat: %s'},
}


def tr(key, *args):
	t = MSG.get(key, {}).get(lang, key)
	return (t % args) if args else t


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

LX = 14          # x coloana stanga
RX = 350         # x coloana dreapta (dashboard + ghid)
FLD = 172        # x aliniere campuri (input-uri) in stanga


def _hdr(text, n=52):
	t = '  %s  ' % text
	side = max(2, (n - len(t)) // 2)
	line = '─' * side
	return line + t + line


# ---- Titlu + versiune ----
QtBind.createLabel(gui, 'S H A D O W   D U N G E O N          v%s' % pVersion, LX, 8)

# ---- Card 1: SHADOW SCRIPT (stanga sus) ----
QtBind.createLabel(gui, _hdr('SHADOW SCRIPT'), LX, 34)
QtBind.createLabel(gui, 'Start city:', LX + 6, 58)
combo_city = QtBind.createCombobox(gui, 88, 56, 150, 22)
QtBind.createButton(gui, 'on_refresh_cities', 'Refresh', 246, 56)
QtBind.createButton(gui, 'on_load_script', ' Load Script ', LX + 6, 84)
QtBind.createButton(gui, 'on_browse_custom', ' Custom Script ', 118, 84)
lblActive = QtBind.createLabel(gui, 'Active:  -', 222, 88)

# ---- Card 2: ROLE & SIGNAL (stanga mijloc) ----
QtBind.createLabel(gui, _hdr('ROLE & SIGNAL'), LX, 112)
QtBind.createLabel(gui, 'Final Trigger (sec):', LX + 6, 136)
txtSeconds = QtBind.createLineEdit(gui, '15', FLD, 134, 45, 20)
cbxHunter = QtBind.createCheckBox(gui, 'on_save', 'GRIDING CHARACTER (send signal)', LX + 6, 164)
QtBind.createButton(gui, 'on_save', '  Save  ', 236, 162)
cbxLeaver = QtBind.createCheckBox(gui, 'on_save', 'LEAVE CHARACTER (leave signal)', LX + 6, 190)
QtBind.createButton(gui, 'on_test_leave', '  Test leave  ', 236, 188)

# ---- Card 3: STATISTICS (stanga jos) ----
QtBind.createLabel(gui, _hdr('STATISTICS'), LX, 214)
lblTickets = QtBind.createLabel(gui, 'Tickets left:  -', LX + 6, 238)
lblUsed = QtBind.createLabel(gui, 'Used (session):  0', LX + 6, 258)
QtBind.createButton(gui, 'on_reset_stats', '  Reset  ', 236, 248)

# ---- Subsol stanga ----
QtBind.createLabel(gui, 'Author : CK3 @ iLog1K.net', LX, 286)

# ---- Dashboard (dreapta sus, compact) ----
QtBind.createLabel(gui, _hdr('DASHBOARD', 30), RX, 6)
lblProfile = QtBind.createLabel(gui, 'Profile:  -', RX + 6, 28)
lblRole = QtBind.createLabel(gui, 'Role:  -', RX + 6, 46)
lblStatus = QtBind.createLabel(gui, 'Status:  -', RX + 6, 64)
btnLang = QtBind.createButton(gui, 'on_lang', '  Lang: EN  ', RX + 6, 84)

# ---- Card: UNIQUES TO SPAWN (dreapta, sub-coloana stanga) ----
UX = RX          # x sub-coloana unici
QtBind.createLabel(gui, _hdr('UNIQUES TO SPAWN', 28), UX, 112)
unique_edits = []
_uy = 136
for _short, _npc in UNIQUES:
	QtBind.createLabel(gui, _short, UX + 6, _uy)
	unique_edits.append(QtBind.createLineEdit(gui, '1', UX + 116, _uy - 2, 40, 20))
	_uy += 22
lblUTotal = QtBind.createLabel(gui, 'Total: 5  (0 = skip)', UX + 6, _uy + 2)

# ---- Ghid (dreapta de tot, coloana inalta si ingusta) ----
GX = RX + 165    # x sub-coloana ghid
QtBind.createLabel(gui, _hdr('GUIDE', 18), GX, 6)
lstGuide = QtBind.createList(gui, GX, 26, 180, 258)


def set_status(txt):
	QtBind.setText(gui, lblStatus, 'Status:  ' + txt)


def update_dashboard():
	"""Actualizeaza Profile + Role in dashboard."""
	try:
		QtBind.setText(gui, lblProfile, 'Profile:  ' + (_char or '-'))
		h = QtBind.isChecked(gui, cbxHunter)
		l = QtBind.isChecked(gui, cbxLeaver)
		if h and l:
			role = 'BOTH (?!)'
		elif h:
			role = 'GRINDING (sends signal)'
		elif l:
			role = 'LEAVER (leaves party)'
		else:
			role = '- pick a role -'
		QtBind.setText(gui, lblRole, 'Role:  ' + role)
	except Exception:
		pass


# ============================================================================
# Unici: citire / setare campuri + total
# ============================================================================
def read_counts():
	"""Numarul ales pentru fiecare unic (dict short -> int >= 0)."""
	c = {}
	for i, (short, _npc) in enumerate(UNIQUES):
		try:
			v = int(QtBind.text(gui, unique_edits[i]).strip())
		except Exception:
			v = 0
		if v < 0:
			v = 0
		c[short] = v
	return c


def set_counts(counts):
	counts = counts or {}
	for i, (short, _npc) in enumerate(UNIQUES):
		try:
			v = int(counts.get(short, DEFAULT_COUNTS[short]))
		except Exception:
			v = DEFAULT_COUNTS[short]
		if v < 0:
			v = 0
		QtBind.setText(gui, unique_edits[i], str(v))


def update_uniques_total():
	try:
		total = sum(read_counts().values())
		QtBind.setText(gui, lblUTotal, 'Total: %d  (0 = skip)' % total)
	except Exception:
		pass


TICKET_MATCH = 'vicious shadows'   # "Chamber of Vicious Shadows Entrance Ticket"


def count_tickets():
	"""Numara ticketele de intrare in Shadow din inventar (dupa nume)."""
	total = 0
	try:
		inv = get_inventory()
		items = inv.get('items') if isinstance(inv, dict) else None
		if items:
			for it in items:
				if isinstance(it, dict) and TICKET_MATCH in (it.get('name') or '').lower():
					total += int(it.get('quantity', 1) or 1)
	except Exception:
		pass
	return total


def update_stats():
	"""Actualizeaza ticketele ramase si cele consumate (delta la scadere)."""
	global _tk_last, stat_used
	cur = count_tickets()
	if _tk_last is None:
		_tk_last = cur
	elif cur < _tk_last:
		stat_used += (_tk_last - cur)
	_tk_last = cur
	try:
		QtBind.setText(gui, lblTickets, 'Tickets left:  %d' % cur)
		QtBind.setText(gui, lblUsed, 'Used (session):  %d' % stat_used)
	except Exception:
		pass


def on_reset_stats():
	global stat_used, _tk_last
	stat_used = 0
	_tk_last = count_tickets()
	update_stats()
	log('[%s] Statistics reset.' % pName)


def render_guide():
	try:
		QtBind.clear(gui, lstGuide)
		for line in GUIDE.get(lang, GUIDE['EN']):
			QtBind.append(gui, lstGuide, line)
	except Exception:
		pass


def seconds_limit():
	try:
		return max(1, int(QtBind.text(gui, txtSeconds).strip()))
	except Exception:
		return 15


# ============================================================================
# Shadow Script (selectare oras / custom -> Training)
# ============================================================================
def _city_from_filename(fn):
	city = fn[:-4] if fn.lower().endswith('.txt') else fn
	if city.lower().startswith('chamber '):
		city = city[8:]
	return city


def on_refresh_cities():
	ensure_scripts()
	city_map.clear()
	QtBind.clear(gui, combo_city)
	idx = 0
	try:
		files = sorted(os.listdir(SHADOW_DIR)) if os.path.isdir(SHADOW_DIR) else []
	except Exception:
		files = []
	for fn in files:
		if fn.lower().endswith('.txt'):
			city = _city_from_filename(fn)
			city_map[idx] = (city, os.path.join(SHADOW_DIR, fn))
			QtBind.append(gui, combo_city, city)
			idx += 1
	if idx == 0:
		log('[%s] Niciun script in %s' % (pName, SHADOW_DIR))


def _load_training(path):
	path = (path or '').strip().strip('"')
	if not path or not os.path.isfile(path):
		log('[%s] Script inexistent: %s' % (pName, path))
		return
	try:
		ok = set_training_script(path)
		name = _city_from_filename(os.path.basename(path))
		log('[%s] Script incarcat: %s (%s)' % (pName, name, ok))
		QtBind.setText(gui, lblActive, 'Active: ' + name)
		set_status(tr('loaded', name))
		_save_active(path)
	except Exception as e:
		log('[%s] set_training_script error: %s' % (pName, e))


def on_load_script():
	sel = city_map.get(QtBind.currentIndex(gui, combo_city))
	if not sel:
		log('[%s] Alege un oras (Refresh).' % pName)
		return
	city, path = sel
	# Oras cunoscut -> reconstruieste scriptul cu unicii alesi.
	if city in CITY_WALK:
		counts = read_counts()
		if sum(counts.values()) <= 0:
			log('[%s] Alege cel putin un unic (toate sunt 0).' % pName)
			return
		path = os.path.join(SHADOW_DIR, 'Chamber %s.txt' % city)
		try:
			with open(path, 'w') as f:
				f.write(build_body(city, counts))
			summary = ', '.join('%dx %s' % (counts[s], s) for s, _n in UNIQUES if counts[s] > 0)
			log('[%s] Script %s reconstruit: %s' % (pName, city, summary))
		except Exception as e:
			log('[%s] Nu pot scrie %s : %s' % (pName, path, e))
			return
	_load_training(path)


def _browse_file():
	"""Dialog nativ Windows (comdlg32) pt. un script custom."""
	try:
		import ctypes
		from ctypes import wintypes

		class OPENFILENAME(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),
			]

		buf = ctypes.create_unicode_buffer(1024)
		flt = ctypes.create_unicode_buffer('Script (*.txt)\0*.txt\0All (*.*)\0*.*\0\0')
		ofn = OPENFILENAME()
		ofn.lStructSize = ctypes.sizeof(OPENFILENAME)
		ofn.lpstrFile = ctypes.cast(buf, wintypes.LPWSTR)
		ofn.nMaxFile = 1024
		ofn.lpstrFilter = ctypes.cast(flt, wintypes.LPCWSTR)
		ofn.nFilterIndex = 1
		ofn.lpstrInitialDir = SHADOW_DIR if os.path.isdir(SHADOW_DIR) else INSTALL_DIR
		ofn.lpstrTitle = 'Alege scriptul Shadow (.txt)'
		ofn.Flags = 0x00000800 | 0x00001000
		if ctypes.windll.comdlg32.GetOpenFileNameW(ctypes.byref(ofn)):
			return buf.value
		return ''
	except Exception as e:
		log('[%s] Browse indisponibil: %s' % (pName, e))
		return ''


def on_browse_custom():
	p = _browse_file()
	if p:
		_load_training(p)


# ============================================================================
# Config
# ============================================================================
def _char_name():
	try:
		cd = get_character_data() or {}
		return cd.get('name') or ''
	except Exception:
		return ''


def _read_all():
	"""Intreg JSON-ul: {'lang':..., 'chars': {name: {...}}}. Migreaza formatul vechi."""
	try:
		if os.path.isfile(CONFIG_FILE):
			with open(CONFIG_FILE, 'r') as f:
				data = json.load(f)
			if isinstance(data, dict):
				if 'chars' not in data:
					# format vechi (setari globale) -> pune-le ca default sub '_shared'
					old = {k: data.get(k) for k in ('seconds', 'hunter', 'leaver', 'active')}
					data = {'lang': data.get('lang', 'EN'), 'chars': {'_shared': old}}
				data.setdefault('lang', 'EN')
				data.setdefault('chars', {})
				return data
	except Exception as e:
		log('[%s] Load error: %s' % (pName, e))
	return {'lang': 'EN', 'chars': {}}


def on_save(*_):
	global _char
	try:
		data = _read_all()
		data['lang'] = lang
		_char = _char_name()          # numele curent, live
		key = _char or '_shared'
		data['chars'][key] = {
			'seconds': seconds_limit(),
			'hunter': bool(QtBind.isChecked(gui, cbxHunter)),
			'leaver': bool(QtBind.isChecked(gui, cbxLeaver)),
			'active': active_path,
			'uniques': read_counts(),
		}
		with open(CONFIG_FILE, 'w') as f:
			json.dump(data, f, indent=2)
		log('[%s] Saved for profile: %s' % (pName, key))
	except Exception as e:
		log('[%s] Save error: %s' % (pName, e))


def _save_active(path):
	global active_path
	active_path = path
	on_save()


def load_config():
	global lang, active_path, _char
	data = _read_all()
	lang = data.get('lang', 'EN')
	chars = data.get('chars', {})
	_char = _char_name()              # numele curent, live
	cfg = chars.get(_char) or chars.get('_shared') or {}
	QtBind.setText(gui, txtSeconds, str(cfg.get('seconds', 15)))
	QtBind.setChecked(gui, cbxHunter, bool(cfg.get('hunter', False)))
	QtBind.setChecked(gui, cbxLeaver, bool(cfg.get('leaver', False)))
	set_counts(cfg.get('uniques') or DEFAULT_COUNTS)
	update_uniques_total()
	active_path = cfg.get('active', '') or ''
	label = 'Active:  -'
	if active_path:
		label = 'Active: ' + _city_from_filename(os.path.basename(active_path))
	QtBind.setText(gui, lblActive, label)
	QtBind.setText(gui, btnLang, '  Lang: %s  ' % lang)
	render_guide()


def on_lang():
	global lang
	lang = 'RO' if lang == 'EN' else 'EN'
	QtBind.setText(gui, btnLang, '  Lang: %s  ' % lang)
	render_guide()
	on_save()


def _refresh_mtime():
	global _last_mtime
	try:
		_last_mtime = os.path.getmtime(SIGNAL_FILE) if os.path.isfile(SIGNAL_FILE) else 0.0
	except Exception:
		_last_mtime = 0.0


def joined_game():
	global _char
	_char = _char_name()
	log('[%s] Character: %s (settings per character).' % (pName, _char or '?'))
	load_config()
	_refresh_mtime()


# ============================================================================
# HUNTER: detectie unici
# ============================================================================
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 _db_rarity(model, name):
	global _mdb
	if _mdb is not None:
		try:
			r = _mdb.cursor().execute('SELECT rarity FROM monsters WHERE id=?', (model,)).fetchone()
			if r and isinstance(r[0], int):
				return r[0]
		except Exception:
			pass
	nl = (name or '').lower()
	for f in _db3_files():
		try:
			con = sqlite3.connect(f, check_same_thread=False)
			r = con.cursor().execute('SELECT name, rarity FROM monsters WHERE id=?', (model,)).fetchone()
		except Exception:
			continue
		if r and r[0] and (not nl or str(r[0]).lower() == nl):
			_mdb = con
			return r[1] if isinstance(r[1], int) else 0
		try:
			con.close()
		except Exception:
			pass
	return None


def monster_rarity(m):
	r = m.get('rarity')
	if isinstance(r, int):
		return r
	model = m.get('model')
	if model is not None:
		try:
			ref = get_monster(model)
			if ref and isinstance(ref.get('rarity'), int):
				return ref['rarity']
		except Exception:
			pass
		dr = _db_rarity(model, m.get('name'))
		if isinstance(dr, int):
			return dr
	return 0


def has_unique_nearby():
	try:
		mons = get_monsters() or {}
	except Exception:
		return False
	for _id, m in mons.items():
		if isinstance(m, dict) and monster_rarity(m) == UNIQUE_RARITY:
			return True
	return False


def send_signal():
	try:
		with open(SIGNAL_FILE, 'w') as f:
			f.write(str(time.time()))
		log('[%s] All uniques dead -> signal sent.' % pName)
	except Exception as e:
		log('[%s] Signal write error: %s' % (pName, e))


# ============================================================================
# LEAVER
# ============================================================================
def do_leave():
	try:
		inject_joymax(LEAVE_OPCODE, b'', False)
		log('[%s] LEAVE party sent (0x%04X).' % (pName, LEAVE_OPCODE))
		return True
	except Exception as e:
		log('[%s] Leave error: %s' % (pName, e))
		return False


def on_test_leave():
	do_leave()


# ============================================================================
# Bucla
# ============================================================================
def event_loop():
	global ready_since, signaled, _last_mtime, seen_unique
	hunter = QtBind.isChecked(gui, cbxHunter)
	leaver = QtBind.isChecked(gui, cbxLeaver)
	now = time.time()
	parts = []

	if hunter:
		if has_unique_nearby():
			seen_unique = True
			ready_since = 0.0
			signaled = False
			parts.append(tr('killing'))
		elif not seen_unique:
			ready_since = 0.0
			parts.append(tr('wait_uniques'))
		else:
			if ready_since == 0.0:
				ready_since = now
			elapsed = now - ready_since
			lim = seconds_limit()
			if elapsed >= lim:
				if not signaled:
					send_signal()
					signaled = True
					seen_unique = False
				parts.append(tr('signaled'))
			else:
				parts.append(tr('countdown', int(lim - elapsed + 1)))

	if leaver:
		try:
			mtime = os.path.getmtime(SIGNAL_FILE) if os.path.isfile(SIGNAL_FILE) else 0.0
		except Exception:
			mtime = _last_mtime
		if mtime > _last_mtime:
			_last_mtime = mtime
			log('[%s] Signal received -> leave party.' % pName)
			do_leave()
			parts.append(tr('left'))
		else:
			parts.append(tr('wait_signal'))

	set_status(' | '.join(parts) if parts else tr('inactive'))
	update_dashboard()
	update_uniques_total()
	update_stats()


# ============================================================================
# Init
# ============================================================================
ensure_scripts()
on_refresh_cities()
load_config()
_refresh_mtime()
update_stats()
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.