' %
(width, color, _format_gui_text(key, args))
)
def _apply_gui_language():
QtBind.setText(gui, btnControlPage, _text('control'))
QtBind.setText(gui, btnButtonsPage, _text('buttons'))
QtBind.setText(gui, btnLanguage, 'TR' if current_language == 'en' else 'EN')
QtBind.setText(gui, lblCommands, '%s' % _text('commands_title'))
QtBind.setText(gui, cbxSro, _text('show_client_packets'))
QtBind.setText(gui, cbxJmx, _text('show_server_packets'))
QtBind.setText(gui, cbxIgnoreSetLeader, _text('ignore_set_leader'))
QtBind.setText(gui, lblLeaders, '%s' % _text('leaders'))
QtBind.setText(gui, btnAddLeader, _text('add'))
QtBind.setText(gui, btnRemLeader, _text('remove'))
QtBind.setText(gui, cbxAnnounceOwnTp, _text('announce_tps'))
QtBind.setText(gui, lblAnnounceChannel, _text('announce_channel'))
QtBind.setText(gui, lblButtonsTitle, '%s' % _text('buttons_title'))
QtBind.setText(gui, lblButtonsChannel, '%s' % _text('chat_type'))
QtBind.setText(gui, lblJobSuitGroup, '%s' % _text('job_suit_group'))
QtBind.setText(gui, btnEquipJobSuit, _text('equip_job'))
QtBind.setText(gui, btnUnequipJobSuit, _text('unequip_job'))
QtBind.setText(gui, lblSetRadiusGroup, '%s' % _text('set_radius_group'))
QtBind.setText(gui, lblRadius, _text('radius'))
QtBind.setText(gui, btnSetRadius, _text('set_radius'))
QtBind.setText(gui, lblSetProfileGroup, '%s' % _text('set_profile_group'))
QtBind.setText(gui, lblProfileName, _text('profile_name'))
QtBind.setText(gui, btnSetProfile, _text('set_profile'))
QtBind.setText(gui, lblQuickControlGroup, '%s' % _text('quick_control'))
QtBind.setText(gui, lblActionsGroup, '%s' % _text('actions'))
QtBind.setText(gui, lblSpecialGroup, '%s' % _text('special'))
for key, widget in quick_control_buttons.items():
QtBind.setText(gui, widget, _text(key))
for key, widget in action_buttons.items():
QtBind.setText(gui, widget, _text(key))
for key, widget in special_buttons.items():
QtBind.setText(gui, widget, _text(key))
_refresh_command_list()
_render_status(lblJobSuitStatus, _job_suit_status)
_render_status(lblSetRadiusStatus, _radius_status)
_render_status(lblSetProfileStatus, _profile_status)
_render_status(lblQuickStatus, _quick_status, 400)
def toggle_language():
global current_language
current_language = 'tr' if current_language == 'en' else 'en'
_apply_gui_language()
def _set_job_suit_status(key, args=(), color='#9aa0ac'):
global _job_suit_status
_job_suit_status = (key, args, color)
_render_status(lblJobSuitStatus, _job_suit_status)
def _send_job_suit_command(command):
channel = QtBind.text(gui, cbxButtonsChannel) or 'All'
if channel not in ANNOUNCE_CHANNELS:
channel = 'All'
message = '%s job' % command
sent = ANNOUNCE_CHANNELS[channel](message)
if sent:
_set_job_suit_status('sent_command', (channel, message), '#3cff7a')
log('Plugin: Buttons: Sent on %s: %s' % (channel, message))
else:
_set_job_suit_status('send_failed', (message,), '#ff4b5c')
log('Plugin: Buttons: Chat send failed on %s: %s' % (channel, message))
return sent
def btnEquipJobSuit_clicked():
return _send_job_suit_command('EQ')
def btnUnequipJobSuit_clicked():
return _send_job_suit_command('UQ')
def _set_radius_status(key, args=(), color='#9aa0ac'):
global _radius_status
_radius_status = (key, args, color)
_render_status(lblSetRadiusStatus, _radius_status)
def btnSetRadius_clicked():
value = (QtBind.text(gui, tbxRadius) or '').strip()
try:
radius = abs(int(float(value)))
except (ValueError, OverflowError):
_set_radius_status('radius_required', (), '#ff4b5c')
log('Plugin: Buttons: Radius must be a finite numeric value')
return False
channel = QtBind.text(gui, cbxButtonsChannel) or 'All'
if channel not in ANNOUNCE_CHANNELS:
channel = 'All'
message = 'SR %d' % radius
sent = ANNOUNCE_CHANNELS[channel](message)
if sent:
_set_radius_status('sent_command', (channel, message), '#3cff7a')
log('Plugin: Buttons: Sent on %s: %s' % (channel, message))
else:
_set_radius_status('send_failed', (message,), '#ff4b5c')
log('Plugin: Buttons: Chat send failed on %s: %s' % (channel, message))
return sent
def _set_profile_status(key, args=(), color='#9aa0ac'):
global _profile_status
_profile_status = (key, args, color)
_render_status(lblSetProfileStatus, _profile_status)
def btnSetProfile_clicked():
profile_name = (QtBind.text(gui, tbxProfileName) or '').strip()
if not profile_name:
_set_profile_status('profile_required', (), '#ff4b5c')
log('Plugin: Buttons: Profile name is empty')
return False
channel = QtBind.text(gui, cbxButtonsChannel) or 'All'
if channel not in ANNOUNCE_CHANNELS:
channel = 'All'
message = 'SETPROFILE ' + profile_name
sent = ANNOUNCE_CHANNELS[channel](message)
if sent:
_set_profile_status('sent_command', (channel, message), '#3cff7a')
log('Plugin: Buttons: Sent on %s: %s' % (channel, message))
else:
_set_profile_status('send_failed', (message,), '#ff4b5c')
log('Plugin: Buttons: Chat send failed on %s: %s' % (channel, message))
return sent
def _send_button_command(command):
global _quick_status
channel = QtBind.text(gui, cbxButtonsChannel) or 'All'
if channel not in ANNOUNCE_CHANNELS:
channel = 'All'
sent = ANNOUNCE_CHANNELS[channel](command)
if sent:
_quick_status = ('sent_command', (channel, command), '#3cff7a')
log('Plugin: Buttons: Sent on %s: %s' % (channel, command))
else:
_quick_status = ('send_failed', (command,), '#ff4b5c')
log('Plugin: Buttons: Chat send failed on %s: %s' % (channel, command))
_render_status(lblQuickStatus, _quick_status, 400)
return sent
def btnQuickStart_clicked():
return _send_button_command('S')
def btnQuickStop_clicked():
return _send_button_command('SS')
def btnQuickTrace_clicked():
return _send_button_command('T')
def btnQuickStopTrace_clicked():
return _send_button_command('N')
def btnQuickFollow_clicked():
return _send_button_command('FL')
def btnQuickStopFollow_clicked():
return _send_button_command('NF')
def btnQuickReturn_clicked():
return _send_button_command('RE')
def btnQuickCome_clicked():
return _send_button_command('COME')
def btnQuickLeaveParty_clicked():
return _send_button_command('LP')
def btnQuickMount_clicked():
return _send_button_command('M')
def btnQuickDismount_clicked():
return _send_button_command('D')
def btnQuickSit_clicked():
return _send_button_command('SIT')
def btnQuickBerserk_clicked():
return _send_button_command('ZK')
def btnQuickPickAll_clicked():
return _send_button_command('PA')
def btnQuickStopPick_clicked():
return _send_button_command('SPA')
def btnQuickSort_clicked():
return _send_button_command('SORT')
def btnQuickRepair_clicked():
return _send_button_command('REPAIR')
def btnQuickStorage_clicked():
return _send_button_command('TIS')
def btnQuickClock_clicked():
return _send_button_command('CLOCK')
def btnQuickDevilExt_clicked():
return _send_button_command('DEVILEXT')
_apply_gui_language()
def show_control_page():
global active_page
active_page = 'control'
for widget, x, y in buttons_widgets:
QtBind.move(gui, widget, OFFSCREEN_X, y)
for widget, x, y in control_widgets:
QtBind.move(gui, widget, x, y)
def show_buttons_page():
global active_page
active_page = 'buttons'
for widget, x, y in control_widgets:
QtBind.move(gui, widget, OFFSCREEN_X, y)
for widget, x, y in buttons_widgets:
QtBind.move(gui, widget, x, y)
def cbxAnnounceOwnTp_clicked(checked):
global announce_own_teleports
announce_own_teleports = checked
save_announce_settings()
def cbxIgnoreSetLeader_clicked(checked):
global ignore_setfcontrolleader
ignore_setfcontrolleader = bool(checked)
save_announce_settings()
def get_announce_channel():
"""Read the selected announce channel directly because QtBind has no documented
combobox change callback."""
try:
value = QtBind.text(gui, cbxAnnounceChannel)
return value if value in ANNOUNCE_CHANNELS else 'All'
except Exception:
return 'All'
def save_announce_settings():
"""Save teleport announcement settings for the current character."""
data = {}
config_path = getAnnounceConfig()
if not config_path:
log('Plugin: Character data is not ready; announce settings were not saved yet')
return False
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
data = json.load(f)
except Exception:
data = {}
data['AnnounceOwnTeleports'] = announce_own_teleports
data['AnnounceChannel'] = get_announce_channel()
data['IgnoreSetFControlLeader'] = ignore_setfcontrolleader
if not os.path.exists(getPath()):
os.makedirs(getPath())
try:
with open(config_path, "w") as f:
f.write(json.dumps(data, indent=4, sort_keys=True))
except Exception as e:
log('Plugin: Error saving announce settings - ' + str(e))
return False
return True
def load_announce_settings():
"""Load teleport announcement settings for the current character."""
global announce_own_teleports, ignore_setfcontrolleader
global _last_seen_announce_channel, _announce_settings_loaded_for
config_file = getAnnounceConfig()
if not config_file:
return False
saved_channel = 'All'
announce_own_teleports = False
ignore_setfcontrolleader = True
if os.path.exists(config_file):
try:
with open(config_file, "r") as f:
data = json.load(f)
announce_own_teleports = bool(data.get("AnnounceOwnTeleports", False))
ignore_setfcontrolleader = bool(data.get("IgnoreSetFControlLeader", True))
saved_channel = data.get("AnnounceChannel", "All")
if saved_channel not in ANNOUNCE_CHANNELS:
saved_channel = 'All'
except Exception as e:
log('Plugin: Error loading announce settings - ' + str(e))
return False
QtBind.setChecked(gui, cbxAnnounceOwnTp, announce_own_teleports)
QtBind.setChecked(gui, cbxIgnoreSetLeader, ignore_setfcontrolleader)
QtBind.setText(gui, cbxAnnounceChannel, saved_channel)
_last_seen_announce_channel = saved_channel
_announce_settings_loaded_for = config_file
return True
cbxDontShow_checked = True
lstOpcodesData = []
# ______________________________ Methods ______________________________ #
# Return plugin configs path (JSON)
def getPluginConfig():
return get_config_dir() + pName + ".json"
# Load default configs for opcodes
def loadDefaultOpcodeConfig():
global lstOpcodesData
lstOpcodesData = []
# Load the list of opcodes with the config file
def loadOpcodeConfigs():
loadDefaultOpcodeConfig()
if os.path.exists(getPluginConfig()):
data = {}
with open(getPluginConfig(), "r") as f:
data = json.load(f)
if "FilteredOpcodes" in data:
global lstOpcodesData
lstOpcodesData = data["FilteredOpcodes"]
if "DontShow" in data:
global cbxDontShow_checked
cbxDontShow_checked = data["DontShow"]
# Save all config
def saveConfigs():
data = {}
data['DontShow'] = cbxDontShow_checked
data['FilteredOpcodes'] = lstOpcodesData
with open(getPluginConfig(), "w") as f:
f.write(json.dumps(data, indent=4, sort_keys=True))
# Checkbox "Show Client Packets" checked
def cbxShowClient_checked(checked):
global cbxShowClient
cbxShowClient = checked
# Checkbox "Show Server Packets" checked
def cbxShowServer_checked(checked):
global cbxShowServer
cbxShowServer = checked
# return True if can log/show the packet
def CanShowPacket(opcode):
if opcode in lstOpcodesData:
if not cbxDontShow_checked:
return True
elif cbxDontShow_checked:
return True
return False
def inject(args):
argCount = len(args)
if argCount < 2:
log("Plugin: Incorrect structure to inject packet")
return 0
opcode = int(args[1], 16)
data = bytearray()
encrypted = False
dataIndex = 2
if argCount >= 3:
enc = args[2].lower()
if enc == 'true' or enc == 'false':
encrypted = enc == "true"
dataIndex += 1
for i in range(dataIndex, argCount):
data.append(int(args[i], 16))
log("Plugin: Injecting packet" + (' (Encrypted)' if encrypted else '') + " :")
log("(Opcode) 0x" + '{:02X}'.format(opcode) + " (Data) " + ("None" if not data else ' '.join(
'{:02X}'.format(x) for x in data)))
inject_joymax(opcode, data, encrypted)
return 0
def handle_training_area_command(player, text):
parts = text.split()
if len(parts) < 2:
log(f"Plugin: !C command from {player} missing area name or ID")
return False
area_name = " ".join(parts[1:])
success = set_training_area(area_name)
if success:
area_info = get_training_area() or {}
radius = area_info.get("radius", "unknown")
log(f"Plugin: {player} switched training area to '{area_name}' (radius {radius})")
log("Plugin: Starting bot after training area change...")
start_bot()
else:
log(f"Plugin: Failed to change training area to '{area_name}' requested by {player}")
return success
def GetItemByExpression(_lambda, start=0, end=0):
"""Search an item by name or servername through lambda expression and return its information"""
inventory = get_inventory()
items = inventory['items']
if end == 0:
end = inventory['size']
for slot, item in enumerate(items):
if start <= slot and slot <= end:
if item:
if _lambda(item['name'], item['servername']):
item['slot'] = slot
return item
return None
def GetJobItem(start, end=0):
"""Return the first real job item in the requested inventory/equipment range."""
inventory = get_inventory() or {}
items = inventory.get('items') or []
if end == 0:
end = len(items) - 1
for slot, item in enumerate(items):
if slot < start or slot > end or not item:
continue
item_data = get_item(item.get('model'))
if item_data and item_data.get('tid1') == 1 and item_data.get('tid2') == 7:
item['slot'] = slot
return item
return None
# Gets the NPC unique ID if the specified name is found near
def GetNPCUniqueID(name):
NPCs = get_npcs()
if NPCs:
name = name.lower()
for UniqueID, NPC in NPCs.items():
if NPC['name'].lower() == name:
return UniqueID
return 0
# Finds an empty inventory slot; iSRO reserves equipment slots 13-16 as well.
def GetEmptySlot():
inventory = get_inventory() or {}
items = inventory.get('items') or []
first_inventory_slot = 17 if get_locale() == 18 else 13
for slot in range(first_inventory_slot, len(items)):
if not items[slot]:
return slot
return -1
# Injects item movement on inventory (equip/unequip/avatar slots)
def Inject_InventoryMovement(movementType, slotInitial, slotFinal, logItemName, quantity=0):
p = struct.pack(' 0:
return players[p]
# FL is also allowed for non-party players when phBot exposes nearby players.
players = get_players() or {}
for player_data in players.values():
if player_data.get('name', '').lower() == player.lower():
return player_data
return None
# Start following a party player using distance. Return success
def start_follow(player, distance):
global followActivated, followPlayer, followDistance
followPlayer = player
followDistance = distance
followActivated = True
return True
# Stop follow player, return whether it was active
def stop_follow():
global followActivated, followPlayer, followDistance
result = followActivated
followActivated = False
followPlayer = ""
followDistance = 0
return result
def get_region_from_coords(x, y):
if x < 0 and y < 0:
return 'Jangan'
elif x >= 0 and y < 0:
return 'Donwhang'
elif x >= 0 and y >= 0:
return 'Hotan'
elif x < 0 and y >= 0:
return 'Samarkand'
return None
def find_nearby_monsters(max_distance=30):
monsters = get_monsters()
nearby = []
if monsters:
current_pos = get_position()
for uid, monster in monsters.items():
if monster['hp'] > 0:
distance = GetDistance(current_pos['x'], current_pos['y'], monster['x'], monster['y'])
if distance <= max_distance:
nearby.append({'uid': uid, 'distance': distance, 'monster': monster})
nearby.sort(key=lambda x: x['distance'])
return nearby
def attack_monster(uid):
log('Plugin: Attacking monster UID: ' + str(uid))
inject_joymax(0x7075, struct.pack(' TELEPORT_PROXIMITY_METERS:
log(f"Plugin: TP: Teleporter is too far away ({nearest_distance:.1f}m > {TELEPORT_PROXIMITY_METERS}m); command skipped.")
return
log(f"Plugin: TP: Selecting teleporter [{source_name}] (Leader: {player})")
inject_joymax(0x7045, struct.pack(' {source_name}",)).start()
def handle_runtime_tp_command(player, text):
"""'TPR ' finds the nearby runtime portal and replays its type-3 teleport flow."""
global _runtime_tp_command_until, _suppress_runtime_announce_until
source_name = text[4:].strip()
if not source_name:
log(f"Plugin: TPR: Portal name is required (Leader: {player})")
return
now = time.time()
if _runtime_tp_command_until > now:
log(f"Plugin: TPR: A runtime portal operation is already running (Leader: {player})")
return
npcs = get_npcs() or {}
current_pos = get_position()
nearest_uid = None
nearest_distance = None
for uid, npc in npcs.items():
if npc.get('servername') != source_name and npc.get('name') != source_name:
continue
if 'x' not in npc or 'y' not in npc:
continue
distance = GetDistance(current_pos['x'], current_pos['y'], npc['x'], npc['y'])
if nearest_distance is None or distance < nearest_distance:
nearest_uid = uid
nearest_distance = distance
if nearest_uid is None:
log(f"Plugin: TPR: Runtime portal '{source_name}' was not found nearby (Leader: {player})")
return
if nearest_distance > TELEPORT_PROXIMITY_METERS:
log(f"Plugin: TPR: Portal is too far away ({nearest_distance:.1f}m > {TELEPORT_PROXIMITY_METERS}m)")
return
# Suppress the outgoing type-3 packet generated below so followers do not
# announce TPR again and create a command loop.
_runtime_tp_command_until = now + 15.0
_suppress_runtime_announce_until = now + 10.0
uid_data = struct.pack('= 3 else 0
z = float(parts[3]) if len(parts) >= 4 else 0
set_training_position(region, x, y, z)
log(f"Plugin: SP: Training area set to (X:{x:.1f},Y:{y:.1f}) (Leader: {player})")
except (IndexError, ValueError):
log(f"Plugin: SP: Wrong training area coordinates! (Leader: {player})")
def handle_setradius_command(player, text):
"""Set the training radius, using 35 meters when no value is supplied."""
rest = text[2:].strip()
if not rest:
radius = 35
set_training_radius(radius)
log(f"Plugin: SR: Training radius reset to {radius} m. (Leader: {player})")
return
try:
radius = abs(int(float(rest.split()[0])))
set_training_radius(radius)
log(f"Plugin: SR: Training radius set to {radius} m. (Leader: {player})")
except (IndexError, ValueError):
log(f"Plugin: SR: Wrong training radius value! (Leader: {player})")
def handle_follow_command(player, text):
"""Follow a party member at the requested distance; movement runs in event_loop()."""
rest = text[2:].strip()
char_name = player
distance = 10
if rest:
parts = rest.split()
try:
if len(parts) >= 1:
char_name = parts[0]
if len(parts) >= 2:
distance = float(parts[1])
except ValueError:
log(f"Plugin: FL: Follow distance incorrect (Leader: {player})")
return
start_follow(char_name, distance)
log(f"Plugin: FL: Starting to follow [{char_name}] using [{distance}] as distance (Leader: {player})")
def handle_trace_command(player, text):
"""Trace a specified player; the bare T command traces the authorized leader."""
target = text[2:].strip()
if not target:
target = player
if start_trace(target):
log(f"Plugin: T: Starting trace to [{target}] (Leader: {player})")
def handle_equip_command(player, text):
"""Equip the first matching item found in inventory slots 13 and above."""
item_name = text[3:].strip()
if not item_name:
log(f"Plugin: EQ command requires an item name (Leader: {player}).")
return
if item_name.lower() == 'job':
first_inventory_slot = 17 if get_locale() == 18 else 13
item = GetJobItem(first_inventory_slot)
else:
item = GetItemByExpression(lambda n, s: item_name in n or item_name == s, 13)
if item:
EquipItem(item)
else:
log(f"Plugin: EQ: Item '{item_name}' was not found in the inventory (Leader: {player}).")
def handle_unequip_command(player, text):
"""Unequip the first matching item found in equipment slots 0 through 12."""
item_name = text[3:].strip()
if not item_name:
log(f"Plugin: UQ command requires an item name (Leader: {player}).")
return
if item_name.lower() == 'job':
item = GetJobItem(8, 8)
else:
item = GetItemByExpression(lambda n, s: item_name in n or item_name == s, 0, 12)
if item:
UnequipItem(item)
else:
log(f"Plugin: UQ: Equipped item '{item_name}' was not found (Leader: {player}).")
def handle_use_command(player, text):
"""Use the first matching item found in inventory slots 13 and above."""
item_name = text[4:].strip()
if not item_name:
log(f"Plugin: USE command requires an item name (Leader: {player}).")
return
item = GetItemByExpression(lambda n, s: item_name in n or item_name == s, 13)
if item:
UseItem(item)
else:
log(f"Plugin: USE: Item '{item_name}' was not found in the inventory (Leader: {player}).")
def handle_sort_command(player):
"""Request phBot's documented asynchronous inventory sort operation."""
try:
if sort_inventory():
log(f"Plugin: SORT: Inventory sorting requested (Leader: {player})")
else:
log(f"Plugin: SORT: Inventory sorting request was rejected (Leader: {player})")
except Exception as e:
log(f"Plugin: SORT: Inventory sorting failed (Leader: {player}) - {e}")
def handle_repair_command(player):
"""Use one Repair Hammer through the existing locale-aware item-use path."""
try:
inventory = get_inventory() or {}
except Exception as e:
log(f"Plugin: REPAIR: Inventory could not be read (Leader: {player}) - {e}")
return
items = inventory.get('items')
if not isinstance(items, list):
log(f"Plugin: REPAIR: Inventory is not ready (Leader: {player})")
return
repair_hammer = None
for slot, item in enumerate(items):
if slot < 13 or not item:
continue
display_name = (item.get('name') or '').strip().lower()
servername = (item.get('servername') or '').upper()
if display_name == 'repair hammer' or ('REPAIR' in servername and 'HAMMER' in servername):
repair_hammer = dict(item)
repair_hammer['slot'] = slot
break
if not repair_hammer:
log(f"Plugin: REPAIR: Repair Hammer was not found in the inventory (Leader: {player})")
return
if UseItem(repair_hammer):
log(f"Plugin: REPAIR: Repair Hammer use requested from slot {repair_hammer['slot']} (Leader: {player})")
else:
log(f"Plugin: REPAIR: Repair Hammer could not be used (Leader: {player})")
def _is_pick_pet_scroll(item):
"""Identify a pick-pet summon scroll without relying on its display name."""
servername = str(item.get('servername') or '').upper()
return ('COS_P_' in servername and 'SCROLL' in servername
and 'COS_P_EXTENSION' not in servername)
def _clock_priority(item):
"""Prefer the shortest explicitly named Clock duration, then the base Clock."""
servername = str(item.get('servername') or '').upper()
for days in (1, 3, 7, 15, 30):
if '_%dD' % days in servername:
return days
return 9999
def handle_clock_command(player):
"""Use exactly one Clock of Reincarnation on the active or sole pick-pet scroll."""
global clock_pending, clock_pending_slot, clock_pending_pet_slot
global clock_previous_remaining, clock_deadline
now = time.time()
if clock_pending and now <= clock_deadline:
log(f"Plugin: CLOCK: A Clock operation is already pending (Leader: {player})")
return
inventory = get_inventory() or {}
items = inventory.get('items') or []
clocks = []
pick_scrolls = []
for slot, item in enumerate(items):
if slot < 13 or not item:
continue
servername = str(item.get('servername') or '').upper()
entry = dict(item)
entry['slot'] = slot
if 'COS_P_EXTENSION' in servername:
clocks.append(entry)
elif _is_pick_pet_scroll(item):
pick_scrolls.append(entry)
if not clocks:
log(f"Plugin: CLOCK: No Clock of Reincarnation was found (Leader: {player})")
return
active_pick_pets = [
pet for pet in (get_pets() or {}).values()
if pet and pet.get('type') == 'pick'
]
target = None
previous_remaining = None
if active_pick_pets:
active_pet = active_pick_pets[0]
pet_servername = str(active_pet.get('servername') or '').upper()
matches = [item for item in pick_scrolls
if pet_servername and pet_servername in
str(item.get('servername') or '').upper()]
if len(matches) == 1:
target = matches[0]
previous_remaining = active_pet.get('remaining')
else:
log("Plugin: CLOCK: The active pick pet could not be matched uniquely "
f"to its summon scroll (matches: {len(matches)}); command cancelled")
return
elif len(pick_scrolls) == 1:
target = pick_scrolls[0]
previous_remaining = target.get('expiration')
else:
log("Plugin: CLOCK: No active pick pet and the inventory does not contain "
f"exactly one pick-pet scroll (found: {len(pick_scrolls)}); command cancelled")
return
# One command deliberately selects and injects one Clock only.
clock = sorted(clocks, key=lambda item: (_clock_priority(item), item['slot']))[0]
clock_slot = int(clock['slot'])
pet_slot = int(target['slot'])
if clock_slot > 255 or pet_slot > 255:
log("Plugin: CLOCK: Inventory slot is outside the one-byte packet range; command cancelled")
return
clock_data = get_item(clock.get('model'))
if not clock_data:
log("Plugin: CLOCK: Static Clock item data is unavailable; command cancelled")
return
locale = get_locale()
if locale == 18:
packet = struct.pack(
' 1:
log("Plugin: DEVILEXT: Multiple Devil/Nasrun items were found in inventory; "
"target is ambiguous and the command was cancelled")
return
gear = sorted(gears, key=lambda item: (_devilext_priority(item), item['slot']))[0]
devilext_gear_slot = int(gear['slot'])
devilext_deadline = time.time() + 10.0
if len(devils) == 1:
devilext_devil_slot = int(devils[0]['slot'])
devilext_was_equipped = False
log("Plugin: DEVILEXT: Using the sole inventory Devil/Nasrun; it will remain unequipped")
_use_devilext_gear()
return
# No Devil item is present in normal inventory. Ask the server to remove
# the equipped Devil; its assigned inventory slot comes from 0xB034.
devilext_was_equipped = True
devilext_state = 'unequipping'
inject_joymax(0x7034, bytes([0x23, 0x04, 0x23]), False)
log(f"Plugin: DEVILEXT: Removing the equipped Devil (Leader: {player})")
def handle_recall_command(player, text):
"""Set recall at the named nearby town portal NPC."""
town = text[3:].strip()
if not town:
log(f"Plugin: RC command requires a town name (Leader: {player}).")
return
npc_uid = GetNPCUniqueID(town)
if npc_uid > 0:
log(f"Plugin: RC: Designating recall to \"{town.title()}\"... (Leader: {player})")
inject_joymax(0x7059, struct.pack('I', npc_uid), False)
else:
log(f"Plugin: RC: NPC '{town}' was not found nearby (Leader: {player}).")
# ______________________________ xControl.py'den Port Edilen Ek Komutlar ______________________________ #
def handle_chat_send_command(player, text):
"""Send a message through an All, Private, Party, Guild, Union, Note, Stall, or Global channel."""
rest = text[4:].strip()
args = rest.split(' ', 1)
if len(args) != 2 or not args[0] or not args[1]:
log(f"Plugin: Invalid CHAT command format; command skipped (Leader: {player}).")
return
t = args[0].lower()
message = args[1]
if t in ('private', 'note'):
args_extra = message.split(' ', 1)
if len(args_extra) != 2 or not args_extra[0] or not args_extra[1]:
log(f"Plugin: CHAT {t} command requires a target and message (Leader: {player}).")
return
target, message = args_extra[0], args_extra[1]
sent = False
if t == 'all':
sent = phBotChat.All(message)
elif t == 'private':
sent = phBotChat.Private(target, message)
elif t == 'party':
sent = phBotChat.Party(message)
elif t == 'guild':
sent = phBotChat.Guild(message)
elif t == 'union':
sent = phBotChat.Union(message)
elif t == 'note':
sent = phBotChat.Note(target, message)
elif t == 'stall':
sent = phBotChat.Stall(message)
elif t == 'global':
sent = phBotChat.Global(message)
else:
log(f"Plugin: CHAT command has an unknown channel type: '{t}' (Leader: {player}).")
return
if sent:
log(f"Plugin: CHAT: Message sent ({t}) (Leader: {player})")
else:
log(f"Plugin: CHAT: Failed to send message ({t}) (Leader: {player})")
def handle_inject_command(player, text):
"""Run the existing packet injector from an authorized leader chat command."""
rest = text[7:].strip()
if not rest:
log(f"Plugin: Invalid INJECT command format; command skipped (Leader: {player}).")
return
args = ['INJECT'] + rest.split()
try:
inject(args)
except (ValueError, IndexError) as e:
log(f"Plugin: Invalid INJECT command: {e} (Leader: {player}).")
def handle_moveon_command(player, text):
"""Move to a random point within the supplied radius, defaulting to 10 meters."""
rest = text[6:].strip()
radius = 10
if rest:
try:
radius = abs(int(float(rest.split()[0])))
except (IndexError, ValueError):
log(f"Plugin: MOVEON radius value is invalid (Leader: {player}).")
return
p_x = random.uniform(-radius, radius)
p_y = random.uniform(-radius, radius)
p = get_position()
dest_x = p_x + p['x']
dest_y = p_y + p['y']
move_to(dest_x, dest_y, p['z'])
log(f"Plugin: MOVEON: Moving to a random position (X:{dest_x:.1f},Y:{dest_y:.1f}) (Leader: {player})")
def handle_setscript_command(player, text):
"""Set the training script path, or clear it when no path is supplied."""
rest = text[9:].strip()
if not rest:
set_training_script('')
log(f"Plugin: SETSCRIPT: Training script reset (Leader: {player})")
return
set_training_script(rest)
log(f"Plugin: SETSCRIPT: Training script set to '{rest}' (Leader: {player})")
def handle_fsh_command(player, text):
"""Run an FScriptHelper recording through phBot's walk-script command bridge."""
rest = text[3:].strip()
if not rest:
log("Plugin: FSH: Usage: FSH [true|false] command_name")
return
stop_during = True
parts = rest.split(None, 1)
first = parts[0].lower()
if first in ('true', 'false'):
if len(parts) < 2 or not parts[1].strip():
log("Plugin: FSH: Command name is required. Usage: FSH [true|false] command_name")
return
stop_during = first == 'true'
command_name = parts[1].strip()
else:
command_name = rest
if ',' in command_name or '\r' in command_name or '\n' in command_name:
log(f"Plugin: FSH: Invalid command name '{command_name}'; commas and line breaks are not allowed.")
return
stop_text = 'true' if stop_during else 'false'
script_line = 'FSH_NPC,%s,%s' % (command_name, stop_text)
log(f"Plugin: FSH requested by Leader [{player}]: {command_name} (stop_bot={stop_text})")
start_script(script_line)
def handle_profile_command(player, text):
"""Handle SETPROFILE (and legacy PROFILE) using phBot's set_profile API."""
if text.upper().startswith('SETPROFILE'):
rest = text[10:].strip()
else:
rest = text[7:].strip()
profile_name = rest if rest else 'Default'
if set_profile(profile_name):
log(f"Plugin: SETPROFILE: Switched to profile '{profile_name}' (Leader: {player})")
else:
log(f"Plugin: SETPROFILE: Failed to switch to profile '{profile_name}' (Leader: {player}).")
def start_pick_all(player):
global pick_all_active, pick_all_target_id, pick_all_last_request_at
if pick_all_active:
log(f"Plugin: PA: Pick All is already active (Leader: {player})")
return
drops = get_drops() or {}
if not drops:
log(f"Plugin: PA: No nearby drops match the phBot pick filter (Leader: {player})")
return
pick_all_active = True
pick_all_target_id = None
pick_all_last_request_at = 0.0
log(f"Plugin: PA: Pick All started for [{len(drops)}] nearby drop(s) (Leader: {player})")
def stop_pick_all(player=None, reason=None):
global pick_all_active, pick_all_target_id, pick_all_last_request_at
was_active = pick_all_active
pick_all_active = False
pick_all_target_id = None
pick_all_last_request_at = 0.0
if reason and was_active:
log("Plugin: PA: Pick All stopped - " + reason)
elif was_active:
log(f"Plugin: SPA: Pick All stopped (Leader: {player})")
elif not reason:
log(f"Plugin: SPA: Pick All was not active (Leader: {player})")
def process_pick_all():
global pick_all_target_id, pick_all_last_request_at
if not pick_all_active:
return
character = get_character_data()
if not character or character.get('hp', 0) <= 0:
stop_pick_all(reason="character is unavailable or dead")
return
drops = get_drops() or {}
eligible = []
position = get_position()
for drop_id, drop in drops.items():
if not isinstance(drop, dict) or drop.get('can_pick') is False:
continue
try:
distance = GetDistance(position['x'], position['y'], float(drop['x']), float(drop['y']))
eligible.append((distance, drop_id, drop))
except (KeyError, TypeError, ValueError):
continue
if not eligible:
stop_pick_all(reason="no nearby drops remain")
return
distance, drop_id, drop = min(eligible, key=lambda entry: entry[0])
if pick_all_target_id != drop_id:
pick_all_target_id = drop_id
pick_all_last_request_at = 0.0
log(f"Plugin: PA: Moving to [{drop.get('name', 'Unknown drop')}]")
if distance > PICK_ALL_RANGE_METERS:
move_to(float(drop['x']), float(drop['y']), float(drop.get('z') or 0.0))
return
now = time.time()
if now - pick_all_last_request_at < PICK_ALL_REQUEST_INTERVAL:
return
packet = b'\x01\x02\x01' + struct.pack(' claim-all flow."""
global tis_active, tis_claim_pending, tis_item_count, tis_deadline
if tis_active or tis_claim_pending:
log(f"Plugin: TIS: An Item Storage operation is already running (Leader: {player})")
return
tis_active = True
tis_claim_pending = False
tis_item_count = 0
tis_deadline = time.time() + 15.0
log(f"Plugin: TIS: Checking Item Storage (Leader: {player})")
inject_joymax(0x7557, struct.pack(' 1:
parts = text.split()
if len(parts) > 1:
pet_type = parts[1].lower()
if MountPet(pet_type):
log("Plugin: Mounted pet [" + pet_type + "]")
else:
log("Plugin: Could not mount pet [" + pet_type + "]")
return True
elif msg_upper == "D":
log("Plugin: Dismounting pet from Leader [" + str(player) + "]")
if DismountPet():
log("Plugin: Dismounted pet")
else:
log("Plugin: No mounted pet to dismount")
return True
elif msg_upper == "T":
log("Plugin: Start trace from Leader [" + str(player) + "]")
start_trace(player)
elif msg_upper == "SIT":
log("Plugin: Sit/Stand from Leader [" + str(player) + "]")
inject_joymax(0x704F, b'\x04', False)
elif msg_upper == "N":
log("Plugin: Stop trace from Leader [" + str(player) + "]")
stop_trace()
elif msg_upper == "S":
log("Plugin: Start bot from Leader [" + str(player) + "]")
start_bot()
elif msg_upper == "SS":
log("Plugin: Stop bot from Leader [" + str(player) + "]")
stop_bot()
elif msg_upper == "Q1":
_leader_teleport_sequence("Q1", Q1_ROUTES)
elif msg_upper == "Q2":
_leader_teleport_sequence("Q2", Q2_ROUTES)
elif msg_upper == "Q3":
_leader_teleport_sequence("Q3", Q3_ROUTES)
elif msg_upper == "RE":
log("Plugin: Return scroll from Leader [" + str(player) + "]")
character = get_character_data()
if character['hp'] == 0:
log('Plugin: Resurrecting at town...')
inject_joymax(0x3053, b'\x01', False)
else:
log('Plugin: Using return scroll...')
use_return_scroll()
elif msg_upper == "COME":
log("Plugin: COME command from Leader [" + str(player) + "] - using reverse return scroll")
if reverse_return(2, player):
log("Plugin: Reverse return scroll used, returning to Leader [" + str(player) + "]")
else:
log("Plugin: No reverse return scroll available for Leader [" + str(player) + "]")
elif msg_upper == "NF":
if stop_follow():
log(f"Plugin: NF: Following stopped (Leader: {player})")
else:
log(f"Plugin: NF: Following was not active (Leader: {player})")
elif msg_upper == "ZK":
log(f"Plugin: ZK: Using Berserker mode (Leader: {player})")
inject_joymax(0x70A7, b'\x01', False)
elif msg_upper == "DC":
log(f"Plugin: DC: Disconnecting... (Leader: {player})")
disconnect()
elif msg_upper == "LP":
if get_party():
log(f"Plugin: LP: Leaving the party... (Leader: {player})")
inject_joymax(0x7061, b'', False)
else:
log(f"Plugin: LP: Not in a party (Leader: {player})")
elif msg_upper == "TIS":
start_item_storage_claim(player)
elif msg_upper == "SORT":
handle_sort_command(player)
elif msg_upper == "REPAIR":
handle_repair_command(player)
elif msg_upper == "CLOCK":
handle_clock_command(player)
elif msg_upper == "DEVILEXT":
handle_devilext_command(player)
elif msg_upper == "PA":
start_pick_all(player)
elif msg_upper == "SPA":
stop_pick_all(player=player)
return True
if is_leader and msg_upper.startswith("TP "):
handle_tp_command(player, text)
return True
if is_leader and msg_upper.startswith("TPR "):
handle_runtime_tp_command(player, text)
return True
if is_leader and msg_upper.startswith("REVERSE "):
handle_reverse_command(player, text)
return True
if is_leader and msg_upper.startswith("SP"):
handle_setpos_command(player, text)
return True
if is_leader and msg_upper.startswith("SR"):
handle_setradius_command(player, text)
return True
if is_leader and msg_upper.startswith("FL"):
handle_follow_command(player, text)
return True
if is_leader and msg_upper.startswith("T "):
handle_trace_command(player, text)
return True
if is_leader and msg_upper.startswith("EQ "):
handle_equip_command(player, text)
return True
if is_leader and msg_upper.startswith("UQ "):
handle_unequip_command(player, text)
return True
if is_leader and msg_upper.startswith("USE "):
handle_use_command(player, text)
return True
if is_leader and msg_upper.startswith("RC "):
handle_recall_command(player, text)
return True
if is_leader and msg_upper.startswith("CHAT "):
handle_chat_send_command(player, text)
return True
if is_leader and msg_upper.startswith("INJECT "):
handle_inject_command(player, text)
return True
if is_leader and msg_upper.startswith("MOVEON"):
handle_moveon_command(player, text)
return True
if is_leader and msg_upper.startswith("SETSCRIPT"):
handle_setscript_command(player, text)
return True
if is_leader and (msg_upper == "FSH" or msg_upper.startswith("FSH ")):
handle_fsh_command(player, text)
return True
if is_leader and (msg_upper == "SETPROFILE" or msg_upper.startswith("SETPROFILE ")):
handle_profile_command(player, text)
return True
if is_leader and (msg_upper == "PROFILE" or msg_upper.startswith("PROFILE ")):
handle_profile_command(player, text)
return True
# Handle MOVEATTACK / GETPOS / MOVE commands (leaders only)
if is_leader or t == 100:
msg_stripped = text.rstrip()
if msg_stripped.startswith("MOVEATTACK"):
try:
parts = msg_stripped[10:].split()
if len(parts) >= 2:
x = float(parts[0])
y = float(parts[1])
z = float(parts[2]) if len(parts) >= 3 else 0
move_and_attack(x, y, z)
else:
log("Plugin: Usage: MOVEATTACK X Y [Z]")
except Exception as e:
log("Plugin: Invalid coordinates! Error: " + str(e))
return True
elif msg_stripped == "GETPOS":
pos = get_position()
phBotChat.Private(player, 'My position is (X:%.1f,Y:%.1f,Z:%.1f,Region:%d)' % (pos['x'], pos['y'], pos['z'], pos['region']))
return True
elif msg_stripped.startswith("MOVE"):
try:
parts = msg_stripped[4:].split()
if len(parts) >= 2:
x = float(parts[0])
y = float(parts[1])
z = float(parts[2]) if len(parts) >= 3 else 0
log('Plugin: Walking to (X:%.1f,Y:%.1f)' % (x, y))
current_pos = get_position()
script = generate_script(current_pos['region'], x, y, z)
if script:
start_script('\n'.join(script))
else:
log('Plugin: Could not generate path, using direct movement')
move_to(x, y, z)
else:
log("Plugin: Usage: MOVE X Y [Z]")
except Exception as e:
log("Plugin: Invalid coordinates! Error: " + str(e))
return True
return False
def handle_silkroad(opcode, data):
if cbxShowClient:
if CanShowPacket(opcode):
log("Client: (Opcode) 0x" + '{:02X}'.format(opcode) + " (Data) " + ("None" if not data else ' '.join(
'{:02X}'.format(x) for x in data)))
if opcode == 0x7045 and announce_own_teleports:
try:
_capture_selected_teleporter(data)
except Exception as e:
log(f"Plugin: TP source cache error: {e}")
if opcode == 0x705A and announce_own_teleports:
try:
_capture_own_teleport_request(data)
except Exception as e:
log(f"Plugin: TP announcement capture error: {e}")
return True
def _capture_selected_teleporter(data):
"""Cache the selected NPC before a runtime portal disappears during scene transition."""
global _last_selected_tp_uid, _last_selected_tp_source, _last_selected_tp_at
if len(data) < 4:
return
uid = struct.unpack_from(' Source: '{source_name}'. "
f"The announcement will be sent via {get_announce_channel()} after teleportation is confirmed.")
else:
log(f"Plugin: Teleport captured -> Source: '{source_name}' | Destination ID: {destination_id}. "
f"The announcement will be sent via {get_announce_channel()} after teleportation is confirmed.")
def handle_joymax(opcode, data):
global clock_pending, clock_pending_slot, clock_pending_pet_slot
global clock_previous_remaining, clock_deadline
global devilext_state, devilext_devil_slot, devilext_deadline
handle_item_storage_packet(opcode, data)
if opcode == 0xB04C and clock_pending and data:
if len(data) >= 2 and data[1] == clock_pending_slot:
if data[0] == 1:
log("Plugin: CLOCK: Server accepted the Clock use request; "
"pet duration update is expected")
else:
log("Plugin: CLOCK: Server rejected the Clock use request "
"(status: %d)" % data[0])
clock_pending = False
clock_pending_slot = None
clock_pending_pet_slot = None
clock_previous_remaining = None
clock_deadline = 0.0
if opcode == 0xB034 and devilext_state == 'unequipping' and data:
if len(data) >= 4 and data[1] == 0x23:
if data[0] == 1:
devilext_devil_slot = data[3]
log("Plugin: DEVILEXT: Devil moved to inventory slot %d" % devilext_devil_slot)
devilext_state = 'use_pending'
Timer(0.5, _use_devilext_gear).start()
else:
log("Plugin: DEVILEXT: Could not remove an equipped Devil (status: %d)" % data[0])
_reset_devilext_state()
elif opcode == 0xB04C and devilext_state == 'using' and data:
if len(data) >= 2 and data[1] == devilext_gear_slot:
if data[0] == 1:
log("Plugin: DEVILEXT: Server accepted the Extension Gear use request")
else:
log("Plugin: DEVILEXT: Server rejected the Extension Gear request "
"(status: %d)" % data[0])
if devilext_was_equipped:
devilext_state = 'restore_pending'
Timer(0.5, _reequip_devilext_devil).start()
else:
_reset_devilext_state()
elif opcode == 0xB034 and devilext_state == 'equipping' and data:
if len(data) >= 4 and data[1] == 0x24 and data[2] == devilext_devil_slot:
if data[0] == 1:
log("Plugin: DEVILEXT: Devil restored successfully; operation complete")
else:
log("Plugin: DEVILEXT: Extension finished, but Devil could not be restored "
"(status: %d, inventory slot: %d)" %
(data[0], devilext_devil_slot))
_reset_devilext_state()
if cbxShowServer:
if CanShowPacket(opcode):
log("Server: (Opcode) 0x" + '{:02X}'.format(opcode) + " (Data) " + ("None" if not data else ' '.join(
'{:02X}'.format(x) for x in data)))
return True
def format_bytes(value):
if not value:
return ""
return ' '.join('{:02X}'.format(x) for x in value)
def refresh_display_fields():
pass
def _process_queued_tp_announcement():
"""Wait for stable post-teleport character data before sending chat."""
global _queued_tp_announcement
pending = _queued_tp_announcement
if not pending:
return
now = time.time()
if pending.get('sent'):
if pending.get('confirmed'):
log("Plugin: TP announcement observed in Party chat.")
_queued_tp_announcement = None
elif now - pending.get('sent_at', now) >= 5.0:
# Own-message echoes are not guaranteed by every phBot build.
_queued_tp_announcement = None
return
if now > pending['deadline']:
log("Plugin: TP announcement canceled; the destination scene did not become ready in time.")
_queued_tp_announcement = None
return
character = get_character_data() or {}
position = get_position() or {}
region = position.get('region')
data_ready = bool(
character.get('name') and
region is not None and
position.get('x') is not None and
position.get('y') is not None
)
if not data_ready:
pending['saw_unready'] = True
pending['stable_region'] = None
pending['stable_checks'] = 0
pending['ready_at'] = None
return
origin_region = pending.get('origin_region')
transition_observed = (
pending['saw_unready'] or
origin_region is None or
region != origin_region or
now >= pending['fallback_at']
)
if not transition_observed:
return
if region == pending.get('stable_region'):
pending['stable_checks'] += 1
else:
pending['stable_region'] = region
pending['stable_checks'] = 1
pending['ready_at'] = None
if pending['stable_checks'] < TP_SCENE_STABLE_CHECKS:
return
if pending['ready_at'] is None:
pending['ready_at'] = now + TP_SCENE_SAFETY_DELAY
return
if now < pending['ready_at']:
return
if pending['is_runtime']:
sent = _send_runtime_tp_announcement(pending['source'])
else:
sent = _send_tp_announcement(pending['source'], pending['destination_id'])
if not sent:
pending['attempts'] += 1
if pending['attempts'] >= 3:
_queued_tp_announcement = None
else:
pending['ready_at'] = now + 2.0
return
pending['sent'] = True
pending['sent_at'] = now
# Called every 500ms
def event_loop():
global attackMode, targetX, targetY, targetZ, _last_seen_announce_channel
global tis_active, tis_claim_pending, tis_deadline
global clock_pending, clock_pending_slot, clock_pending_pet_slot
global clock_previous_remaining, clock_deadline
global devilext_state, devilext_deadline
global _runtime_tp_command_until, _suppress_runtime_announce_until
global _announce_settings_loaded_for
global _leaders_loaded_for, _queued_tp_announcement
_process_queued_tp_announcement()
process_pick_all()
if (tis_active or tis_claim_pending) and tis_deadline and time.time() > tis_deadline:
tis_active = False
tis_claim_pending = False
tis_deadline = 0.0
log("Plugin: TIS: Timed out waiting for the Item Storage server response")
if clock_pending and clock_deadline and time.time() > clock_deadline:
clock_pending = False
clock_pending_slot = None
clock_pending_pet_slot = None
clock_previous_remaining = None
clock_deadline = 0.0
log("Plugin: CLOCK: Timed out waiting for the server response")
if devilext_state and devilext_deadline and time.time() > devilext_deadline:
timed_out_state = devilext_state
if timed_out_state == 'using' and devilext_was_equipped and devilext_devil_slot is not None:
log("Plugin: DEVILEXT: Extension response timed out; attempting to restore Devil")
_reequip_devilext_devil()
else:
log("Plugin: DEVILEXT: Timed out during [%s]; operation stopped" % timed_out_state)
_reset_devilext_state()
now = time.time()
if _runtime_tp_command_until and now > _runtime_tp_command_until:
_runtime_tp_command_until = 0.0
if _suppress_runtime_announce_until and now > _suppress_runtime_announce_until:
_suppress_runtime_announce_until = 0.0
# Combobox için "değişti" event'i phBot'ta belgeli olmadığından, seçili kanalı burada
# (zaten var olan 500ms event_loop() hook'unda) yoklayıp değiştiyse config'e kaydediyoruz.
# isJoined() kontrolü, karaktere özel config dosyası (getConfig()) henüz belli değilken
# yanlışlıkla "default_leaders.json"a yazmayı önlüyor.
if isJoined():
leader_config = getConfig()
if leader_config and leader_config != _leaders_loaded_for:
loadLeadersConfigs()
announce_config = getAnnounceConfig()
if announce_config and announce_config != _announce_settings_loaded_for:
load_announce_settings()
current_announce_channel = get_announce_channel()
if current_announce_channel != _last_seen_announce_channel:
_last_seen_announce_channel = current_announce_channel
save_announce_settings()
# FL/NF komutlarıyla başlatılan takip hareketi - attackMode bloğundan önce çalıştırılıyor
# çünkü o blok içindeki erken "return" bu koda hiç ulaşılmamasına sebep olabilir.
if followActivated:
player_pos = near_follow_player(followPlayer)
if player_pos:
if followDistance > 0:
p = get_position()
playerDistance = round(GetDistance(p['x'], p['y'], player_pos['x'], player_pos['y']), 2)
if followDistance < playerDistance:
x_unit = (player_pos['x'] - p['x']) / playerDistance
y_unit = (player_pos['y'] - p['y']) / playerDistance
movementDistance = playerDistance - followDistance
log("Plugin: Following " + followPlayer + "...")
move_to(movementDistance * x_unit + p['x'], movementDistance * y_unit + p['y'], 0)
else:
log("Plugin: Following " + followPlayer + "...")
move_to(player_pos['x'], player_pos['y'], 0)
if attackMode:
character = get_character_data()
if character and character.get('hp', 0) > 0:
current_pos = get_position()
if targetX != 0 or targetY != 0:
distance = GetDistance(current_pos['x'], current_pos['y'], targetX, targetY)
if distance > 2:
return
log('Plugin: Reached target position, setting training area...')
set_training_position(current_pos['region'], targetX, targetY, targetZ)
log('Plugin: Training area set to (X:%.1f,Y:%.1f)' % (targetX, targetY))
log('Plugin: Starting bot...')
start_bot()
attackMode = False
targetX = 0
targetY = 0
targetZ = 0
# Called when the bot successfully connects to the game server
def connected():
global inGame, _announce_settings_loaded_for, _leaders_loaded_for
global _queued_tp_announcement
inGame = None
_announce_settings_loaded_for = None
_leaders_loaded_for = None
_queued_tp_announcement = None
stop_pick_all(reason="connection changed")
# Called when the character enters the game world
def joined_game():
global current_character_name
character_data = get_character_data()
if character_data and isinstance(character_data, dict) and "name" in character_data:
current_character_name = character_data.get("name", "Unknown")
update_account_info()
loadLeadersConfigs()
load_announce_settings()
loadOpcodeConfigs()
def teleported():
"""Send any pending teleport announcement after phBot reports a completed teleport."""
global _pending_tp_source, _pending_tp_destination_id, _pending_tp_armed_at
global _pending_tp_is_runtime, _pending_tp_origin_region
global _runtime_tp_command_until, _suppress_runtime_announce_until
global _queued_tp_announcement
source = _pending_tp_source
destination_id = _pending_tp_destination_id
armed_at = _pending_tp_armed_at
is_runtime = _pending_tp_is_runtime
origin_region = _pending_tp_origin_region
_pending_tp_source = None
_pending_tp_destination_id = None
_pending_tp_armed_at = None
_pending_tp_is_runtime = False
_pending_tp_origin_region = None
_runtime_tp_command_until = 0.0
_suppress_runtime_announce_until = 0.0
if source is None:
return
if armed_at is not None and (time.time() - armed_at) > TP_ANNOUNCE_TIMEOUT:
return
# Sahne geçişi tam oturmadan gönderilen chat paketleri sunucu tarafından sessizce
# yutulabiliyor (phBotChat True dönse bile mesaj partiye ulaşmıyor) - inject_teleport()
# akışındaki 2 saniyelik bekleme ile aynı sebeple burada da kısa bir gecikme kullanıyoruz.
# Wait for post-teleport character/position data to become stable. The
# fallback covers same-region teleports where no region change is visible.
if is_runtime or destination_id is not None:
now = time.time()
message = "TPR %s" % source if is_runtime else "TP %s %s" % (source, destination_id)
_queued_tp_announcement = {
'source': source,
'destination_id': destination_id,
'is_runtime': is_runtime,
'origin_region': origin_region,
'created_at': now,
'deadline': now + TP_ANNOUNCE_TIMEOUT,
'fallback_at': now + TP_SCENE_FALLBACK_DELAY,
'saw_unready': False,
'stable_region': None,
'stable_checks': 0,
'ready_at': None,
'message': message,
'attempts': 0,
'sent': False,
'sent_at': None,
'confirmed': False,
}
def _send_tp_announcement(source, destination_id):
channel = get_announce_channel()
message = f"TP {source} {destination_id}"
sent = ANNOUNCE_CHANNELS.get(channel, phBotChat.All)(message)
if sent:
log(f"Plugin: TP announcement sent ({channel}): {message}")
save_announce_settings()
else:
log("Plugin: Failed to send TP announcement (phBotChat reported a message send failure).")
return sent
def _send_runtime_tp_announcement(source):
channel = get_announce_channel()
message = f"TPR {source}"
sent = ANNOUNCE_CHANNELS.get(channel, phBotChat.All)(message)
if sent:
log(f"Plugin: TPR announcement sent ({channel}): {message}")
save_announce_settings()
else:
log("Plugin: Failed to send TPR announcement (phBotChat reported a message send failure).")
return sent
if os.path.exists(getPath()):
loadLeadersConfigs()
loadOpcodeConfigs()
else:
os.makedirs(getPath())
log('Plugin: ' + pName + ' folder has been created')
update_account_info()
refresh_display_fields()
# Plugin loaded
log('[%s] Loaded - ⚜ Made By FascinaTe' % pName)