import QtBind from threading import Timer from phBot import * import os import phBotChat import time import sqlite3 import glob import urllib.request import re # ============================================================ # ============================================================ Plugin = 'BetterTraining' PluginVersion = '1.00' # ============================================================ # Globals # ============================================================ enabled = False solo = False members = 8 levelOffset = 0 spots = [] use_monstername_as_name = False use_mob_level = False # Character specific settings character_settings = {} loading_character_settings = False current_character = None hometown_enabled = False buy_items_enabled = False buy_keep_town_gold_enabled = False BUY_GOLD_THRESHOLD = 0 # Area change state area_change_pending = False pending_area = None area_change_timer = None area_change_stage = 0 # 1 = local STOP -> delay 1 # 2 = local CHANGE AREA/RANGE -> delay 2 # 3 = local START done -> chat STOP -> delay 1 # 4 = forced town return -> wait 10000 ms -> local START # 5 = chat SETPOS/SETRADIUS -> delay 2 area_change_death_remaining = None DEATH_WAIT_TIMEOUT = 30 RETURN_TO_TOWN_START_DELAY = 10 # seconds # Chat Control integration xcontrol_enabled = False xcontrol_stop_command = 'STOP' xcontrol_stop_delay_ms = 1000 xcontrol_setpos_command = 'SETPOS x% y% r% z%' xcontrol_setrange_command = 'SETRADIUS %Range' xcontrol_start_delay_ms = 1000 xcontrol_start_command = 'START' CONFIG_FILE = os.path.join( os.path.dirname(os.path.abspath(__file__)), Plugin + '_Config.txt' ) # ============================================================ # GUI Tab 1 # ============================================================ tab1 = QtBind.init(__name__, Plugin) x = -1 y = 0 lbl_version = QtBind.createLabel(tab1, ' Version: v' + PluginVersion + ' ', 538, -2) QtBind.createLabel( tab1, 'Enable Spot Changer', x + 25, y + 6 ) checkEnable = QtBind.createCheckBox( tab1, 'checkEnable_clicked', '', x - 2, y ) checkSolo = QtBind.createCheckBox( tab1, 'checkSolo_clicked', 'Solo', x - 2, y + 20 ) # ============================================================ # Spot Editor # ============================================================ y = 45 QtBind.createLabel(tab1, 'Race:', x + 3, y) comboRace = QtBind.createCombobox( tab1, x + 45, y - 3, 80, 20 ) QtBind.append(tab1, comboRace, 'All') QtBind.append(tab1, comboRace, 'Europe') QtBind.append(tab1, comboRace, 'Chinese') y += 21 QtBind.createLabel(tab1, 'Level:', x + 3, y) editLevel = QtBind.createLineEdit( tab1, '1', x + 45, y - 3, 80, 20 ) checkMobLevel = QtBind.createCheckBox( tab1, 'useMobLevel_clicked', 'Use Mob Lvl', x + 122, y - 5 ) y += 21 QtBind.createLabel(tab1, 'Name:', x + 3, y) editName = QtBind.createLineEdit( tab1, '', x + 45, y - 3, 80, 20 ) checkMonsterName = QtBind.createCheckBox( tab1, 'useMonsterName_clicked', 'Use Mobname', x + 122, y - 5 ) y += 21 QtBind.createLabel(tab1, 'Range:', x + 3, y) editRange = QtBind.createLineEdit( tab1, '75', x + 45, y - 3, 80, 20 ) y += 21 buttonAdd = QtBind.createButton( tab1, 'addSpot_clicked', 'Add Current Position', x, y ) buttonDelete = QtBind.createButton( tab1, 'deleteSpot_clicked', 'Delete Selected', x + 125, y ) # ============================================================ # Spot List # ============================================================ listX = 230 listY = 5 QtBind.createLabel( tab1, 'Training Spots:', listX, listY ) SPOT_HEADER = 'ID Lvl Race Spot (Region,X,Y,Z) Range Name' QtBind.createLabel( tab1, SPOT_HEADER, listX + 5, listY + 22 ) spotList = QtBind.createList( tab1, listX - 5, listY + 35, 495, 200 ) # ============================================================ # Action Buttons (Walk, Stop) # ============================================================ settingsY = 155 btn_walk = QtBind.createButton(tab1, 'btn_walk_clicked', ' Walk to Position ', 0, settingsY - 3) btn_stop = QtBind.createButton(tab1, 'btn_stop_clicked', ' Stop ', 125, settingsY - 3) settingsY += 24 QtBind.createLabel( tab1, 'Party Members:', 3, settingsY ) editMembers = QtBind.createLineEdit( tab1, '8', 85, settingsY - 3, 30, 20 ) settingsY += 24 QtBind.createLabel( tab1, 'Level Offset:', 3, settingsY ) editLevelOffset = QtBind.createLineEdit( tab1, '0', 85, settingsY - 3, 30, 20 ) # ============================================================ # Chat Control Spot Change # ============================================================ y += 122 QtBind.createLabel(tab1, 'Send spot change command via party chat. Variables( Y= %y / X= %x / Z= %z / Region= %r / Range= %Range )', x + 25, y + 6) checkXControl = QtBind.createCheckBox( tab1, 'checkXControl_clicked', '', x - 2, y ) QtBind.createLabel(tab1, 'Stop Bot Command', x + 8, y + 25) editXControlStop = QtBind.createLineEdit( tab1, 'STOP', x, y + 40, 110, 20 ) QtBind.createLabel(tab1, 'Delay (ms)', x + 115, y + 25) editXControlDelay1 = QtBind.createLineEdit( tab1, '1000', x + 113, y + 40, 60, 20 ) QtBind.createLabel(tab1, 'Spot change Command', x + 205, y + 25) editXControlSetPos = QtBind.createLineEdit( tab1, 'SETPOS x% y% r% z%', x + 176, y + 40, 185, 20 ) QtBind.createLabel(tab1, 'Range change Command', x + 390, y + 25) editXControlSetRange = QtBind.createLineEdit( tab1, 'SETRADIUS %Range', x + 365, y + 40, 185, 20 ) QtBind.createLabel(tab1, 'Delay (ms)', x + 558, y + 25) editXControlDelay2 = QtBind.createLineEdit( tab1, '1000', x + 555, y + 40, 60, 20 ) QtBind.createLabel(tab1, 'Start Bot Command', x + 622, y + 25) editXControlStart = QtBind.createLineEdit( tab1, 'START', x + 620, y + 40, 98, 20 ) buttonSaveXControl = QtBind.createButton( tab1, 'saveXControl_clicked', 'Save', x + 722, y + 38 ) # ============================================================ # Helper # ============================================================ def get_character_name(): try: data = get_character_data() if not data: return None if 'name' in data: return data['name'] except: pass return None def get_server_name(): try: data = get_character_data() or {} for key in ('server', 'servername', 'server_name'): value = data.get(key) if value: return str(value).strip() except Exception: pass try: data = get_client() or {} for key in ('server', 'servername', 'server_name'): value = data.get(key) if value: return str(value).strip() except Exception: pass return 'UnknownServer' def get_character_key(character=None, server=None): character = str(character or get_character_name() or '').strip() server = str(server or get_server_name() or 'UnknownServer').strip() if not character: return None return '%s_%s' % (server, character) # ============================================================ # Default Character Settings # ============================================================ def get_default_character_settings(): return { 'enabled': False, 'solo': False, 'members': 8, 'levelOffset': 0, 'xcontrol_enabled': False, 'xcontrol_stop_command': 'STOP', 'xcontrol_stop_delay_ms': 500, 'xcontrol_setpos_command': 'SETPOS x% y% r% z%', 'xcontrol_setrange_command': 'SETRADIUS %Range', 'xcontrol_start_delay_ms': 500, 'xcontrol_start_command': 'START', 'hometown_enabled': False, 'buy_items_enabled': False, 'buy_gold_threshold': 0, 'buy_keep_town_gold_enabled': False, 'bought_items': [], 'hometown_rules': _hometown_character_defaults() if 'hometown_rules' in globals() else [dict(r) for r in HOMETOWN_DEFAULTS] } def finish_loading_character_settings(): global loading_character_settings loading_character_settings = False # ============================================================ # Load & Save Character Settings # ============================================================ def load_character_settings(): global loading_character_settings global enabled global solo global members global levelOffset global current_character global hometown_enabled global buy_items_enabled global BUY_GOLD_THRESHOLD global buy_keep_town_gold_enabled global xcontrol_enabled global xcontrol_stop_command global xcontrol_stop_delay_ms global xcontrol_setpos_command global xcontrol_setrange_command global xcontrol_start_delay_ms global xcontrol_start_command global auto_settings_signature character_name = get_character_name() if not character_name: return character = get_character_key(character_name) if not character: return legacy_name = character_name migrated_legacy_runtime = False if legacy_name in character_settings and legacy_name != character: legacy_settings = character_settings.pop(legacy_name) if character not in character_settings: character_settings[character] = legacy_settings elif isinstance(legacy_settings, dict): for key, value in legacy_settings.items(): if key not in character_settings[character]: character_settings[character][key] = value migrated_legacy_runtime = True current_character = character loading_character_settings = True if character not in character_settings: character_settings[character] = get_default_character_settings() settings = character_settings[character] enabled = settings['enabled'] hometown_enabled = bool(settings.get('hometown_enabled', True)) buy_items_enabled = bool(settings.get('buy_items_enabled', False)) buy_keep_town_gold_enabled = bool(settings.get('buy_keep_town_gold_enabled', False)) try: BUY_GOLD_THRESHOLD = max(0, int(settings.get('buy_gold_threshold', 0))) except Exception: BUY_GOLD_THRESHOLD = 0 solo = settings['solo'] members = settings['members'] levelOffset = settings['levelOffset'] xcontrol_enabled = settings.get('xcontrol_enabled', False) xcontrol_stop_command = settings.get('xcontrol_stop_command', 'STOP') xcontrol_stop_delay_ms = clamp_delay(settings.get('xcontrol_stop_delay_ms', 500)) xcontrol_setpos_command = settings.get('xcontrol_setpos_command', 'SETPOS x% y% r% z%') xcontrol_setrange_command = settings.get('xcontrol_setrange_command', 'SETRADIUS %Range') xcontrol_start_delay_ms = clamp_delay(settings.get('xcontrol_start_delay_ms', 500)) xcontrol_start_command = settings.get('xcontrol_start_command', 'START') QtBind.setChecked(tab1, checkEnable, enabled) QtBind.setChecked(tab1, checkSolo, solo) QtBind.setText(tab1, editMembers, str(members)) QtBind.setText(tab1, editLevelOffset, str(levelOffset)) QtBind.setChecked(tab1, checkXControl, xcontrol_enabled) if 'buy_items_check' in globals() and 'tab2' in globals(): QtBind.setChecked(tab2, buy_items_check, buy_items_enabled) if 'buy_gold_threshold_edit' in globals() and 'tab2' in globals(): QtBind.setText(tab2, buy_gold_threshold_edit, str(BUY_GOLD_THRESHOLD)) if 'buy_keep_town_gold_check' in globals() and 'tab2' in globals(): QtBind.setChecked(tab2, buy_keep_town_gold_check, buy_keep_town_gold_enabled) load_xcontrol_tab1() if 'tab2' in globals(): _hometown_refresh_tab1() auto_settings_signature = None Timer(1.0, finish_loading_character_settings, []).start() if migrated_legacy_runtime: save_config() log('%s: Settings for [%s] loaded' % (Plugin, character)) def save_character_settings(): global current_character global buy_items_enabled global BUY_GOLD_THRESHOLD global buy_keep_town_gold_enabled if not current_character: current_character = get_character_key() if not current_character: return settings = character_settings.get( current_character, get_default_character_settings() ) settings['enabled'] = enabled settings['solo'] = solo settings['members'] = members settings['levelOffset'] = levelOffset settings['buy_items_enabled'] = bool(buy_items_enabled) settings['buy_keep_town_gold_enabled'] = bool(buy_keep_town_gold_enabled) try: BUY_GOLD_THRESHOLD = max(0, int(BUY_GOLD_THRESHOLD)) except Exception: BUY_GOLD_THRESHOLD = 0 settings['buy_gold_threshold'] = BUY_GOLD_THRESHOLD settings['xcontrol_enabled'] = xcontrol_enabled settings['xcontrol_stop_command'] = xcontrol_stop_command settings['xcontrol_stop_delay_ms'] = xcontrol_stop_delay_ms settings['xcontrol_setpos_command'] = xcontrol_setpos_command settings['xcontrol_setrange_command'] = xcontrol_setrange_command settings['xcontrol_start_delay_ms'] = xcontrol_start_delay_ms settings['xcontrol_start_command'] = xcontrol_start_command character_settings[current_character] = settings # ============================================================ # Action Buttons Logic # ============================================================ def btn_walk_clicked(): index = QtBind.currentIndex(tab1, spotList) if index < 0 or index >= len(spots): log('%s: Please select a spot from the list first!' % Plugin) return spot = spots[index] log('%s: Generating path to [%s]...' % (Plugin, spot['name'])) stop_script() stop_bot() stop_trace() stop_trade() try: lines = generate_script(spot['r'], spot['x'], spot['y'], spot['z']) if lines: script_str = '\n'.join([str(line).strip() for line in lines if str(line).strip()]) start_script(script_str) log('%s: Walking to spot...' % Plugin) else: log('%s: Could not generate path! Maybe already there or unreachable.' % Plugin) except Exception as ex: log('%s: Walk error: %s' % (Plugin, ex)) def btn_stop_clicked(): stop_script() stop_bot() stop_trace() stop_trade() log('%s: All actions stopped by user.' % Plugin) # ============================================================ # Tab 1 Callbacks # ============================================================ def checkEnable_clicked(checked): global enabled if loading_character_settings: return enabled = bool(checked) save_character_settings() save_config() if enabled: log('%s: Plugin enabled for [%s]' % (Plugin, current_character)) else: log('%s: Plugin disabled for [%s]' % (Plugin, current_character)) def checkSolo_clicked(checked): global solo if loading_character_settings: return solo = bool(checked) save_character_settings() save_config() if solo: log('%s: Solo mode enabled for [%s]' % (Plugin, current_character)) else: log('%s: Party mode enabled for [%s]' % (Plugin, current_character)) def saveMembers_clicked(): global members value = QtBind.text(tab1, editMembers) try: value = int(value) except: log('%s: Invalid party member value!' % Plugin) QtBind.setText(tab1, editMembers, str(members)) return value = max(1, min(8, value)) members = value QtBind.setText(tab1, editMembers, str(members)) save_character_settings() save_config() log('%s: Party members for [%s] = %d' % (Plugin, current_character, members)) def saveLevelOffset_clicked(): global levelOffset value = QtBind.text(tab1, editLevelOffset) try: value = int(value) except: log('%s: Invalid level offset!' % Plugin) QtBind.setText(tab1, editLevelOffset, str(levelOffset)) return value = max(-255, min(255, value)) levelOffset = value QtBind.setText(tab1, editLevelOffset, str(levelOffset)) save_character_settings() save_config() log('%s: Level offset for [%s] = %+d' % (Plugin, current_character, levelOffset)) # ============================================================ # Chat Control Settings # ============================================================ def clamp_delay(value): try: value = int(value) except: return 500 return max(0, min(60000, value)) def load_xcontrol_tab1(): QtBind.setText(tab1, editXControlStop, xcontrol_stop_command) QtBind.setText(tab1, editXControlDelay1, str(xcontrol_stop_delay_ms)) QtBind.setText(tab1, editXControlSetPos, xcontrol_setpos_command) QtBind.setText(tab1, editXControlSetRange, xcontrol_setrange_command) QtBind.setText(tab1, editXControlDelay2, str(xcontrol_start_delay_ms)) QtBind.setText(tab1, editXControlStart, xcontrol_start_command) def read_xcontrol_tab1(): global xcontrol_stop_command global xcontrol_stop_delay_ms global xcontrol_setpos_command global xcontrol_setrange_command global xcontrol_start_delay_ms global xcontrol_start_command xcontrol_stop_command = QtBind.text(tab1, editXControlStop).strip() xcontrol_stop_delay_ms = clamp_delay(QtBind.text(tab1, editXControlDelay1)) xcontrol_setpos_command = QtBind.text(tab1, editXControlSetPos).strip() xcontrol_setrange_command = QtBind.text(tab1, editXControlSetRange).strip() xcontrol_start_delay_ms = clamp_delay(QtBind.text(tab1, editXControlDelay2)) xcontrol_start_command = QtBind.text(tab1, editXControlStart).strip() def saveXControl_clicked(): if not current_character: log('%s: No active character. Chat settings were not saved.' % Plugin) return read_xcontrol_tab1() save_character_settings() save_config() log('%s: Chat Control settings saved for [%s].' % (Plugin, current_character)) def checkXControl_clicked(checked): global xcontrol_enabled if loading_character_settings: return xcontrol_enabled = bool(checked) read_xcontrol_tab1() save_character_settings() save_config() log('%s: Spot change %s for [%s].' % (Plugin, 'enabled' if checked else 'disabled', current_character)) def send_xcontrol_party(message): if not xcontrol_enabled: return False if not message: return True try: result = phBotChat.Party(message) if result: log('%s: Party command sent: [%s]' % (Plugin, message)) return True log('%s: Failed to send Party command: [%s]' % (Plugin, message)) except Exception as ex: log('%s: Chat error: %s' % (Plugin, ex)) return False def format_xcontrol_command(template, spot): if not spot: return template values = { 'x': str(spot.get('x', '')), 'y': str(spot.get('y', '')), 'r': str(spot.get('r', '')), 'z': str(spot.get('z', '')), 'region': str(spot.get('r', '')), 'radius': str(spot.get('radius', '')), 'range': str(spot.get('radius', '')), 'Range': str(spot.get('radius', '')), 'spot': str(spot.get('name', '')), 'level': str(spot.get('level', '')), 'race': str(spot.get('race', '')), 'char': str(get_character_name() or '') } command = template replacements = [ ('%Range%', values['Range']), ('%Range', values['Range']), ('%X%', values['x']), ('%Y%', values['y']), ('%R%', values['r']), ('%Z%', values['z']), ('x%', values['x']), ('y%', values['y']), ('r%', values['r']), ('z%', values['z']), ('{x}', values['x']), ('{y}', values['y']), ('{r}', values['r']), ('{z}', values['z']), ('{region}', values['region']), ('{radius}', values['radius']), ('{range}', values['range']), ('{Range}', values['Range']), ('{spot}', values['spot']), ('{level}', values['level']), ('{race}', values['race']), ('{char}', values['char']) ] for placeholder, value in replacements: command = command.replace(placeholder, value) return command def send_xcontrol_stop(spot=None): if xcontrol_enabled: send_xcontrol_party(format_xcontrol_command(xcontrol_stop_command, spot)) def send_xcontrol_setpos(spot): if xcontrol_enabled and spot: send_xcontrol_party(format_xcontrol_command(xcontrol_setpos_command, spot)) def send_xcontrol_setrange(spot): if xcontrol_enabled and spot: send_xcontrol_party(format_xcontrol_command(xcontrol_setrange_command, spot)) def send_xcontrol_start(spot=None): if xcontrol_enabled: send_xcontrol_party(format_xcontrol_command(xcontrol_start_command, spot)) # ============================================================ # Spot Management # ============================================================ def clamp_number(value, minimum, maximum): try: value = int(value) except: return None return max(minimum, min(maximum, value)) def useMonsterName_clicked(checked): global use_monstername_as_name use_monstername_as_name = bool(checked) save_config() def useMobLevel_clicked(checked): global use_mob_level use_mob_level = bool(checked) save_config() def _resolve_monster_info(monster, db_conn=None): if not isinstance(monster, dict): return None, None name = str(monster.get('name') or '').strip() servername = str(monster.get('servername') or '').strip() model_id = monster.get('model') level = monster.get('level') if model_id is not None: try: m_data = get_monster(int(model_id)) if isinstance(m_data, dict): if not name: name = str(m_data.get('name') or '').strip() if not servername: servername = str(m_data.get('servername') or '').strip() if level is None: level = m_data.get('level') except Exception: pass if (not name or level is None) and servername: try: m_data = get_monster_string(servername) if isinstance(m_data, dict): if not name: name = str(m_data.get('name') or '').strip() if level is None: level = m_data.get('level') except Exception: pass if (not name or level is None) and db_conn is not None: try: row = None if model_id is not None: row = db_conn.execute('SELECT name, servername, level FROM monsters WHERE id=? LIMIT 1', (int(model_id),)).fetchone() if not row and servername: row = db_conn.execute('SELECT name, servername, level FROM monsters WHERE servername=? LIMIT 1', (servername,)).fetchone() if row: if not name and row[0]: name = str(row[0]).strip() if not servername and row[1]: servername = str(row[1]).strip() if level is None and row[2] is not None: level = int(row[2]) except Exception: pass if not name and servername: name = servername try: level = int(level) if level is not None else None if level is not None and (level < 1 or level > 255): level = None except (TypeError, ValueError): level = None return (name or None), level def get_most_common_monster_name(): try: monsters = get_monsters() except Exception as ex: log('%s: get_monsters() error: %s' % (Plugin, ex)) return None if not monsters: log('%s: get_monsters() returned 0 nearby monsters (stand next to live mobs).' % Plugin) return None values = monsters.values() if isinstance(monsters, dict) else monsters db_path = _buy_items_db_path((get_client() or {}).get('path', '')) db_conn = sqlite3.connect(db_path) if db_path and os.path.exists(db_path) else None counts = {} try: for monster in values: name, _ = _resolve_monster_info(monster, db_conn) if name: counts[name] = counts.get(name, 0) + 1 finally: if db_conn: db_conn.close() if not counts: return None return max(counts, key=counts.get) def get_most_common_monster_level(): try: monsters = get_monsters() except Exception as ex: log('%s: get_monsters() error: %s' % (Plugin, ex)) return None if not monsters: log('%s: get_monsters() returned 0 nearby monsters (stand next to live mobs).' % Plugin) return None values = monsters.values() if isinstance(monsters, dict) else monsters db_path = _buy_items_db_path((get_client() or {}).get('path', '')) db_conn = sqlite3.connect(db_path) if db_path and os.path.exists(db_path) else None records = [] key_counts = {} try: for monster in values: if not isinstance(monster, dict): continue name, mob_level = _resolve_monster_info(monster, db_conn) if mob_level is None: continue group_key = name or str(monster.get('model') or monster.get('servername') or mob_level) records.append((group_key, mob_level)) key_counts[group_key] = key_counts.get(group_key, 0) + 1 finally: if db_conn: db_conn.close() if not records or not key_counts: return None most_common_key = max(key_counts, key=key_counts.get) level_counts = {} for group_key, mob_level in records: if group_key == most_common_key: level_counts[mob_level] = level_counts.get(mob_level, 0) + 1 if not level_counts: return None return max(level_counts, key=level_counts.get) def addSpot_clicked(): race = QtBind.text(tab1, comboRace) if race not in ['Europe', 'Chinese', 'All']: log('%s: Invalid race!' % Plugin) return if bool(QtBind.isChecked(tab1, checkMobLevel)): mob_level = get_most_common_monster_level() if mob_level is None: log('%s: Use Mob Lvl is enabled, but no valid nearby monster levels were found.' % Plugin) return level = mob_level QtBind.setText(tab1, editLevel, str(level)) else: level = clamp_number(QtBind.text(tab1, editLevel), 1, 255) if level is None: log('%s: Invalid level!' % Plugin) return if bool(QtBind.isChecked(tab1, checkMonsterName)): name = get_most_common_monster_name() if not name: log('%s: Use monstername as name is enabled, but no nearby monster names were found.' % Plugin) return QtBind.setText(tab1, editName, name) else: name = QtBind.text(tab1, editName).strip() if not name: log('%s: Please enter a spot name!' % Plugin) return radius = clamp_number(QtBind.text(tab1, editRange), 0, 255) if radius is None: log('%s: Invalid range!' % Plugin) return try: position = get_position() if not position: log('%s: Could not get current position!' % Plugin) return region = int(position['region']) pos_x = int(position['x']) pos_y = int(position['y']) pos_z = int(position['z']) except Exception as ex: log('%s: Error reading position: %s' % (Plugin, ex)) return spot = { 'race': race, 'level': level, 'name': name, 'r': region, 'x': pos_x, 'y': pos_y, 'z': pos_z, 'radius': radius } spots.append(spot) spots.sort(key=lambda s: (s['level'], s['race'])) save_config() refresh_spot_list() log('%s: Added spot [%s] Race=%s Level=%d Region=%d X=%d Y=%d Z=%d Range=%d' % (Plugin, name, race, level, region, pos_x, pos_y, pos_z, radius)) QtBind.setText(tab1, editName, '') QtBind.setText(tab1, editLevel, '1') QtBind.setText(tab1, editRange, '50') def deleteSpot_clicked(): index = QtBind.currentIndex(tab1, spotList) if index < 0: log('%s: Please select a spot first!' % Plugin) return if index >= len(spots): return spot = spots[index] log('%s: Deleting spot [%s]' % (Plugin, spot['name'])) del spots[index] save_config() refresh_spot_list() def refresh_spot_list(): QtBind.clear(tab1, spotList) nbsp = '\u00a0' rows = [] for index, spot in enumerate(spots): rows.append({ 'id': str(index), 'level': str(spot['level']), 'race': str(spot['race']), 'spot': 'Spot %d,%d,%d,%d' % (spot['r'], spot['x'], spot['y'], spot['z']), 'range': str(spot['radius']), 'name': str(spot['name']) }) id_width = max((len(row['id']) for row in rows), default=2) level_width = max((len(row['level']) for row in rows), default=3) race_width = max((len(row['race']) for row in rows), default=len('Chinese')) spot_width = max((len(row['spot']) for row in rows), default=len('Spot (Region,X,Y,Z)')) range_width = max((len(row['range']) for row in rows), default=3) for row in rows: text = ( row['id'] + nbsp * (id_width - len(row['id']) + 3) + 'Lvl ' + row['level'] + nbsp * (level_width - len(row['level']) + 2) + 'Race ' + row['race'] + nbsp * (race_width - len(row['race']) + 3) + row['spot'] + nbsp * (spot_width - len(row['spot']) + 2) + 'Range ' + row['range'] + nbsp * (range_width - len(row['range']) + 2) + row['name'] ) QtBind.append(tab1, spotList, text) # ============================================================ # Training Area & Party Handling # ============================================================ def get_character_race(): try: data = get_character_data() return 'Europe' if data.get('model', 0) > 10000 else 'Chinese' except Exception as ex: log('%s: Race detection error: %s' % (Plugin, ex)) return None def get_available_spots(): race = get_character_race() available = [] for spot in spots: if spot['race'] == race or spot['race'] == 'All': available.append(spot) return available def is_character_alive(): try: data = get_character_data() except: return False if not data: return False return not data.get('dead', False) def cancel_area_change_timer(): global area_change_timer if area_change_timer is not None: try: area_change_timer.cancel() except: pass area_change_timer = None def reset_area_change_death_timer(): global area_change_death_remaining area_change_death_remaining = None def schedule_area_change(delay_seconds): global area_change_timer cancel_area_change_timer() area_change_timer = Timer(max(0.05, delay_seconds), continue_area_change, []) area_change_timer.start() def return_to_town_after_death_timeout(): global area_change_pending global area_change_stage global area_change_death_remaining cancel_area_change_timer() area_change_pending = True area_change_stage = 4 area_change_death_remaining = None log( '%s: Character stayed dead for %d seconds. Returning to town. ' 'Local bot will start again after %d ms.' % (Plugin, DEATH_WAIT_TIMEOUT, int(RETURN_TO_TOWN_START_DELAY * 1000)) ) try: stop_bot() except: pass try: inject_joymax(0x3053, b'\x01', False) except Exception as ex: log('%s: Error returning character to town: %s' % (Plugin, ex)) Timer(RETURN_TO_TOWN_START_DELAY, finish_forced_return, []).start() def start_chat_area_change(area): global area_change_stage global area_change_pending if not xcontrol_enabled: area_change_pending = False return area_change_stage = 3 send_xcontrol_stop(area) schedule_area_change(xcontrol_stop_delay_ms / 1000.0) def continue_chat_area_change(): global area_change_pending global pending_area global area_change_stage global area_change_timer area_change_timer = None if not area_change_pending or not pending_area: return area = pending_area if area_change_stage == 3: send_xcontrol_setpos(area) send_xcontrol_setrange(area) area_change_stage = 5 schedule_area_change(xcontrol_start_delay_ms / 1000.0) return if area_change_stage == 5: send_xcontrol_start(area) log('%s: Local bot + Chat area change sequence completed for [%s].' % (Plugin, area['name'])) area_change_pending = False pending_area = None area_change_stage = 0 reset_area_change_death_timer() return def finish_forced_return(): global area_change_stage global area_change_pending global pending_area try: if _bot_is_running(): started = True else: started = start_bot() if not started and not _bot_is_running(): log('%s: Local bot did not start after forced return; retrying in 3 seconds.' % Plugin) Timer(3.0, finish_forced_return, []).start() return log('%s: Local bot started after forced return to town.' % Plugin) except Exception as ex: log('%s: Error starting local bot after forced return: %s. Retrying in 3 seconds.' % (Plugin, ex)) Timer(3.0, finish_forced_return, []).start() return if enabled and xcontrol_enabled and area_change_pending and pending_area: start_chat_area_change(pending_area) else: area_change_pending = False pending_area = None area_change_stage = 0 reset_area_change_death_timer() def check_level(level): global area_change_pending if globals().get('buy_items_state') is not None or globals().get('hometown_state') is not None: return if area_change_pending: return available_spots = get_available_spots() if not available_spots: log('%s: No valid training spots for this character!' % Plugin) return effective_level = max(1, min(255, level + levelOffset)) eligible_spots = [spot for spot in available_spots if spot['level'] <= effective_level] if eligible_spots: newArea = max(eligible_spots, key=lambda spot: spot['level']) else: newArea = min(available_spots, key=lambda spot: spot['level']) currentArea = get_training_area() or {} if ( currentArea.get('x') == newArea['x'] and currentArea.get('y') == newArea['y'] and currentArea.get('z') == newArea['z'] and currentArea.get('region') == newArea['r'] ): return log('%s: Changing training area to [%s] Spot Level %d' % (Plugin, newArea['name'], newArea['level'])) start_area_change(newArea) def start_area_change(newArea): global area_change_pending global pending_area global area_change_stage global area_change_death_remaining area_change_pending = True pending_area = newArea area_change_stage = 1 area_change_death_remaining = None stop_bot() set_training_script('') schedule_area_change(xcontrol_stop_delay_ms / 1000.0) def continue_area_change(): global area_change_pending global pending_area global area_change_timer global area_change_stage global area_change_death_remaining area_change_timer = None if not area_change_pending or not pending_area: return if not enabled: area_change_pending = False pending_area = None area_change_stage = 0 reset_area_change_death_timer() return if area_change_stage == 4: finish_forced_return() return if area_change_stage in (1, 2) and not is_character_alive(): if area_change_death_remaining is None: area_change_death_remaining = DEATH_WAIT_TIMEOUT log('%s: Character died during area change. Waiting up to %d seconds for resurrection.' % (Plugin, DEATH_WAIT_TIMEOUT)) area_change_death_remaining -= 1 if area_change_death_remaining <= 0: return_to_town_after_death_timeout() return schedule_area_change(1.0) return area_change_death_remaining = None area = pending_area if area_change_stage == 1: try: set_training_position(area['r'], area['x'], area['y'], area['z']) set_training_radius(area['radius']) except Exception as ex: log('%s: Error applying training area: %s' % (Plugin, ex)) schedule_area_change(1.0) return area_change_stage = 2 schedule_area_change(xcontrol_start_delay_ms / 1000.0) return if area_change_stage == 2: try: start_bot() except Exception as ex: log('%s: Error starting local bot after area change: %s' % (Plugin, ex)) schedule_area_change(1.0) return log('%s: Local bot started at training area [%s].' % (Plugin, str(area.get('name', 'Unknown')))) if xcontrol_enabled: start_chat_area_change(area) else: area_change_pending = False pending_area = None area_change_stage = 0 reset_area_change_death_timer() return if area_change_stage in (3, 5): continue_chat_area_change() return area_change_pending = False pending_area = None area_change_stage = 0 reset_area_change_death_timer() def check_party(): character = get_character_data() if not character or 'level' not in character: return level = character['level'] if solo: check_level(level) return party = get_party() if not party or len(party) != members: return lowestLvl = level for key in party: try: member = party[key] if member['name'] == character['name']: continue memberLevel = member.get('level', 0) if 0 < memberLevel < lowestLvl: lowestLvl = memberLevel except: continue check_level(lowestLvl) # ============================================================ # Save & Load Configuration File # ============================================================ def save_config(): try: if current_character: read_xcontrol_tab1() save_character_settings() directory = os.path.dirname(CONFIG_FILE) if not os.path.exists(directory): os.makedirs(directory) with open(CONFIG_FILE, 'w', encoding='utf-8') as f: f.write('[Settings]\n') f.write('use_monstername_as_name=%d\n' % (1 if use_monstername_as_name else 0)) f.write('use_mob_level=%d\n' % (1 if use_mob_level else 0)) f.write('\n') for character in list(character_settings): settings = character_settings[character] if not isinstance(settings, dict): log('%s: Repairing invalid settings for [%s].' % (Plugin, character)) settings = get_default_character_settings() character_settings[character] = settings f.write('[%s]\n' % character) f.write('enabled=%d\n' % (1 if settings['enabled'] else 0)) f.write('hometown_enabled=%d\n' % (1 if settings.get('hometown_enabled', True) else 0)) f.write('buy_items_enabled=%d\n' % (1 if settings.get('buy_items_enabled', False) else 0)) f.write('buy_keep_town_gold_enabled=%d\n' % (1 if settings.get('buy_keep_town_gold_enabled', False) else 0)) try: threshold = max(0, int(settings.get('buy_gold_threshold', 0))) except Exception: threshold = 0 f.write('buy_gold_threshold=%d\n' % threshold) bought_names = [] for item_name in settings.get('bought_items', []): item_name = str(item_name or '').strip() if item_name and not item_name.isdigit() and item_name not in bought_names: bought_names.append(item_name) f.write('bought_items=%s\n' % ','.join(bought_names)) f.write('solo=%d\n' % (1 if settings['solo'] else 0)) f.write('members=%d\n' % settings['members']) f.write('levelOffset=%d\n' % settings['levelOffset']) f.write('xcontrol_enabled=%d\n' % (1 if settings.get('xcontrol_enabled', False) else 0)) f.write('xcontrol_stop=%s\n' % str(settings.get('xcontrol_stop_command', 'STOP')).replace('\\n', ' ')) f.write('xcontrol_stopDelayMs=%d\n' % clamp_delay(settings.get('xcontrol_stop_delay_ms', 500))) f.write('xcontrol_setpos=%s\n' % str(settings.get('xcontrol_setpos_command', 'SETPOS x% y% r% z%')).replace('\\n', ' ')) f.write('xcontrol_setrange=%s\n' % str(settings.get('xcontrol_setrange_command', 'SETRADIUS %Range')).replace('\\n', ' ')) f.write('xcontrol_startDelayMs=%d\n' % clamp_delay(settings.get('xcontrol_start_delay_ms', 500))) f.write('xcontrol_start=%s\n' % str(settings.get('xcontrol_start_command', 'START')).replace('\\n', ' ')) rules = settings.get('hometown_rules') if not isinstance(rules, list) or len(rules) != len(HOMETOWN_DEFAULTS): rules = _hometown_character_defaults()['hometown_rules'] settings['hometown_rules'] = rules for idx, rule in enumerate(rules): if not isinstance(rule, dict): rule = _hometown_character_defaults()['hometown_rules'][idx] rules[idx] = rule try: enabled_value = 1 if bool(rule.get('enabled', False)) else 0 race_value = str(rule.get('race', HOMETOWN_DEFAULTS[idx]['race'])) if race_value not in ('Chinese', 'Europe', 'All'): race_value = HOMETOWN_DEFAULTS[idx]['race'] level_value = max(0, min(255, int(rule.get('level', HOMETOWN_DEFAULTS[idx]['level'])))) except Exception: base = HOMETOWN_DEFAULTS[idx] enabled_value = 1 if bool(base.get('enabled', False)) else 0 race_value = base.get('race', 'All') level_value = int(base.get('level', 0)) town_value = str(rule.get('town', HOMETOWN_DEFAULTS[idx]['town'])).replace('|', ' ') f.write('%d|%d|%s|%d|%s\n' % (idx, enabled_value, race_value, level_value, town_value)) f.write('\n') f.write('[Spots]\n') spots.sort(key=lambda s: (s['level'], s['race'])) for spot in spots: safe_name = spot['name'].replace('|', '/') f.write('%s|%d|%s|%d|%d|%d|%d|%d\n' % (spot['race'], spot['level'], safe_name, spot['r'], spot['x'], spot['y'], spot['z'], spot['radius'])) except Exception as ex: log('%s: Error saving configuration: %s' % (Plugin, ex)) def load_config(): global spots global character_settings global use_monstername_as_name global use_mob_level spots = [] character_settings = {} use_monstername_as_name = False use_mob_level = False QtBind.setChecked(tab1, checkMonsterName, False) QtBind.setChecked(tab1, checkMobLevel, False) legacy_xcontrol = { 'xcontrol_enabled': False, 'xcontrol_stop_command': 'STOP', 'xcontrol_stop_delay_ms': 500, 'xcontrol_setpos_command': 'SETPOS x% y% r% z%', 'xcontrol_setrange_command': 'SETRADIUS %Range', 'xcontrol_start_delay_ms': 500, 'xcontrol_start_command': 'START' } legacy_xcontrol_found = False character_xcontrol_explicit = set() loaded_hometown = {} legacy_character_sections = set() legacy_hometown_sections = set() separate_hometown_sections = set() if not os.path.exists(CONFIG_FILE): save_config() refresh_spot_list() return try: with open(CONFIG_FILE, 'r', encoding='utf-8') as f: lines = f.readlines() section = '' character = None for raw_line in lines: line = raw_line.strip() if not line or line.startswith('#'): continue if line.startswith('[') and line.endswith(']'): section = line[1:-1] if section.startswith('Character:'): legacy_name = section[len('Character:'):].strip() legacy_character_sections.add(legacy_name) character = get_character_key(legacy_name) if legacy_name == get_character_name() else legacy_name if character not in character_settings: character_settings[character] = get_default_character_settings() elif section not in ('Settings', 'Spots', 'XControl') and not section.startswith('Hometown:'): if '_' not in section: legacy_character_sections.add(section) character = section if character not in character_settings: character_settings[character] = get_default_character_settings() else: character = None continue if section == 'Settings': if '=' in line: key, value = line.split('=', 1) if key.strip() == 'use_monstername_as_name': use_monstername_as_name = value.strip() == '1' QtBind.setChecked(tab1, checkMonsterName, use_monstername_as_name) elif key.strip() == 'use_mob_level': use_mob_level = value.strip() == '1' QtBind.setChecked(tab1, checkMobLevel, use_mob_level) continue if character is not None and (section.startswith('Character:') or (section not in ('Settings', 'Spots', 'XControl') and not section.startswith('Hometown:'))): if '=' not in line: if '|' in line: rules = loaded_hometown.setdefault(character, _hometown_copy_defaults()) try: parts = line.split('|') if len(parts) >= 4: idx = int(parts[0]) if 0 <= idx < len(HOMETOWN_DEFAULTS): rules[idx]['enabled'] = parts[1] == '1' rules[idx]['race'] = parts[2] if parts[2] in _hometown_race_options() else HOMETOWN_DEFAULTS[idx]['race'] rules[idx]['level'] = max(0, min(255, int(parts[3]))) if len(parts) >= 5 and rules[idx].get('custom_town') and parts[4] in _hometown_town_options(): rules[idx]['town'] = parts[4] town_base = next((r for r in HOMETOWN_DEFAULTS if not r.get('custom_town') and r['town'] == parts[4]), HOMETOWN_DEFAULTS[0]) rules[idx]['teleport'] = tuple(town_base['teleport']) rules[idx]['spawn'] = tuple(town_base['spawn']) except Exception: pass continue key, value = line.split('=', 1) value = value.strip() if key == 'enabled': character_settings[character]['enabled'] = (value == '1') elif key == 'hometown_enabled': character_settings[character]['hometown_enabled'] = (value == '1') elif key == 'buy_items_enabled': character_settings[character]['buy_items_enabled'] = (value == '1') elif key == 'buy_keep_town_gold_enabled': character_settings[character]['buy_keep_town_gold_enabled'] = (value == '1') elif key == 'buy_gold_threshold': try: character_settings[character]['buy_gold_threshold'] = max(0, int(value)) except Exception: character_settings[character]['buy_gold_threshold'] = 0 elif key == 'bought_items': names = [] for raw_name in value.split(','): item_name = raw_name.strip() if item_name and item_name not in names: names.append(item_name) character_settings[character]['bought_items'] = names elif key == 'solo': character_settings[character]['solo'] = (value == '1') elif key == 'members': try: character_settings[character]['members'] = max(1, min(8, int(value))) except: character_settings[character]['members'] = 8 elif key == 'levelOffset': try: character_settings[character]['levelOffset'] = max(-255, min(255, int(value))) except: character_settings[character]['levelOffset'] = 0 elif key == 'xcontrol_enabled': character_settings[character]['xcontrol_enabled'] = (value == '1') character_xcontrol_explicit.add(character) elif key == 'xcontrol_stop': character_settings[character]['xcontrol_stop_command'] = value character_xcontrol_explicit.add(character) elif key == 'xcontrol_stopDelayMs': character_settings[character]['xcontrol_stop_delay_ms'] = clamp_delay(value) character_xcontrol_explicit.add(character) elif key == 'xcontrol_setpos': character_settings[character]['xcontrol_setpos_command'] = value character_xcontrol_explicit.add(character) elif key == 'xcontrol_setrange': character_settings[character]['xcontrol_setrange_command'] = value character_xcontrol_explicit.add(character) elif key == 'xcontrol_startDelayMs': character_settings[character]['xcontrol_start_delay_ms'] = clamp_delay(value) character_xcontrol_explicit.add(character) elif key == 'xcontrol_start': character_settings[character]['xcontrol_start_command'] = value character_xcontrol_explicit.add(character) continue if section == 'XControl': if '=' not in line: continue key, value = line.split('=', 1) value = value.strip() legacy_xcontrol_found = True if key == 'enabled': legacy_xcontrol['xcontrol_enabled'] = (value == '1') elif key == 'stop': legacy_xcontrol['xcontrol_stop_command'] = value elif key == 'stopDelayMs': legacy_xcontrol['xcontrol_stop_delay_ms'] = clamp_delay(value) elif key == 'setpos': legacy_xcontrol['xcontrol_setpos_command'] = value elif key == 'setrange': legacy_xcontrol['xcontrol_setrange_command'] = value elif key == 'startDelayMs': legacy_xcontrol['xcontrol_start_delay_ms'] = clamp_delay(value) elif key == 'start': legacy_xcontrol['xcontrol_start_command'] = value continue if section.startswith('Hometown:'): hometown_character = section.split(':', 1)[1] separate_hometown_sections.add(hometown_character) if '_' not in hometown_character: legacy_hometown_sections.add(hometown_character) rules = loaded_hometown.setdefault(hometown_character, _hometown_copy_defaults()) try: parts = line.split('|') if len(parts) >= 4: idx = int(parts[0]) if 0 <= idx < len(HOMETOWN_DEFAULTS): rules[idx]['enabled'] = parts[1] == '1' rules[idx]['race'] = parts[2] if parts[2] in _hometown_race_options() else HOMETOWN_DEFAULTS[idx]['race'] rules[idx]['level'] = max(0, min(255, int(parts[3]))) if len(parts) >= 5 and rules[idx].get('custom_town') and parts[4] in _hometown_town_options(): rules[idx]['town'] = parts[4] town_base = next((r for r in HOMETOWN_DEFAULTS if not r.get('custom_town') and r['town'] == parts[4]), HOMETOWN_DEFAULTS[0]) rules[idx]['teleport'] = tuple(town_base['teleport']) rules[idx]['spawn'] = tuple(town_base['spawn']) except Exception: pass continue if section == 'Spots': parts = line.split('|') if len(parts) != 8: continue try: race = parts[0] if race not in ['Europe', 'Chinese', 'All']: continue level = max(1, min(255, int(parts[1]))) name = parts[2] if parts[2] else 'Unnamed' spots.append({ 'race': race, 'level': level, 'name': name, 'r': int(parts[3]), 'x': int(parts[4]), 'y': int(parts[5]), 'z': int(parts[6]), 'radius': max(0, min(255, int(parts[7]))) }) except: log('%s: Invalid spot line: %s' % (Plugin, line)) migrated_character_sections = False def _resolve_legacy_character_key(name): name = str(name or '').strip() if not name: return name live_name = get_character_name() if live_name and name == live_name: return get_character_key(live_name) or name suffix = '_' + name matches = [key for key in character_settings if key.endswith(suffix) and key != name] if len(matches) == 1: return matches[0] return name for legacy_name in list(legacy_character_sections): target_key = _resolve_legacy_character_key(legacy_name) if target_key == legacy_name: continue legacy_settings = character_settings.pop(legacy_name, {}) target_existed = target_key in character_settings if not target_existed and isinstance(legacy_settings, dict): character_settings[target_key] = legacy_settings else: target_settings = character_settings.setdefault(target_key, get_default_character_settings()) if isinstance(legacy_settings, dict): for key, value in legacy_settings.items(): if key not in target_settings: target_settings[key] = value if target_key != legacy_name: migrated_character_sections = True normalized_hometown = {} for hometown_name, rules in loaded_hometown.items(): target_key = _resolve_legacy_character_key(hometown_name) if target_key not in normalized_hometown or '_' in hometown_name: normalized_hometown[target_key] = rules loaded_hometown = normalized_hometown if any(_resolve_legacy_character_key(name) != name for name in legacy_hometown_sections): migrated_character_sections = True if any('_' in name or _resolve_legacy_character_key(name) != name for name in separate_hometown_sections): migrated_character_sections = True for hometown_character, rules in loaded_hometown.items(): if hometown_character not in character_settings: character_settings[hometown_character] = get_default_character_settings() character_settings[hometown_character]['hometown_rules'] = rules migrated_legacy_xcontrol = False if legacy_xcontrol_found: for character in character_settings: settings = character_settings[character] if character not in character_xcontrol_explicit: settings.update(legacy_xcontrol) migrated_legacy_xcontrol = True spots.sort(key=lambda s: (s['level'], s['race'])) if migrated_legacy_xcontrol or migrated_character_sections: save_config() refresh_spot_list() log('%s: Loaded %d spots and %d character settings.' % (Plugin, len(spots), len(character_settings))) except Exception as ex: log('%s: Error loading configuration: %s' % (Plugin, ex)) # ============================================================ # Auto Hometown & Auto Buy GUI (Tab 2) # ============================================================ tab2 = QtBind.init(__name__, Plugin + '(+)') buy_items_pending = False buy_items_check = QtBind.createCheckBox(tab2, 'buy_items_clicked', '', 0, 0) QtBind.createLabel(tab2, 'Enable Buy Newly Available NPC Items', 25, 5) buy_keep_town_gold_check = QtBind.createCheckBox(tab2, 'buy_keep_town_gold_clicked', '', 0, 20) QtBind.createLabel(tab2, 'Keep enough Gold for Town Loop Items', 25, 25) QtBind.createLabel(tab2, '*Must have at least 2 "Return Scrolls" set in (Town > Buy)', 5, 73) QtBind.createLabel(tab2, '*Must enable "Equip better items" in (Training Area > Options)', 5, 90) QtBind.createLabel(tab2, 'Dont buy items if that will result in Gold beeing lower then:', 5, 45) buy_gold_threshold_edit = QtBind.createLineEdit(tab2, '0', 290, 43, 90, 20) # ============================================================ # SP Gap Calculator GUI # ============================================================ calc_y = 145 QtBind.createLabel(tab2, 'SP Gap Calculator', 5, calc_y) calc_live_btn = QtBind.createButton(tab2, 'calc_get_live_stats_clicked', 'Get Live Stats', 208, calc_y + 80) QtBind.createLabel(tab2, 'Current Skill Points:', 5, calc_y + 25) calc_cur_sp_edit = QtBind.createLineEdit(tab2, '0', 134, calc_y + 22, 70, 20) QtBind.createLabel(tab2, 'Current Percent:', 5, calc_y + 45) calc_cur_perc_edit = QtBind.createLineEdit(tab2, '0.00', 134, calc_y + 42, 70, 20) QtBind.createLabel(tab2, 'Current Level:', 5, calc_y + 65) calc_start_lvl_edit = QtBind.createLineEdit(tab2, '1', 134, calc_y + 62, 70, 20) QtBind.createLabel(tab2, 'Current Mastery Level:', 5, calc_y + 85) calc_mastery_lvl_edit = QtBind.createLineEdit(tab2, '1', 134, calc_y + 82, 70, 20) QtBind.createLabel(tab2, 'Wanted Full Farmed Level:', 5, calc_y + 105) calc_max_lvl_edit = QtBind.createLineEdit(tab2, '110', 134, calc_y + 102, 70, 20) calc_btn = QtBind.createButton(tab2, 'calc_sp_clicked', 'Calculate SP and Gap', 208, calc_y + 102) calc_result_label1 = QtBind.createLabel(tab2, 'Skills and Mastery will be taken from the Mastery Tab ', 5, calc_y + 135) calc_result_label2 = QtBind.createLabel(tab2, 'Results will be displayed here. ', 5, calc_y + 150) buy_items_state = None BUY_REGION_TOWN = { 25000: 'Jangan', 26265: 'Donwhang', 23687: 'Hotan', 27244: 'Samarkand', 26959: 'Constantinople', 23088: 'Alexandria (South)', 23089: 'Alexandria (South)', 23602: 'Alexandria (North)', 23603: 'Alexandria (North)' } def buy_keep_town_gold_clicked(checked): global buy_keep_town_gold_enabled if globals().get('loading_character_settings', False): return buy_keep_town_gold_enabled = bool(checked) if current_character: save_config() log('%s: Keep Gold for Town Loop Items %s.' % (Plugin, 'enabled' if buy_keep_town_gold_enabled else 'disabled')) def buy_items_clicked(checked): global buy_items_enabled if globals().get('loading_character_settings', False): return buy_items_enabled = bool(checked) if current_character: save_config() log('%s: Buy Newly Available NPC Items %s.' % (Plugin, 'enabled' if buy_items_enabled else 'disabled')) # ============================================================ # SP Gap Calculator Logic & Live Stats # ============================================================ def calc_get_live_stats_clicked(): try: char_data = get_character_data() if not char_data: QtBind.setText(tab2, calc_result_label1, 'Error: Character not logged in or data unavailable. ') QtBind.setText(tab2, calc_result_label2, '') return lvl = int(char_data.get('level', 1) or 1) QtBind.setText(tab2, calc_start_lvl_edit, str(lvl)) sp = int(char_data.get('sp', 0) or 0) QtBind.setText(tab2, calc_cur_sp_edit, str(sp)) # Berechne die exakten Prozente direkt aus den phBot-Daten (ohne DB-Abfrage!) perc = 0.0 cur_exp = float(char_data.get('current_exp', char_data.get('exp', 0)) or 0) max_exp = float(char_data.get('max_exp', 0) or 0) if max_exp > 0: perc = (100.0 / max_exp) * cur_exp perc = max(0.0, min(perc, 99.99)) else: # Fallback perc = float(char_data.get('percent', char_data.get('exp_percent', 0.0)) or 0.0) QtBind.setText(tab2, calc_cur_perc_edit, f"{perc:.2f}") masteries = get_mastery() or {} max_mastery = 1 if masteries: levels = [int(m.get('level', 0)) for m in masteries.values() if m] if levels: max_mastery = max(levels) QtBind.setText(tab2, calc_mastery_lvl_edit, str(max_mastery)) QtBind.setText(tab2, calc_result_label1, 'Live stats successfully loaded from client! ') QtBind.setText(tab2, calc_result_label2, f'Loaded: Lvl {lvl}, SP {sp}, Perc {perc:.2f}%, Mastery {max_mastery}') log('%s: SP Calculator loaded live stats for [%s].' % (Plugin, get_character_name() or 'Unknown')) except Exception as ex: QtBind.setText(tab2, calc_result_label1, 'Error reading live stats. Check logs. ') QtBind.setText(tab2, calc_result_label2, '') log('%s: Error in calc_get_live_stats_clicked: %s' % (Plugin, ex)) def calc_sp_clicked(): import json try: cur_sp = int(QtBind.text(tab2, calc_cur_sp_edit)) cur_perc_str = QtBind.text(tab2, calc_cur_perc_edit).replace(',', '.') cur_perc = float(cur_perc_str) cur_perc = max(0.0, min(cur_perc, 99.99)) start_lvl = int(QtBind.text(tab2, calc_start_lvl_edit)) start_mastery_lvl = int(QtBind.text(tab2, calc_mastery_lvl_edit)) max_lvl = int(QtBind.text(tab2, calc_max_lvl_edit)) except: QtBind.setText(tab2, calc_result_label1, 'Error: Please enter valid numbers. ') QtBind.setText(tab2, calc_result_label2, '') return char_name = get_character_name() server_name = get_server_name() if not char_name: QtBind.setText(tab2, calc_result_label1, 'Error: Character is not logged in. ') QtBind.setText(tab2, calc_result_label2, '') return profile_path = os.path.join(get_config_dir(), f"{server_name}_{char_name}.json") if not os.path.exists(profile_path): QtBind.setText(tab2, calc_result_label1, f'Error: Profile {server_name}_{char_name}.json missing. ') QtBind.setText(tab2, calc_result_label2, '') return try: with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) except: QtBind.setText(tab2, calc_result_label1, 'Error: Failed to read JSON profile. ') QtBind.setText(tab2, calc_result_label2, '') return masteries = profile_data.get('Auto Mastery', {}).get('Mastery', []) skills = [s.strip() for s in profile_data.get('Auto Mastery', {}).get('Skill', [])] mastery_count = len(masteries) client = get_client() db_path = _buy_items_db_path(client.get('path', '')) if not db_path: QtBind.setText(tab2, calc_result_label1, 'Error: data.db3 not found. ') QtBind.setText(tab2, calc_result_label2, '') return sp_needed = 0 conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row try: if skills: placeholders = ','.join('?' for _ in skills) query = f"SELECT SUM(sp) as total_sp FROM skills WHERE sp BETWEEN 0 AND 999999 AND mastery > 0 AND level BETWEEN ? AND ? AND name IN ({placeholders})" params = [start_mastery_lvl, max_lvl] + skills row = conn.execute(query, params).fetchone() if row and row['total_sp']: sp_needed += row['total_sp'] query = "SELECT SUM(sp) as total_sp FROM leveldata WHERE level BETWEEN ? AND ?" row = conn.execute(query, (start_mastery_lvl, max_lvl)).fetchone() if row and row['total_sp']: sp_needed += (row['total_sp'] * mastery_count) finally: conn.close() sp_needed -= cur_sp if sp_needed < 0: sp_needed = 0 SP_GAP_TABLE = { 1: [1,2,0,0,0,0,0,0,0,0], 2: [3,3,4,0,0,0,0,0,0,0], 3: [4,5,6,7,0,0,0,0,0,0], 4: [5,6,8,9,12,0,0,0,0,0], 5: [6,8,9,12,15,19,0,0,0,0], 6: [10,12,15,19,24,30,40,0,0,0], 7: [14,17,21,26,32,41,55,78,0,0], 8: [17,21,26,32,41,52,70,99,157,0], 9: [21,26,32,39,49,63,84,119,190,400], 10: [25,30,37,46,58,75,99,141,224,472], 11: [34,41,51,63,79,101,135,191,304,642], 12: [43,52,64,80,100,128,171,243,385,813], 13: [52,63,78,96,121,156,208,294,467,986], 14: [61,74,91,113,142,183,244,345,549,1158], 15: [70,85,105,130,163,210,279,396,629,1328], 16: [86,105,129,160,201,259,345,489,776,1638], 17: [102,125,153,190,239,307,409,579,920,1943], 18: [118,144,177,219,275,354,472,668,1061,2240], 19: [133,163,200,247,310,399,532,754,1198,2528], 20: [148,181,222,274,345,443,591,837,1330,2807], 21: [173,212,260,322,404,520,693,982,1559,3292], 22: [199,243,298,369,464,597,796,1127,1790,3779], 23: [225,275,337,417,524,674,899,1273,2022,4269], 24: [251,306,376,466,585,752,1003,1421,2256,4763], 25: [277,339,415,514,646,831,1108,1570,2493,5263], 26: [304,371,455,564,708,911,1215,1721,2733,5769], 27: [331,404,496,614,772,992,1323,1874,2976,6283], 28: [358,438,537,665,836,1075,1433,2030,3224,6807], 29: [386,472,579,717,901,1159,1545,2189,3477,7340], 30: [415,507,622,771,968,1245,1660,2351,3735,7884], 31: [453,553,679,841,1056,1358,1810,2565,4073,8599], 32: [490,599,736,911,1144,1471,1961,2779,4413,9317], 33: [528,646,793,981,1233,1585,2113,2994,4755,10039], 34: [567,693,850,1052,1322,1700,2266,3211,5099,10766], 35: [605,740,908,1124,1412,1816,2421,3430,5447,11499], 36: [644,787,966,1197,1503,1933,2577,3651,5799,12242], 37: [684,836,1026,1270,1596,2052,2736,3875,6155,12994], 38: [724,885,1086,1345,1690,2172,2896,4103,6517,13757], 39: [765,935,1147,1421,1785,2295,3060,4335,6884,14534], 40: [807,986,1210,1498,1882,2420,3226,4571,7259,15325], 41: [856,1046,1284,1590,1997,2568,3424,4851,7704,16264], 42: [905,1107,1358,1682,2113,2716,3622,5131,8149,17204], 43: [955,1167,1433,1774,2229,2865,3821,5412,8596,18148], 44: [1005,1228,1508,1867,2345,3015,4020,5695,9045,19096], 45: [1055,1290,1583,1960,2462,3166,4221,5980,9498,20052], 46: [1106,1352,1659,2054,2581,3318,4424,6268,9955,21016], 47: [1158,1415,1736,2150,2701,3473,4630,6559,10418,21993], 48: [1210,1478,1814,2246,2822,3629,4838,6854,10886,22982], 49: [1263,1543,1894,2345,2946,3788,5050,7154,11363,23988], 50: [1316,1609,1975,2445,3072,3949,5266,7460,11848,25012], 51: [1384,1692,2077,2571,3230,4153,5538,7845,12459,26303], 52: [1453,1776,2179,2698,3390,4359,5812,8233,13077,27606], 53: [1522,1861,2283,2827,3552,4567,6089,8626,13700,28923], 54: [1593,1946,2389,2958,3716,4778,6370,9024,14333,30258], 55: [1664,2034,2496,3090,3882,4991,6655,9428,14974,31612], 56: [1736,2122,2605,3225,4051,5209,6945,9839,15627,32990], 57: [1810,2213,2715,3362,4224,5431,7241,10258,16292,34395], 58: [1886,2305,2829,3502,4400,5657,7543,10686,16972,35830], 59: [1963,2399,2945,3646,4580,5889,7852,11124,17667,37298], 60: [2042,2496,3063,3793,4765,6127,8169,11573,18380,38802], 61: [2162,2643,3244,4016,5046,6487,8650,12254,19462,41086], 62: [2284,2792,3426,4242,5330,6853,9137,12944,20558,43399], 63: [2408,2943,3612,4472,5618,7223,9631,13644,21670,45747], 64: [3040,3716,4560,5646,7094,9120,12160,17227,27361,57762], 65: [3832,4684,5749,7117,8942,11497,15330,21717,34492,72817], 66: [4021,4914,6031,7467,9381,12062,16083,22784,36186,76392], 67: [4213,5149,6320,7824,9830,12639,16852,23874,37917,80048], 68: [4410,5390,6615,8190,10290,13230,17640,24991,39691,83792], 69: [4612,5637,6918,8566,10762,13837,18449,26136,41510,87632], 70: [4820,5891,7230,8951,11246,14459,19279,27312,43378,91577], 71: [5037,6156,7555,9354,11752,15110,20147,28541,45330,95698], 72: [5264,6433,7895,9775,12281,15790,21053,29825,47370,100004], 73: [5500,6723,8251,10215,12834,16500,22001,31168,49502,104505], 74: [5748,7025,8622,10674,13411,17243,22991,32570,51729,109207], 75: [6007,7341,9010,11155,14015,18019,24025,34036,54057,114122], 76: [6277,7672,9415,11657,14645,18829,25106,35567,56489,119257], 77: [6559,8017,9839,12181,15304,19677,26236,37168,59031,124624], 78: [6855,8378,10282,12729,15993,20562,27417,38840,61688,130232], 79: [7163,8755,10744,13302,16713,21487,28650,40588,64464,136092], 80: [7485,9149,11228,13901,17465,22454,29940,42415,67365,142216], 81: [7880,9631,11820,14634,18386,23638,31518,44651,70917,149715], 82: [8233,10062,12350,15289,19209,24697,32930,46651,74094,156422], 83: [8590,10499,12885,15952,20042,25768,34358,48674,77306,163205], 84: [8950,10939,13425,16621,20882,26848,35798,50714,80546,170044], 85: [9320,11391,13980,17308,21745,27958,37278,52811,83876,177074], 86: [9700,11855,14550,18013,22632,29098,38798,54964,87296,184294], 87: [10090,12332,15135,18738,23542,30268,40358,57174,90806,191704], 88: [10490,10490,15735,19480,24475,31468,41958,59440,94406,199303], 89: [10890,13310,16335,20223,25408,32668,43558,61707,98005,206903], 90: [11290,13799,16935,20966,26342,33868,45158,63974,101605,214503], 91: [12901,15768,19352,23960,30103,38704,51605,73107,116111,245124], 92: [16059,19627,24008,29823,37470,48176,64235,90999,144528,305115], 93: [14179,17330,21268,26332,33084,42536,56715,80346,127609,269396], 94: [22011,26902,33016,40877,51359,66032,88043,124728,198097,418205], 95: [21294,26025,31940,39545,49685,63881,85174,120663,191642,404577], 96: [18991,23212,28487,35269,44313,56974,75965,107617,170921,360834], 97: [24668,30150,37002,45812,57559,74004,98672,139785,222012,468691], 98: [27379,33463,41068,50846,63884,82137,109515,155147,246410,520198], 99: [36473,44578,54710,67736,85104,109419,145892,206681,328258,692988], 100: [37643,46008,56464,69908,87833,112929,150571,213309,338786,715214], 101: [43886,53639,65829,81503,102401,131659,178545,248689,394977,833840], 102: [50455,61667,75682,93702,117728,151364,201819,285910,454092,958639], 103: [57456,90224,86184,106703,134063,172367,229823,325582,517101,1091658], 104: [64884,79303,97326,120499,151396,194653,259537,367677,583958,1232799], 105: [67967,83071,101951,126225,158590,203902,271869,385147,611705,1291377], 106: [80468,98350,120703,149441,187760,241405,321874,455988,724216,1528901], 107: [92392,112924,138588,171585,215582,277177,369569,523566,831530,1755452], 108: [105084,128437,157627,195157,245197,315253,420338,595478,945760,1996604], 109: [118555,144901,177833,220174,276629,355666,474222,671814,1066999,2252553], 110: [130275,159226,195413,241940,303976,390826,521102,738227,1172479,2475233], 111: [137238,167736,205857,254871,320222,411715,548953,777683,1235144,2607525], 112: [144215,176262,216322,267827,336501,432644,576859,817217,1297933,2740080], 113: [151177,184771,226765,280756,352745,453530,604706,856667,1360589,2872354], 114: [158149,193293,237224,293706,369015,474447,632597,896178,1423342,3004834], 115: [165114,201806,247672,306641,385267,495343,660458,935648,1486030,3137174], 116: [173588,212163,260382,322377,405038,520764,694351,983664,1562291,3298169], 117: [182051,222507,273076,338094,424785,546152,728203,1031621,1638457,3458965], 118: [190519,232857,285779,353822,444545,571558,762078,1079610,1714675,3619869], 119: [198987,243206,298480,369547,464302,596960,795947,1127591,1790880,3780748], 120: [207458,253560,311187,385279,484068,622373,829831,1175594,1867120,3941698], 121: [218426,266965,327639,405648,509660,655277,873703,1237746,1965832,4150089], 122: [229396,280373,344094,426021,535257,688188,917584,1299910,2064564,4358523], 123: [240361,293775,360542,446386,560843,721084,961446,1362048,2163253,4566868], 124: [251328,307179,376992,466753,586433,753985,1005313,1424194,2261955,4775238], 125: [262290,320576,393434,487109,612009,786869,1049158,1486308,2360606,4983502], 126: [273265,333991,409898,507493,637619,819796,1093061,1548504,2459388,5192042], 127: [284236,347399,426354,527866,663217,852707,1136943,1610670,2558122,5400480], 128: [295194,360793,442791,548218,688786,885583,1180777,1672767,2656748,5608690], 129: [306162,374198,459243,568586,714377,918485,1224647,1734917,2755456,5817074], 130: [317133,387606,475699,588960,739976,951398,1268530,1797084,2854193,6025518], 131: [333101,407123,499651,618615,777235,999302,1332402,1887570,2997905,6328911], 132: [349074,426647,523612,648281,814507,1047223,1396298,1978089,3141670,6632415], 133: [365034,446153,547551,677920,851746,1095102,1460136,2068526,3285306,6935646], 134: [381003,465670,571504,707577,889007,1143009,1524011,2159016,3429026,7239054], 135: [396976,485193,595464,737241,926278,1190928,1587905,2249531,3572785,7542547], 136: [437949,535271,656924,813335,1021882,1313848,1751798,2481713,3941544,8321038], 137: [478917,585343,718376,889418,1117474,1436752,1915669,2713864,4310255,9099427], 138: [519873,635401,779810,965479,1213038,1559620,2079494,2945949,4678860,9877594], 139: [560849,685482,841274,1041577,1308648,1682547,2243396,3178144,5047641,10656131], 140: [601822,735560,902733,1117670,1404252,1805467,2407289,3410326,5416400,11434622] } remaining_ratio = 1.0 - (cur_perc / 100.0) def get_sp_for_gap(target_gap): total = 0.0 for lvl in range(start_lvl, max_lvl): eff_gap = max(0, min(lvl - start_mastery_lvl, 9)) if eff_gap >= target_gap: eff_gap = target_gap if lvl in SP_GAP_TABLE: sp_for_this_lvl = SP_GAP_TABLE[lvl][eff_gap] if lvl == start_lvl: sp_for_this_lvl *= remaining_ratio total += sp_for_this_lvl return total base_gap = -1 for g in range(10): if get_sp_for_gap(g) >= sp_needed: base_gap = g break if base_gap == -1: sp_gainable = get_sp_for_gap(9) res1 = f"{sp_needed} SP Needed, {int(sp_gainable)} SP will be Gained till Lvl {max_lvl} with Gap 9." res2 = "Warning: Gap 9 is not enough to reach the required SP!" elif base_gap == 0: sp_gainable = get_sp_for_gap(0) res1 = f"{sp_needed} SP Needed, {int(sp_gainable)} SP will be Gained till Lvl {max_lvl} with Gap 0." res2 = "You can stay on Gap 0 the entire time." else: pre_gap = base_gap gap = base_gap - 1 sp_gainable = get_sp_for_gap(gap) gap_change_lvl = start_lvl - 1 while sp_needed > sp_gainable and gap_change_lvl < max_lvl: gap_change_lvl += 1 eff_gap_lower = max(0, min(gap_change_lvl - start_mastery_lvl, 9)) if eff_gap_lower >= gap: eff_gap_lower = gap eff_gap_higher = max(0, min(gap_change_lvl - start_mastery_lvl, 9)) if eff_gap_higher >= pre_gap: eff_gap_higher = pre_gap if eff_gap_higher > eff_gap_lower and gap_change_lvl in SP_GAP_TABLE: diff = SP_GAP_TABLE[gap_change_lvl][eff_gap_higher] - SP_GAP_TABLE[gap_change_lvl][eff_gap_lower] if gap_change_lvl == start_lvl: diff *= remaining_ratio sp_gainable += diff eff_gap_pre = max(0, min(gap_change_lvl - start_mastery_lvl, 9)) if eff_gap_pre >= pre_gap: eff_gap_pre = pre_gap lvl_sp_full = SP_GAP_TABLE.get(gap_change_lvl, [0]*10)[eff_gap_pre] if gap_change_lvl == start_lvl: lvl_sp_remaining = lvl_sp_full * remaining_ratio else: lvl_sp_remaining = lvl_sp_full if lvl_sp_remaining > 0: percent_of_remaining = 100.0 - ((sp_gainable - sp_needed) / (lvl_sp_remaining / 100.0)) else: percent_of_remaining = 0.0 if gap_change_lvl == start_lvl: absolute_percent = cur_perc + (percent_of_remaining / 100.0 * (100.0 - cur_perc)) else: absolute_percent = percent_of_remaining res1 = f"{sp_needed} SP Needed, {int(sp_gainable)} SP will be Gained till Lvl {max_lvl} with Gap {pre_gap}." res2 = f"On lvl {gap_change_lvl} with {absolute_percent:.2f} % change to Gap {gap}" QtBind.setText(tab2, calc_result_label1, res1.ljust(110)) QtBind.setText(tab2, calc_result_label2, res2.ljust(110)) # ============================================================ # Auto Buy Items & Town Loop Gold Calculation # ============================================================ cached_db_path = None def _buy_items_db_path(client_path): global cached_db_path if cached_db_path and os.path.exists(cached_db_path): return cached_db_path plugin_dir = os.path.dirname(os.path.abspath(__file__)) data_dir = os.path.abspath(os.path.join(plugin_dir, '..', 'Data')) for candidate in glob.glob(os.path.join(data_dir, '*.db3')): try: conn = sqlite3.connect(candidate) row = conn.execute("SELECT v FROM data WHERE k='path' LIMIT 1").fetchone() conn.close() if row and str(row[0]).rstrip('\\/').lower() == str(client_path).rstrip('\\/').lower(): cached_db_path = candidate return candidate except Exception: pass return None def _buy_items_inventory_count(item_id=0, item_servername=''): """Count total quantity of an item across all inventory slots (including split stacks).""" total_count = 0 target_id = int(item_id or 0) target_sn = str(item_servername or '').strip().upper() try: inventory = get_inventory() or {} for entry in inventory.get('items') or []: if not entry: continue matched = False if target_id > 0: for key in ('model', 'id', 'item_id'): try: if int(entry.get(key, -1)) == target_id: matched = True break except Exception: pass if not matched and target_sn: entry_sn = str(entry.get('servername') or '').strip().upper() if not entry_sn: try: model_id = int(entry.get('model', 0) or 0) if model_id > 0: item_data = get_item(model_id) or {} entry_sn = str(item_data.get('servername') or '').strip().upper() except Exception: entry_sn = '' if entry_sn and entry_sn == target_sn: matched = True if matched: total_count += max(1, int(entry.get('quantity', 1) or 1)) except Exception: pass return total_count def _get_town_loop_gold_needed(db_path_data): try: char_name = get_character_name() server_name = get_server_name() char_data = get_character_data() if not char_name or not server_name or not char_data: return 0 char_level = int(char_data.get('level', 0)) char_db_path = os.path.join(get_config_dir(), f"{server_name}_{char_name}.db3") if not os.path.exists(char_db_path): return 0 conn_char = sqlite3.connect(char_db_path) conn_char.row_factory = sqlite3.Row town_items = conn_char.execute( "SELECT id, item_servername, quantity FROM town WHERE enabled = 1 AND quantity > 0 AND from_level IN (0, ?)", (char_level,) ).fetchall() conn_char.close() if not town_items: return 0 total_gold_needed = 0 conn_data = sqlite3.connect(db_path_data) if (db_path_data and os.path.exists(db_path_data)) else None try: for t_item in town_items: item_id = int(t_item['id'] or 0) item_sn = str(t_item['item_servername'] or '').strip() target_qty = int(t_item['quantity'] or 0) if item_id <= 0 and conn_data and item_sn: row_id = conn_data.execute("SELECT id FROM items WHERE servername = ? LIMIT 1", (item_sn,)).fetchone() if row_id and row_id[0]: item_id = int(row_id[0]) current_qty = _buy_items_inventory_count(item_id, item_sn) if target_qty > current_qty: if item_sn in ('ITEM_ETC_SCROLL_RETURN_01', 'ITEM_ETC_SCROLL_RETURN_02', 'ITEM_ETC_SCROLL_RETURN_03'): target_qty += 1 needed_qty = target_qty - current_qty price = _buy_items_get_price(item_id) if price <= 0 and conn_data and item_sn: row_price = conn_data.execute("SELECT price FROM items WHERE servername = ? LIMIT 1", (item_sn,)).fetchone() if row_price and row_price[0]: price = int(row_price[0]) if price > 0: total_gold_needed += (needed_qty * price) finally: if conn_data: conn_data.close() return total_gold_needed except Exception as ex: log('%s: Error calculating town loop gold: %s' % (Plugin, ex)) return 0 def _buy_items_get_gold(): try: data = get_character_data() or {} return int(data.get('gold', data.get('gold_amount', 0)) or 0) except Exception: return 0 def _buy_items_get_price(item_id): try: item_id = int(item_id or 0) if item_id <= 0: return 0 item_data = get_item(item_id) or {} price = item_data.get('price', 0) return max(0, int(price or 0)) except Exception: return 0 def _buy_items_inventory_has(item_id): return _buy_items_inventory_count(item_id=item_id) > 0 def _buy_items_category(servername): name = str(servername or '').upper() if '_SHIELD_' in name: return 'Shield' if any(token in name for token in ('_RING_', '_EARRING_', '_NECKLACE_')): return 'Accessory' if any(token in name for token in ('_HEAVY_', '_LIGHT_', '_CLOTHES_', '_ARMOR_', '_GARMENT_')): return 'Armor' if any(token in name for token in ('_SWORD_', '_BLADE_', '_SPEAR_', '_TWOHAND_', '_AXE_', '_BOW_', '_CROSSBOW_', '_DAGGER_', '_TSTAFF_', '_STAFF_', '_HARP_', '_TANK_', '_DARKSTAFF_')): return 'Weapon' return 'Other' def _buy_items_item_gender(servername): name = str(servername or '').upper() if name.startswith(('ITEM_CH_M_', 'ITEM_EU_M_')): return 'Male' if name.startswith(('ITEM_CH_W_', 'ITEM_EU_W_')): return 'Female' return None def _buy_items_character_gender(equipped_items): for _slot, item_data, entry in equipped_items: name = str(item_data.get('servername') or entry.get('servername') or '') gender = _buy_items_item_gender(name) if gender: return gender try: data = get_character_data() or {} for key in ('gender', 'sex', 'char_gender', 'character_gender'): value = str(data.get(key, '')).strip().lower() if value in ('m', 'male', '0', 'man'): return 'Male' if value in ('w', 'f', 'female', '1', 'woman'): return 'Female' except Exception: pass return None def _buy_items_find_upgrades(db_path, race, char_level, mastery_level=None, bought_item_ids=None): bought_values = [str(v).strip() for v in (bought_item_ids or []) if str(v).strip()] bought_names = {v.upper() for v in bought_values if not v.isdigit()} legacy_bought_ids = {int(v) for v in bought_values if v.isdigit() and int(v) > 0} inv = get_inventory() or {} items = inv.get('items') or [] equipped = [] for slot in range(min(13, len(items))): if slot == 8 or not items[slot]: continue entry = items[slot] try: item_data = get_item(int(entry.get('model', 0))) or {} if int(item_data.get('tid1', -1)) == 1: equipped.append((slot, item_data, entry)) except Exception: continue if not equipped: return [] character_gender = _buy_items_character_gender(equipped) bag_item_counts = {} for entry in items[13:]: if not entry: continue try: bag_data = get_item(int(entry.get('model', 0))) or {} bag_servername = str(bag_data.get('servername') or entry.get('servername') or '').strip().upper() if bag_servername: bag_item_counts[bag_servername] = bag_item_counts.get(bag_servername, 0) + max(1, int(entry.get('quantity', 1) or 1)) except Exception: continue conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row upgrades = {} try: if legacy_bought_ids: placeholders = ','.join('?' for _ in legacy_bought_ids) try: legacy_rows = conn.execute('SELECT id, servername FROM items WHERE id IN (%s)' % placeholders, tuple(legacy_bought_ids)).fetchall() bought_names.update(str(r['servername'] or '').strip().upper() for r in legacy_rows if r['servername']) except Exception: pass for slot, current, equipped_entry in equipped: tid = (int(current.get('tid1', -1)), int(current.get('tid2', -1)), int(current.get('tid3', -1))) current_level = int(current.get('level', 0)) race_pattern = '%_CH_%' if race == 'Chinese' else '%_EU_%' current_name = str(current.get('servername') or equipped_entry.get('servername') or '') slot_category = _buy_items_category(current_name) if race == 'Europe' and slot_category == 'Armor': slot_allowed_level = int(mastery_level if mastery_level is not None else 0) else: slot_allowed_level = int(char_level) rows = conn.execute(""" SELECT DISTINCT a.id, a.servername, a.name, a.level, a.tid1, a.tid2, a.tid3 FROM items a INNER JOIN npcgoods b ON a.id = b.item INNER JOIN monsters c ON b.model = c.id WHERE a.tid1=? AND a.tid2=? AND a.tid3=? AND a.level<=? AND a.servername LIKE ? AND (c.servername LIKE 'NPC_%_SMITH%' OR c.servername LIKE 'NPC_%_ARMOR%' OR c.servername LIKE 'NPC_%_ACCESSORY%') ORDER BY a.level DESC """, (tid[0], tid[1], tid[2], slot_allowed_level, race_pattern)).fetchall() best_item = None for row in rows: servername = str(row['servername'] or '') if servername.strip().upper() in bought_names: continue if race == 'Chinese' and '_CH_' not in servername: continue if race == 'Europe' and '_EU_' not in servername: continue if slot_category == 'Armor': required_gender = _buy_items_item_gender(current_name) or character_gender candidate_gender = _buy_items_item_gender(servername) if required_gender and candidate_gender != required_gender: continue if candidate_gender and not required_gender: continue best_item = dict(row) break replace = bool(best_item and int(best_item.get('level', 0)) > current_level) if replace: candidate_key = str(best_item.get('servername') or '').strip().upper() if candidate_key and bag_item_counts.get(candidate_key, 0) > 0: bag_item_counts[candidate_key] -= 1 log('%s: Buy items: %s is already in inventory; skipping purchase for slot %d.' % (Plugin, best_item.get('name') or best_item.get('servername'), slot)) continue current_label = '%s Lv.%d' % (current.get('name') or current.get('servername') or 'Unknown', current_level) best_label = '%s Lv.%s' % (best_item.get('name') or best_item.get('servername') or 'Unknown', best_item.get('level')) log('%s: Debug: Current=(%s) Buyable=(%s)' % (Plugin, current_label, best_label)) best_item['slot'] = slot best_item['current_level'] = current_level upgrades[slot] = best_item available = {} for candidate in upgrades.values(): rows = conn.execute(""" SELECT DISTINCT z.name AS zone_name, p.region FROM npcgoods g JOIN monsters c ON c.id = g.model JOIN npcpos p ON p.id = c.id JOIN zones z ON z.id = p.region WHERE g.item = ? AND (c.servername LIKE 'NPC_%_SMITH%' OR c.servername LIKE 'NPC_%_ARMOR%' OR c.servername LIKE 'NPC_%_ACCESSORY%') """, (int(candidate['id']),)).fetchall() towns = set() for row in rows: region = int(row['region']) town = BUY_REGION_TOWN.get(region) if not town: zn = str(row['zone_name'] or '').lower() if 'donwhang' in zn: town = 'Donwhang' elif zn == 'jangan': town = 'Jangan' elif 'hotan' in zn: town = 'Hotan' elif 'samarkand' in zn: town = 'Samarkand' elif 'constantinople' in zn: town = 'Constantinople' elif 'alexandria' in zn: town = 'Alexandria (North)' if region == 23603 else 'Alexandria (South)' if town: towns.add(town) available[int(candidate['id'])] = towns for slot, candidate in list(upgrades.items()): candidate['towns'] = available.get(int(candidate['id']), set()) if not candidate['towns']: del upgrades[slot] finally: conn.close() return list(upgrades.values()) def _buy_items_make_script(upgrades, startpos): town_order = ['Jangan', 'Donwhang', 'Hotan', 'Samarkand', 'Constantinople', 'Alexandria (South)', 'Alexandria (North)'] town_scores = { town: sum(1 for item in upgrades if town in item.get('towns', set())) for town in town_order } selected_town = max(town_order, key=lambda town: town_scores[town]) if town_order else None if not selected_town or town_scores.get(selected_town, 0) == 0: return [] town_routes = { 'Constantinople_1': [ 'path,26959,-10636,2598,80', 'walk,26959,-10627,2583,80', 'DoStorage', 'walk,26959,-10631,2589,80', 'walk,26959,-10641,2603,81', 'walk,26959,-10651,2617,80', 'walk,26959,-10662,2632,80', 'walk,26959,-10667,2639,83', 'DoBlacksmith', 'walk,26959,-10673,2646,83', ], 'Constantinople_2': [ 'walk,26959,-10680,2643,83', 'walk,26959,-10695,2635,79', 'walk,26959,-10714,2624,79', 'walk,26959,-10730,2616,79', 'walk,26959,-10739,2610,82', 'walk,26959,-10749,2605,83', ], 'Constantinople_3': [ 'walk,26959,-10747,2602,83', 'walk,26959,-10737,2591,79', 'walk,26959,-10727,2578,80', 'walk,26959,-10717,2565,83', 'walk,26959,-10705,2550,80', 'walk,26959,-10695,2538,80', 'walk,26959,-10687,2528,80', 'walk,26959,-10683,2523,80', ], 'Donwhang_1': [ 'path,26265,3567,1999,-106', 'walk,26265,3574,1992,-106', 'DoStorage', 'walk,26265,3568,2001,-106', 'walk,26265,3564,2021,-106', 'walk,26265,3567,2033,-106', 'DoBlacksmith', 'walk,26265,3571,2040,-106', ], 'Donwhang_2': [ 'walk,26265,3569,2033,-106', 'walk,26265,3570,2023,-106', 'walk,26265,3573,2011,-106', ], 'Donwhang_3': [ 'walk,26265,3559,2007,-106', 'walk,26265,3544,2003,-106', 'walk,26265,3527,1998,-105', 'walk,26265,3514,1995,-104', ], 'Hotan_1': [ 'path,23687,96,53,244', 'walk,23687,102,56,244', 'DoStorage', 'walk,23687,96,58,244', 'walk,23687,87,63,244', 'walk,23687,73,69,244', 'walk,23687,62,73,243', 'DoBlacksmith', 'walk,23687,52,75,243', ], 'Hotan_2': [ 'walk,23687,54,67,243', 'walk,23687,56,54,250', 'walk,23687,60,41,244', 'walk,23687,62,28,243', 'walk,23687,59,20,243', ], 'Hotan_3': [ 'walk,23687,65,15,243', 'walk,23687,72,8,243', 'walk,23431,84,-1,243', ], 'Jangan_1': [ 'path,25000,6433,1073,-32', 'walk,25000,6432,1064,-32', 'DoStorage', 'walk,25000,6427,1067,-32', 'walk,25000,6415,1076,-32', 'walk,25000,6405,1083,-33', 'walk,25000,6393,1093,-29', 'walk,25000,6386,1097,0', 'walk,25000,6383,1098,0', 'DoBlacksmith', 'walk,25000,6373,1099,0', ], 'Jangan_2': [ 'walk,25000,6373,1090,0', 'walk,25000,6372,1078,0', 'walk,25000,6371,1069,0', ], 'Jangan_3': [ 'walk,25000,6377,1068,0', 'walk,25000,6386,1068,0', 'walk,25000,6401,1068,-32', 'walk,25000,6420,1068,-32', 'walk,25000,6444,1068,-32', 'walk,25000,6472,1067,-32', 'walk,25000,6482,1068,0', 'walk,25000,6499,1068,0', ], 'Samarkand_1': [ 'path,27244,-5143,2817,180', 'walk,27244,-5132,2805,180', 'DoStorage', 'walk,27244,-5134,2811,180', 'walk,27244,-5145,2832,180', 'walk,27244,-5155,2853,180', 'walk,27244,-5163,2870,192', 'walk,27500,-5173,2892,192', 'walk,27500,-5180,2908,192', 'walk,27499,-5189,2924,180', 'walk,27499,-5195,2942,180', 'walk,27499,-5196,2948,182', 'DoBlacksmith', 'walk,27500,-5198,2957,180', ], 'Samarkand_2': [ 'walk,27500,-5202,2948,184', 'walk,27500,-5210,2935,186', 'walk,27500,-5219,2924,186', 'walk,27500,-5230,2916,184', 'walk,27500,-5243,2914,180', ], 'Samarkand_3': [ 'walk,27500,-5241,2908,180', 'walk,27500,-5237,2898,180', 'walk,27243,-5229,2877,180', 'walk,27243,-5224,2864,180', 'walk,27243,-5217,2849,180', 'walk,27243,-5212,2836,180', ], } selected_upgrades = [item for item in upgrades if selected_town in item.get('towns', set())] categories = {'WeaponShield': [], 'Armor': [], 'Accessory': []} for item in selected_upgrades: try: item_id = int(item.get('id', 0)) except Exception: continue servername = str(item.get('servername') or '').strip() if item_id <= 0 or not servername: continue category = _buy_items_category(servername) if category in ('Weapon', 'Shield'): categories['WeaponShield'].append({'id': item_id, 'servername': servername}) elif category == 'Armor': categories['Armor'].append({'id': item_id, 'servername': servername}) elif category == 'Accessory': categories['Accessory'].append({'id': item_id, 'servername': servername}) segment_keys = ['%s_%d' % (selected_town, n) for n in (1, 2, 3) if '%s_%d' % (selected_town, n) in town_routes] if not segment_keys: return [] segments = [] for key in segment_keys: route_lines = list(town_routes[key]) endpoint = None for line in reversed(route_lines): if line.startswith('walk,'): parts = line.split(',') try: endpoint = (int(parts[1]), float(parts[2]), float(parts[3])) except Exception: endpoint = None break segments.append({'name': key, 'lines': route_lines, 'endpoint': endpoint}) return { 'town': selected_town, 'segments': segments, 'categories': categories, 'candidate_items': [{'id': int(i['id']), 'servername': str(i.get('servername') or '')} for i in selected_upgrades if i.get('id') is not None] } def _buy_items_after_teleport(): global buy_items_state if not buy_items_enabled or buy_items_state: return if not _bot_is_running(): return try: char = get_character_data() or {} pos = get_position() or {} current_region = int(pos.get('region', char.get('region', 0)) or 0) if current_region not in BUY_REGION_TOWN: return client = get_client() or {} race = get_character_race() if race not in ('Chinese', 'Europe'): log('%s: Buy items: could not determine race.' % Plugin) return inventory = get_inventory() or {} equip = (inventory.get('items') or []) if not any(item for slot, item in enumerate(equip[:13]) if slot != 8): return char_level = int(char.get('level', 0)) mastery_level = 0 if race == 'Europe': masteries = get_mastery() or {} mastery_level = max([int(m.get('level', 0)) for m in masteries.values()] or [0]) db_path = _buy_items_db_path(client.get('path', '')) if not db_path: return char_key = get_character_key(char.get('name')) char_settings = character_settings.get(char_key, get_default_character_settings()) bought_item_ids = char_settings.get('bought_items', []) legacy_ids = [] for value in bought_item_ids: text_value = str(value or '').strip() if text_value.isdigit() and int(text_value) > 0: legacy_ids.append(int(text_value)) if legacy_ids: try: conn = sqlite3.connect(db_path) placeholders = ','.join('?' for _ in legacy_ids) rows = conn.execute('SELECT id, servername FROM items WHERE id IN (%s)' % placeholders, tuple(legacy_ids)).fetchall() conn.close() id_to_name = {int(row[0]): str(row[1] or '').strip() for row in rows} migrated_names = [] for value in bought_item_ids: text_value = str(value or '').strip() if text_value.isdigit(): name = id_to_name.get(int(text_value), '') else: name = text_value if name and name not in migrated_names: migrated_names.append(name) char_settings['bought_items'] = migrated_names bought_item_ids = migrated_names save_config() except Exception as migration_error: log('%s: Buy items: could not migrate old bought_items IDs: %s' % (Plugin, migration_error)) upgrades = _buy_items_find_upgrades(db_path, race, char_level, mastery_level, bought_item_ids) if not upgrades: log('%s: Buy items: no higher NPC equipment available within character/mastery level limits.' % Plugin) return startpos = (current_region, int(pos.get('x', char.get('x', 0))), int(pos.get('y', char.get('y', 0))), int(pos.get('z', 0))) plan = _buy_items_make_script(upgrades, startpos) if not plan: log('%s: Buy items: no suitable town/vendor route found.' % Plugin) return total = sum(len(items) for items in plan['categories'].values()) buy_items_state = { 'phase': 'stop_wait', 'started': time.time(), 'watchdog_started': time.time(), 'last_stop_request': time.time(), 'plan': plan, 'segment_index': 0, 'buy_index': 0, 'buy_waiting': False, 'character_key': get_character_key(char.get('name')), 'town': plan['town'], } log('%s: Buy items: %d upgrade item(s) found for %s; stopping bot.' % (Plugin, total, plan['town'])) stop_bot() except Exception as ex: buy_items_state = None log('%s: Buy items: preparation failed: %s' % (Plugin, ex)) def _buy_items_loop(): global buy_items_state if not buy_items_state: return state = buy_items_state now = time.time() plan = state.get('plan') or {} if state.get('phase') not in ('restart_script', 'restart_wait_teleport', 'restart_final_wait') and now - state.get('watchdog_started', now) >= 120.0: log('%s: Buy items: 120-second timeout; jumping to Restartbot.' % Plugin) state['phase'] = 'restart_script' state['started'] = now def save_if_in_inventory(item, expected_quantity=1): item_id = int(item.get('id', 0) or 0) item_name = str(item.get('servername') or '').strip() if item_id <= 0 or not item_name: return False name_upper = item_name.upper() if '_RING_' in name_upper: target_count = 2 else: target_count = 1 bag_count = 0 try: inventory = get_inventory() or {} items_list = inventory.get('items') or [] for entry in items_list[13:]: if not entry: continue matched = False for key in ('model', 'id', 'item_id'): try: if int(entry.get(key, -1)) == item_id: matched = True break except Exception: pass if not matched: entry_sn = str(entry.get('servername') or '').strip().upper() if entry_sn == name_upper: matched = True if matched: bag_count += max(1, int(entry.get('quantity', 1) or 1)) except Exception: pass if bag_count >= target_count: key = state.get('character_key') settings = character_settings.get(key) if key else None if isinstance(settings, dict): bought = settings.setdefault('bought_items', []) normalized = [] for value in bought: value = str(value or '').strip() if value and not value.isdigit() and value not in normalized: normalized.append(value) if item_name not in normalized: normalized.append(item_name) settings['bought_items'] = normalized save_config() log('%s: Buy items: confirmed %d of %s in inventory (target: %d); saved servername.' % (Plugin, bag_count, item_name, target_count)) return True return False phase = state.get('phase') if phase == 'stop_wait': if _bot_is_running(): if now - state.get('last_stop_request', 0) >= 2.0: stop_bot() state['last_stop_request'] = now return if now - state.get('started', now) >= 1.0: state['phase'] = 'start_segment' elif phase == 'start_segment': segments = plan.get('segments', []) idx = state.get('segment_index', 0) if idx >= 3 or idx >= len(segments): state['phase'] = 'restart_delay' state['started'] = now return segment = segments[idx] try: start_script('\n'.join(segment.get('lines', []))) state['active_segment'] = segment state['phase'] = 'wait_endpoint' state['started'] = now state['last_position_check'] = 0.0 log('%s: Buy items: started %s.' % (Plugin, segment.get('name', 'Town segment'))) except Exception as ex: log('%s: Buy items: could not start %s: %s' % (Plugin, segment.get('name', 'Town segment'), ex)) state['phase'] = 'restart_script' state['started'] = now elif phase == 'wait_endpoint': if now - state.get('last_position_check', 0.0) < 0.5: return state['last_position_check'] = now segment = state.get('active_segment') or {} endpoint = segment.get('endpoint') pos = get_position() or {} arrived = False if endpoint: try: arrived = (int(pos.get('region', 0)) == endpoint[0] and abs(float(pos.get('x', 0)) - endpoint[1]) <= 3 and abs(float(pos.get('y', 0)) - endpoint[2]) <= 3) except Exception: arrived = False if arrived: state['phase'] = 'endpoint_delay' state['started'] = now log('%s: Buy items: %s endpoint reached (X/Y tolerance +/-3); preparing to buy in 2 seconds.' % (Plugin, segment.get('name', 'Town segment'))) elif now - state.get('started', now) > 120.0: # 120 Sekunden Timeout, falls der Punkt nicht erreicht wird log('%s: Buy items: %s timeout (120s) reached before endpoint; continuing.' % (Plugin, segment.get('name', 'Town segment'))) state['phase'] = 'endpoint_delay' state['started'] = now elif phase == 'endpoint_delay': if now - state.get('started', now) < 2.0: return idx = state.get('segment_index', 0) category_by_segment = ['WeaponShield', 'Armor', 'Accessory'] segment = state.get('active_segment') or {} endpoint = segment.get('endpoint') or (0, 0, 0) # Regionen-Check entfernt, da der phBot nun den Weg eigenständig findet try: db_path = _buy_items_db_path((get_client() or {}).get('path', '')) if globals().get('buy_keep_town_gold_enabled', False): town_gold_needed = _get_town_loop_gold_needed(db_path) actual_threshold = max(BUY_GOLD_THRESHOLD, town_gold_needed) log('%s: Buy items: Town Loop Gold needed: %d, Manual Threshold: %d -> Active: %d' % (Plugin, town_gold_needed, BUY_GOLD_THRESHOLD, actual_threshold)) else: actual_threshold = BUY_GOLD_THRESHOLD except Exception: actual_threshold = BUY_GOLD_THRESHOLD state['dynamic_threshold'] = actual_threshold state['category'] = category_by_segment[idx] state['buy_index'] = 0 state['buy_waiting'] = False state['simulated_gold'] = _buy_items_get_gold() state['phase'] = 'buy_category' elif phase == 'buy_category': category = state.get('category') items = (plan.get('categories') or {}).get(category, []) idx = state.get('buy_index', 0) if idx >= len(items): state['phase'] = 'next_segment' return item = items[idx] price = _buy_items_get_price(item.get('id', 0)) active_threshold = state.get('dynamic_threshold', BUY_GOLD_THRESHOLD) if state.get('buy_waiting'): if now - state.get('started', now) < 8.0: return state['simulated_gold'] -= price log('%s: Buy items: wait finished, simulated gold reduced. Moving to next item.' % Plugin) state['buy_index'] = idx + 1 state['buy_waiting'] = False state['started'] = now return gold = state.get('simulated_gold', 0) if gold >= price + active_threshold: try: start_script('buy,%s,1' % item.get('servername', '')) log('%s: Buy items: started buy for %s (simulated gold=%d, price=%d, threshold=%d).' % (Plugin, item.get('servername', ''), gold, price, active_threshold)) state['buy_waiting'] = True state['started'] = now except Exception as ex: log('%s: Buy items: failed to start buy for %s: %s' % (Plugin, item.get('servername', ''), ex)) state['buy_index'] = idx + 1 else: log('%s: Buy items: insufficient gold for %s (needs %d + threshold %d).' % (Plugin, item.get('servername', ''), price, active_threshold)) state['buy_index'] = idx + 1 elif phase == 'next_segment': categories = plan.get('categories', {}) category_by_segment = ['WeaponShield', 'Armor', 'Accessory'] current_seg_idx = state.get('segment_index', 0) next_items_found = False for check_idx in range(current_seg_idx + 1, len(category_by_segment)): cat_name = category_by_segment[check_idx] if categories.get(cat_name): next_items_found = True break if next_items_found: state['segment_index'] = current_seg_idx + 1 if state['segment_index'] < 3: state['phase'] = 'start_segment' else: state['phase'] = 'restart_delay' state['started'] = now else: log('%s: Buy items: No more items in remaining categories; skipping unused route segments.' % Plugin) state['phase'] = 'restart_delay' state['started'] = now elif phase == 'restart_delay': if now - state.get('started', now) >= 1.0: state['phase'] = 'restart_script' state['started'] = now elif phase == 'restart_script': pos = get_position() or {} char = get_character_data() or {} state['return_start'] = (float(char.get('x', pos.get('x', 0))), float(char.get('y', pos.get('y', 0)))) try: start_script('use,returnscroll') state['phase'] = 'restart_wait_teleport' state['started'] = now state['last_teleport_check'] = now log('%s: Buy items: started returnscroll. Waiting for teleport.' % Plugin) except Exception as ex: log('%s: Buy items: returnscroll failed: %s' % (Plugin, ex)) state['phase'] = 'restart_delay' state['started'] = now elif phase == 'restart_wait_teleport': if now - state.get('last_teleport_check', 0) >= 2.0: state['last_teleport_check'] = now char = get_character_data() or {} current = (float(char.get('x', 0)), float(char.get('y', 0))) original = state.get('return_start', current) teleported = abs(current[0] - original[0]) > 1.0 or abs(current[1] - original[1]) > 1.0 timeout = now - state.get('started', now) >= 120.0 if teleported or timeout: if timeout: log('%s: Buy items: Teleport timeout reached (120s).' % Plugin) else: log('%s: Buy items: Teleport detected.' % Plugin) state['phase'] = 'restart_final_wait' state['started'] = now elif phase == 'restart_final_wait': if now - state.get('started', now) >= 5.0: log('%s: Buy items: Sequence finished. Checking inventory for all bought items.' % Plugin) plan = state.get('plan', {}) categories = plan.get('categories', {}) for cat_name, items in categories.items(): for item in items: save_if_in_inventory(item, 1) log('%s: Buy items: Starting bot.' % Plugin) _ensure_bot_started('Buy items') buy_items_state = None # ============================================================ # Auto Hometown Logic # ============================================================ HOMETOWN_MAX_ATTEMPTS = 2 HOMETOWN_STOP_DELAY = 4.0 HOMETOWN_TELEPORT_DELAY = 2.0 HOMETOWN_SPAWN_DELAY = 1.0 HOMETOWN_TARGET_TOLERANCE = 3.0 HOMETOWN_RETRY_WAIT = 3.0 HOMETOWN_DEFAULTS = [ {'enabled': False, 'race': 'Chinese', 'town': 'Jangan', 'level': 0, 'teleport': (25000, 6446, 1088, -32), 'spawn': (25000, 6433, 1099, -32)}, {'enabled': True, 'race': 'Chinese', 'town': 'Donwhang', 'level': 28, 'teleport': (26265, 3552, 2088, -106), 'spawn': (26265, 3554, 2070, -106)}, {'enabled': True, 'race': 'All', 'town': 'Hotan', 'level': 41, 'teleport': (23687, 113, 32, 244), 'spawn': (23687, 116, 15, 244)}, {'enabled': True, 'race': 'Europe', 'town': 'Samarkand', 'level': 32, 'teleport': (27244, -5176, 2870, 192), 'spawn': (27244, -5157, 2830, 180)}, {'enabled': False, 'race': 'Europe', 'town': 'Constantinople', 'level': 0, 'teleport': (26959, -10660, 2598, 83), 'spawn': (26959, -10654, 2601, 83)}, {'enabled': False, 'race': 'All', 'town': 'Alexandria S', 'level': 101, 'teleport': (23088, -16643, -294, 863), 'spawn': (23088, -16635, -331, 863)}, {'enabled': False, 'race': 'All', 'town': 'Alexandria N', 'level': 101, 'teleport': (23602, -16137, 61, 1525), 'spawn': (23603, -16117, 52, 1537)}, {'enabled': False, 'race': 'All', 'town': 'Baghdad', 'level': 130, 'teleport': (22618,-8534,-718,-236), 'spawn': (22618,-8533,-736,-236)}, {'enabled': False, 'race': 'All', 'town': 'Jangan', 'level': 0, 'teleport': (25000, 6446, 1088, -32), 'spawn': (25000, 6433, 1099, -32), 'custom_town': True}, ] hometown_rules = [] hometown_state = None hometown_last_character = None hometown_last_tab1_signature = None hometown_last_check = 0.0 auto_settings_signature = None def _hometown_copy_defaults(): return [dict(r, teleport=tuple(r['teleport']), spawn=tuple(r['spawn'])) for r in HOMETOWN_DEFAULTS] def _hometown_race_options(): return ['Chinese', 'Europe', 'All'] def _hometown_town_options(): return ['Jangan', 'Donwhang', 'Hotan', 'Samarkand', 'Constantinople', 'Alexandria S', 'Alexandria N', 'Baghdad'] def _hometown_build_tab1(): global hometown_master_check x = 450 hometown_master_check = QtBind.createCheckBox(tab2, 'hometown_master_clicked', '', x - 3, 5) QtBind.createLabel(tab2, 'Enable Auto Hometown', x + 22, 10) QtBind.createLabel(tab2, 'Set Hometown at a given Level per Race', x + 0, 33) y = 55 QtBind.createLabel(tab2, 'Enable', x + 0, y) QtBind.createLabel(tab2, 'Race', x + 45, y) QtBind.createLabel(tab2, 'Town', x + 120, y) QtBind.createLabel(tab2, 'At Lvl', x + 212, y) for i, rule in enumerate(HOMETOWN_DEFAULTS): yy = y + 23 + i * 27 rule['check'] = QtBind.createCheckBox(tab2, 'hometown_enable_clicked', '', x - 3, yy - 4) if rule.get('custom_town'): rule['race_label'] = QtBind.createLabel(tab2, 'All', x + 38, yy + 2) rule['combo'] = QtBind.createCombobox(tab2, x + 95, yy - 3, 115, 21) for town_name in _hometown_town_options(): QtBind.append(tab2, rule['combo'], town_name) else: rule['combo'] = QtBind.createCombobox(tab2, x + 30, yy - 3, 80, 21) for race in _hometown_race_options(): QtBind.append(tab2, rule['combo'], race) QtBind.createLabel(tab2, rule['town'], x + 120, yy + 2) rule['level_edit'] = QtBind.createLineEdit(tab2, str(rule['level']), x + 210, yy - 3, 45, 21) _hometown_build_tab1() def _hometown_character_defaults(): return {'hometown_rules': [dict(r, teleport=tuple(r['teleport']), spawn=tuple(r['spawn'])) for r in HOMETOWN_DEFAULTS]} def _hometown_ensure_character(): global current_character global hometown_rules char = get_character_key() if not char: return False current_character = char settings = character_settings.setdefault(char, get_default_character_settings()) rules = settings.get('hometown_rules') if isinstance(settings, dict) else None if not isinstance(rules, list) or len(rules) != len(HOMETOWN_DEFAULTS) or any(not isinstance(r, dict) for r in rules): rules = _hometown_character_defaults()['hometown_rules'] settings['hometown_rules'] = rules hometown_rules = rules return True def _hometown_refresh_tab1(): if not _hometown_ensure_character(): return settings = character_settings.get(current_character, {}) QtBind.setChecked(tab2, hometown_master_check, bool(settings.get('hometown_enabled', True))) for i, rule in enumerate(hometown_rules): base = HOMETOWN_DEFAULTS[i] QtBind.setChecked(tab2, base['check'], bool(rule.get('enabled', False))) QtBind.setText(tab2, base['level_edit'], str(int(rule.get('level', base['level'])))) try: QtBind.setText(tab2, base['combo'], rule.get('town', base['town']) if base.get('custom_town') else rule.get('race', base['race'])) except Exception: pass def _hometown_read_tab1(save=True): global hometown_rules if not _hometown_ensure_character(): return False changed = False for i, base in enumerate(HOMETOWN_DEFAULTS): rule = hometown_rules[i] enabled_now = bool(QtBind.isChecked(tab2, base['check'])) if base.get('custom_town'): town_now = QtBind.text(tab2, base['combo']).strip() if town_now not in _hometown_town_options(): town_now = str(rule.get('town', 'Jangan')) race_now = 'All' town_base = next((r for r in HOMETOWN_DEFAULTS if not r.get('custom_town') and r['town'] == town_now), HOMETOWN_DEFAULTS[0]) else: race_now = QtBind.text(tab2, base['combo']).strip() if race_now not in _hometown_race_options(): race_now = base['race'] town_now = rule.get('town', base['town']) town_base = None try: level_now = int(QtBind.text(tab2, base['level_edit']).strip()) except Exception: level_now = int(rule.get('level', base['level'])) level_now = max(0, min(255, level_now)) if (rule.get('enabled') != enabled_now or rule.get('race') != race_now or int(rule.get('level', 0)) != level_now or (base.get('custom_town') and rule.get('town') != town_now)): rule['enabled'] = enabled_now rule['race'] = race_now rule['level'] = level_now if base.get('custom_town'): rule['town'] = town_now rule['teleport'] = tuple(town_base['teleport']) rule['spawn'] = tuple(town_base['spawn']) changed = True QtBind.setText(tab2, base['level_edit'], str(level_now)) if changed and save: character_settings[current_character]['hometown_rules'] = hometown_rules if not any(bool(rule.get('enabled', False)) for rule in hometown_rules): character_settings[current_character]['hometown_enabled'] = False QtBind.setChecked(tab2, hometown_master_check, False) save_config() return changed def hometown_master_clicked(checked): if not _hometown_ensure_character(): return character_settings[current_character]['hometown_enabled'] = bool(checked) save_config() log('%s: Auto Hometown manually %s for [%s].' % (Plugin, 'enabled' if checked else 'disabled', current_character)) def hometown_enable_clicked(checked): _hometown_read_tab1(save=True) def _hometown_signature(): sig = [] try: master = bool(QtBind.isChecked(tab2, hometown_master_check)) except Exception: master = True sig.append(('master', master)) for base in HOMETOWN_DEFAULTS: try: level = QtBind.text(tab2, base['level_edit']).strip() except Exception: level = '' try: race = QtBind.text(tab2, base['combo']).strip() except Exception: race = '' try: enabled_now = bool(QtBind.isChecked(tab2, base['check'])) except Exception: enabled_now = False sig.append((enabled_now, race, level)) return tuple(sig) def _hometown_current_region(): try: p = get_position() return int(p.get('region', 0)) if p else 0 except Exception: return 0 def _hometown_current_town(): region = _hometown_current_region() for rule in HOMETOWN_DEFAULTS: if int(rule['spawn'][0]) == region or int(rule['teleport'][0]) == region: return rule['town'] return None def _hometown_rule_matches(rule, race, level): return bool(rule.get('enabled')) and int(rule.get('level', 0)) <= level and rule.get('race') in ('All', race) def _hometown_select_rule(race, level): candidates = [r for r in hometown_rules if _hometown_rule_matches(r, race, level)] if not candidates: return None return max(candidates, key=lambda r: int(r.get('level', 0))) def _hometown_generate_and_start(target, label): region, x, y, z = target try: lines = generate_script(region, x, y, z) except Exception as ex: log('%s: Auto Hometown: generate_script failed [%s]: %s' % (Plugin, label, ex)) return False if not lines: log('%s: Auto Hometown: generate_script returned no route [%s].' % (Plugin, label)) return False lines = [str(line).strip() for line in lines if str(line).strip()] try: start_script('\n'.join(lines)) except Exception as ex: log('%s: Auto Hometown: start_script failed [%s]: %s' % (Plugin, label, ex)) return False log('%s: Auto Hometown: Walk to [%s].' % (Plugin, label)) return True def _hometown_disable_through_level(level): changed = False for i, rule in enumerate(hometown_rules): if int(rule.get('level', 0)) <= level and rule.get('enabled'): rule['enabled'] = False changed = True if changed: character_settings[current_character]['hometown_rules'] = hometown_rules if not any(bool(item.get('enabled', False)) for item in hometown_rules): character_settings[current_character]['hometown_enabled'] = False _hometown_refresh_tab1() save_config() def _hometown_finish(rule): level = int(rule.get('level', 0)) _hometown_disable_through_level(level) HOMETOWN_REGION_NAMES = { 25000: 'Jangan', 26265: 'Donwhang', 23687: 'Hotan', 27244: 'Samarkand', 26959: 'Constantinople', 23602: 'alexandria', 23603: 'alexandria', 23088: 'alexandria', 22618: 'Baghdad', } def _hometown_find_recall_npc(rule): target_region = int(rule['spawn'][0]) expected_name = HOMETOWN_REGION_NAMES.get(target_region, rule.get('town', '')) npcs = get_npcs() or {} for uid, npc in npcs.items(): npc_name = str(npc.get('name', '')) if expected_name.strip().lower() in npc_name.strip().lower(): return uid, npc_name return 0, expected_name def _hometown_set_recall(rule): try: uid, npc_name = _hometown_find_recall_npc(rule) if not uid: return False import struct inject_joymax(0x7059, struct.pack('= HOMETOWN_MAX_ATTEMPTS: log('%s: Auto Hometown: %d/%d attempts failed. Rule [%s] disabled.' % (Plugin, attempt, HOMETOWN_MAX_ATTEMPTS, rule['town'])) rule['enabled'] = False if not any(bool(item.get('enabled', False)) for item in hometown_rules): character_settings[current_character]['hometown_enabled'] = False _hometown_refresh_tab1() save_config() hometown_state = None _ensure_bot_started('Auto Hometown') return hometown_state['attempt'] = attempt + 1 hometown_state['phase'] = 'retry_wait' hometown_state['started'] = time.time() log('%s: Auto Hometown: retrying [%s] attempt %d/%d.' % (Plugin, rule['town'], hometown_state['attempt'], HOMETOWN_MAX_ATTEMPTS)) def _auto_save_tab1_settings(): global auto_settings_signature global BUY_GOLD_THRESHOLD global buy_keep_town_gold_enabled if loading_character_settings or not current_character: return try: sig = ( bool(QtBind.isChecked(tab1, checkEnable)), bool(QtBind.isChecked(tab1, checkSolo)), QtBind.text(tab1, editMembers).strip(), QtBind.text(tab1, editLevelOffset).strip(), bool(QtBind.isChecked(tab1, checkXControl)), QtBind.text(tab1, editXControlStop), QtBind.text(tab1, editXControlDelay1), QtBind.text(tab1, editXControlSetPos), QtBind.text(tab1, editXControlSetRange), QtBind.text(tab1, editXControlDelay2), QtBind.text(tab1, editXControlStart), QtBind.text(tab2, buy_gold_threshold_edit).strip() if 'buy_gold_threshold_edit' in globals() else '0', bool(QtBind.isChecked(tab2, buy_keep_town_gold_check)) if 'buy_keep_town_gold_check' in globals() else False ) except Exception: return if auto_settings_signature is None: auto_settings_signature = sig return if sig == auto_settings_signature: return auto_settings_signature = sig try: checkEnable_clicked(sig[0]) checkSolo_clicked(sig[1]) saveMembers_clicked() saveLevelOffset_clicked() read_xcontrol_tab1() try: threshold_text = QtBind.text(tab2, buy_gold_threshold_edit).strip() threshold_value = max(0, int(threshold_text)) if threshold_text else 0 except Exception: threshold_value = 0 BUY_GOLD_THRESHOLD = threshold_value QtBind.setText(tab2, buy_gold_threshold_edit, str(threshold_value)) buy_keep_town_gold_enabled = sig[12] save_character_settings() save_config() except Exception as ex: log('%s: Automatic GUI save error: %s' % (Plugin, ex)) def _bot_is_running(): try: data = get_character_data() except Exception: return False return bool(data.get('botting', False)) if isinstance(data, dict) else False def _ensure_bot_started(context): if _bot_is_running(): return True try: started = start_bot() if started: return True log('%s: %s: start_bot() did not confirm that the bot started.' % (Plugin, context)) except Exception as ex: log('%s: %s: start_bot failed: %s' % (Plugin, context, ex)) return False def _hometown_loop(): global hometown_state global hometown_last_check global hometown_last_character global hometown_last_tab1_signature char = get_character_key() if not char: return if char != hometown_last_character: hometown_last_character = char _hometown_ensure_character() _hometown_refresh_tab1() hometown_last_tab1_signature = _hometown_signature() hometown_state = None hometown_last_check = 0.0 return sig = _hometown_signature() if sig != hometown_last_tab1_signature: master_now = bool(sig[0][1]) if sig and sig[0][0] == 'master' else True if master_now: _hometown_read_tab1(save=True) else: character_settings[current_character]['hometown_enabled'] = False save_config() hometown_last_tab1_signature = sig now = time.time() if hometown_state: phase = hometown_state['phase'] rule = hometown_state['rule'] if phase == 'retry_wait': if now - hometown_state['started'] >= HOMETOWN_RETRY_WAIT: hometown_state['phase'] = 'stop_wait' hometown_state['started'] = now try: stop_bot() except Exception: pass return if phase == 'stop_wait': if now - hometown_state['started'] >= HOMETOWN_STOP_DELAY: hometown_state['phase'] = 'route_to_destination' hometown_state['started'] = now if not _hometown_generate_and_start(rule['teleport'], 'TELEPORT %s' % rule['town']): _hometown_retry_or_disable() return if phase == 'route_to_destination': try: pos = get_position() or {} px = float(pos.get('x', 0)) py = float(pos.get('y', 0)) pregion = int(pos.get('region', 0)) target_region = int(rule['teleport'][0]) target_x = float(rule['teleport'][1]) target_y = float(rule['teleport'][2]) close_xy = (abs(px - target_x) <= HOMETOWN_TARGET_TOLERANCE and abs(py - target_y) <= HOMETOWN_TARGET_TOLERANCE) if pregion == target_region and close_xy: hometown_state['phase'] = 'before_recall' hometown_state['started'] = now log('%s: Auto Hometown: teleport point [%s] reached. Waiting %.1fs before recall.' % (Plugin, rule['town'], HOMETOWN_TELEPORT_DELAY)) elif now - hometown_state['started'] > 120: log('%s: Auto Hometown: teleport point [%s] not reached after 120s.' % (Plugin, rule['town'])) _hometown_retry_or_disable() except Exception as ex: log('%s: Auto Hometown: destination position check error: %s' % (Plugin, ex)) return if phase == 'before_recall': if now - hometown_state['started'] >= HOMETOWN_TELEPORT_DELAY: if _hometown_set_recall(rule): hometown_state['phase'] = 'after_recall_wait' hometown_state['started'] = now else: _hometown_retry_or_disable() return if phase == 'after_recall_wait': if now - hometown_state['started'] >= HOMETOWN_SPAWN_DELAY: hometown_state['phase'] = 'route_to_spawn' hometown_state['started'] = now if not _hometown_generate_and_start(rule['spawn'], 'SPAWN %s' % rule['town']): _hometown_retry_or_disable() return if phase == 'route_to_spawn': try: pos = get_position() or {} px = float(pos.get('x', 0)) py = float(pos.get('y', 0)) pregion = int(pos.get('region', 0)) target_region = int(rule['spawn'][0]) target_x = float(rule['spawn'][1]) target_y = float(rule['spawn'][2]) close_xy = (abs(px - target_x) <= HOMETOWN_TARGET_TOLERANCE and abs(py - target_y) <= HOMETOWN_TARGET_TOLERANCE) if pregion == target_region and close_xy: hometown_state['phase'] = 'before_start' hometown_state['started'] = now log('%s: Auto Hometown: spawn [%s] reached. Waiting %.1fs before bot start.' % (Plugin, rule['town'], HOMETOWN_SPAWN_DELAY)) elif now - hometown_state['started'] > 120: log('%s: Auto Hometown: spawn [%s] not reached after 120s.' % (Plugin, rule['town'])) _hometown_retry_or_disable() except Exception as ex: log('%s: Auto Hometown: spawn position check error: %s' % (Plugin, ex)) return if phase == 'before_start': if now - hometown_state['started'] >= HOMETOWN_SPAWN_DELAY: _hometown_finish(rule) hometown_state = None _ensure_bot_started('Auto Hometown') return return settings = character_settings.get(current_character, {}) if not bool(settings.get('hometown_enabled', True)): return try: char_data = get_character_data() except Exception: return if not isinstance(char_data, dict) or not bool(char_data.get('botting', False)): return current_region = _hometown_current_region() town_regions = {int(r['teleport'][0]) for r in HOMETOWN_DEFAULTS} | {int(r['spawn'][0]) for r in HOMETOWN_DEFAULTS} if current_region not in town_regions: return if now - hometown_last_check < 2.0: return hometown_last_check = now level = int(char_data.get('level', 0)) race = get_character_race() if race not in ('Chinese', 'Europe'): return _hometown_ensure_character() rule = _hometown_select_rule(race, level) if not rule: return current_town = _hometown_current_town() or ('Region %d' % current_region) log('%s: Auto Hometown: Change town from %s to %s' % (Plugin, current_town, rule['town'])) log('%s: Auto Hometown: Generate script from Region %d X %d Y %d Z %d to' % (Plugin, current_region, int(get_position().get('x', 0)), int(get_position().get('y', 0)), int(get_position().get('z', 0)))) log('%s: Auto Hometown: %s Region %d X %d Y %d Z %d' % (Plugin, rule['town'], int(rule['spawn'][0]), int(rule['spawn'][1]), int(rule['spawn'][2]), int(rule['spawn'][3]))) _hometown_start_change(rule) # ============================================================ # Game & phBot Events # ============================================================ def load_character_settings_when_ready(): character = get_character_name() if character: load_character_settings() return Timer(2.0, load_character_settings_when_ready, []).start() def joined_game(): global area_change_pending global pending_area global area_change_stage cancel_area_change_timer() area_change_pending = False pending_area = None area_change_stage = 0 reset_area_change_death_timer() log('%s: Character joined game.' % Plugin) Timer(1.0, load_character_settings_when_ready, []).start() def teleported(): global buy_items_pending try: if buy_items_state: log('%s: Buy items: teleport ignored; AutoBuy is already running.' % Plugin) return if buy_items_pending: return buy_items_pending = True except Exception as ex: log('%s: Buy items: teleport trigger failed: %s' % (Plugin, ex)) def handle_joymax(opcode, data): if enabled and opcode == 0x3056: check_party() return True def handle_event(t, data): global area_change_pending global area_change_death_remaining if t == 7 and area_change_pending and area_change_stage in (1, 2): area_change_death_remaining = DEATH_WAIT_TIMEOUT log('%s: Character died during area change. Maximum recovery wait is %d seconds.' % (Plugin, DEATH_WAIT_TIMEOUT)) schedule_area_change(1.0) # ============================================================ # Update Checker # ============================================================ def check_for_update(): import urllib.request import re import ssl try: ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE url = "https://raw.githubusercontent.com/chefkoch1837/phBot/main/BetterTraining.py?t=" + str(int(time.time())) req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req, timeout=5, context=ctx) as response: content = response.read().decode('utf-8') match = re.search(r"PluginVersion\s*=\s*['\"]([^'\"]+)['\"]", content) if match: online_version_str = match.group(1).strip() # Hilfsfunktion: Macht aus "1.1.2 Beta" -> [1, 1, 2] def parse_v(v_str): v_match = re.search(r'([0-9]+(?:\.[0-9]+)*)', v_str) if v_match: return [int(x) for x in v_match.group(1).split('.')] return [0] online_v = parse_v(online_version_str) local_v = parse_v(PluginVersion) if online_v > local_v: msg = 'New version available: v%s > v%s' % (PluginVersion, online_version_str) log('%s: %s' % (Plugin, msg)) log('BetterTraining: https://github.com/chefkoch1837/phBot/blob/main/BetterTraining.py') # Hier ändern wir den Text des Labels und füllen ihn auf 60 Zeichen auf try: QtBind.setText(tab1, lbl_version, msg.ljust(60)) except Exception as ui_ex: log('%s: Could not update UI label: %s' % (Plugin, ui_ex)) else: log('%s: Plugin is up to date.' % Plugin) else: log('%s: Update Check - Could not find PluginVersion in online file.' % Plugin) except Exception as ex: log('%s: Update Check failed: %s' % (Plugin, str(ex))) def event_loop(): global buy_items_pending try: _auto_save_tab1_settings() _hometown_loop() _buy_items_loop() if buy_items_pending and not hometown_state and not buy_items_state: buy_items_pending = False Timer(1.0, _buy_items_after_teleport, []).start() except Exception as ex: log('%s: BetterTraining loop error: %s' % (Plugin, ex)) # ============================================================ # Startup # ============================================================ load_config() Timer(2.0, load_character_settings_when_ready, []).start() Timer(5.0, check_for_update, []).start() # <--- DIESE ZEILE HINZUFÜGEN log('Plugin: %s by Chefkoch, Inspired by Bine11 AutoTraining' % Plugin) log('Plugin: %s v%s successfully loaded!' % (Plugin, PluginVersion))