Released with files, no release notes written yet.
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
▸ ShadowDundeon.py v2.6 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.