from phBot import * from threading import Timer import phBotChat import QtBind import struct import random import json import os import sqlite3 pName = 'xControl' pVersion = '1.9.1' pUrl = 'https://raw.githubusercontent.com/JellyBitz/phBot-xPlugins/master/xControl.py' # ______________________________ Initializing ______________________________ # # Globals inGame = None followActivated = False followPlayer = '' followDistance = 0 # Graphic user interface gui = QtBind.init(__name__,pName) QtBind.createLabel(gui,'Control your party using in-game chat. Leader writes commands and your character will follow it.',11,11) QtBind.createLabel(gui,'< COMMAND (uppercased) #Variable (required) #Variable? (optional) >',11,30) QtBind.createLabel(gui,'- START : Start bot\n- STOP : Stop bot\n- TRACE #Player? : Start trace to leader or another player\n- NOTRACE : Stop trace\n- RETURN : Use some "Return Scroll" from your inventory\n- TP #A #B : Use teleport from location A to B\n- RECALL #Town : Set recall on city portal\n- ZERK : Use berserker mode if is available\n- GETOUT : Left party\n- MOVEON #Radius? : Set a random movement\n- MOUNT #PetType? : Mount horse by default\n- DISMOUNT #PetType? : Dismount horse by default\n- SETPOS #PosX? #PosY? #Region? #PosZ? : Set training position\n- SETRADIUS #Radius? : Set training radius\n- SETSCRIPT #Path? : Change script path for training area\n- SETAREA #Name : Changes training area by config name\n- PROFILE #Name? : Loads a profile by his name\n- DC : Disconnect from game',15,45) QtBind.createLabel(gui,'- INJECT #Opcode #Encrypted? #Data? : Inject packet\n- CHAT #Type #Message : Send any message type\n- FOLLOW #Player? #Distance? : Trace a party player using distance\n- NOFOLLOW : Stop following\n- JUMP : Generate knockback visual effect\n- SIT : Sit or Stand up, depends\n- CAPE #Type? : Use PVP Cape\n- EQUIP #ItemName : Equips an item from inventory\n- UNEQUIP #ItemName : Unequips item from character\n- REVERSE #Type #Name?\n- GETPOS : Gets current position\n- USE #ItemName : Use item from inventory',345,80) tbxLeaders = QtBind.createLineEdit(gui,"",525,11,110,20) lstLeaders = QtBind.createList(gui,525,32,110,38) btnAddLeader = QtBind.createButton(gui,'btnAddLeader_clicked'," Add ",635,10) btnRemLeader = QtBind.createButton(gui,'btnRemLeader_clicked'," Remove ",635,32) # ______________________________ Methods ______________________________ # # Return xControl folder path def getPath(): return get_config_dir()+pName+"\\" # Return character configs path (JSON) def getConfig(): return getPath()+inGame['server'] + "_" + inGame['name'] + ".json" # Check if character is ingame def isJoined(): global inGame inGame = get_character_data() if not (inGame and "name" in inGame and inGame["name"]): inGame = None return inGame # Load default configs def loadDefaultConfig(): # Clear data QtBind.clear(gui,lstLeaders) # Loads all config previously saved def loadConfigs(): loadDefaultConfig() if isJoined(): # Check config exists to load if os.path.exists(getConfig()): data = {} with open(getConfig(),"r") as f: data = json.load(f) if "Leaders" in data: for nickname in data["Leaders"]: QtBind.append(gui,lstLeaders,nickname) # Add leader to the list def btnAddLeader_clicked(): if inGame: player = QtBind.text(gui,tbxLeaders) # Player nickname it's not empty if player and not lstLeaders_exist(player): # Init dictionary data = {} # Load config if exist if os.path.exists(getConfig()): with open(getConfig(), 'r') as f: data = json.load(f) # Add new leader if not "Leaders" in data: data['Leaders'] = [] data['Leaders'].append(player) # Replace configs with open(getConfig(),"w") as f: f.write(json.dumps(data, indent=4, sort_keys=True)) QtBind.append(gui,lstLeaders,player) QtBind.setText(gui, tbxLeaders,"") log('Plugin: Leader added ['+player+']') # Remove leader selected from list def btnRemLeader_clicked(): if inGame: selectedItem = QtBind.text(gui,lstLeaders) if selectedItem: if os.path.exists(getConfig()): data = {"Leaders":[]} with open(getConfig(), 'r') as f: data = json.load(f) try: # remove leader nickname from file if exists data["Leaders"].remove(selectedItem) with open(getConfig(),"w") as f: f.write(json.dumps(data, indent=4, sort_keys=True)) except: pass # just ignore file if doesn't exist QtBind.remove(gui,lstLeaders,selectedItem) log('Plugin: Leader removed ['+selectedItem+']') # Return True if nickname exist at the leader list def lstLeaders_exist(nickname): nickname = nickname.lower() players = QtBind.getItems(gui,lstLeaders) for i in range(len(players)): if players[i].lower() == nickname: return True return False # Inject teleport packet, using the source and destination name def inject_teleport(source,destination): t = get_teleport_data(source, destination) if t: npcs = get_npcs() for key, npc in npcs.items(): if npc['name'] == source or npc['servername'] == source: log("Plugin: Selecting teleporter ["+source+"]") # Teleport found, select it inject_joymax(0x7045, struct.pack(' 0: return players[p] return None # Calc the distance from point A to B def GetDistance(ax,ay,bx,by): return ((bx-ax)**2 + (by-ay)**2)**0.5 # Stop follow player def stop_follow(): global followActivated,followPlayer,followDistance result = followActivated # stop followActivated = False followPlayer = "" followDistance = 0 return result # Try to summon a vehicle def MountHorse(): # search item with similar name or exact server name item = GetItemByExpression(lambda n,s: s.startswith('ITEM_COS_C_'),13) if item: UseItem(item) return True log('Plugin: Horse not found at your inventory') return False # Try to mount pet by type, return success def MountPet(petType): # just in case if petType == 'pick': return False elif petType == 'horse': return MountHorse() # get all summoned pets pets = get_pets() if pets: for uid,pet in pets.items(): if pet['type'] == petType: p = b'\x01' # mount flag p += struct.pack('I',uid) inject_joymax(0x70CB,p, False) return True return False # Try to dismount pet by type, return success def DismountPet(petType): petType = petType.lower() # just in case if petType == 'pick': return False # get all summoned pets pets = get_pets() if pets: for uid,pet in pets.items(): if pet['type'] == petType: p = b'\x00' p += struct.pack('I',uid) inject_joymax(0x70CB,p, False) return True return False # 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(): NPCName = NPC['name'].lower() if name == NPCName: return UniqueID return 0 # Search an item by name or servername through lambda expression and return his information def GetItemByExpression(_lambda,start=0,end=0): inventory = get_inventory() items = inventory['items'] if end == 0: end = inventory['size'] # check items between intervals for slot, item in enumerate(items): if start <= slot and slot <= end: if item: # Search by lambda if _lambda(item['name'],item['servername']): # Save slot location item['slot'] = slot return item return None # Finds an empty slot, returns -1 if inventory is full def GetEmptySlot(): items = get_inventory()['items'] # check the first empty for slot, item in enumerate(items): if slot >= 13: if not item: return slot return -1 # Injects item movement on inventory def Inject_InventoryMovement(movementType,slotInitial,slotFinal,logItemName,quantity=0): p = struct.pack('= 3 else 0 z = float(p[3]) if len(p) >= 4 else 0 set_training_position(region,x,y,z) log("Plugin: Training area set to (X:%.1f,Y:%.1f)"%(x,y)) except: log("Plugin: Wrong training area coordinates!") elif msg == 'GETPOS': # Check current position 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'])) elif msg.startswith("SETRADIUS"): # deletes empty spaces on right msg = msg.rstrip() if msg == "SETRADIUS": # set default radius radius = 35 set_training_radius(radius) log("Plugin: Training radius reseted to "+str(radius)+" m.") else: try: # split and parse movement radius radius = int(float(msg[9:].split()[0])) # to absolute radius = (radius if radius > 0 else radius*-1) set_training_radius(radius) log("Plugin: Training radius set to "+str(radius)+" m.") except: log("Plugin: Wrong training radius value!") elif msg.startswith('SETSCRIPT'): # deletes empty spaces on right msg = msg.rstrip() if msg == 'SETSCRIPT': # reset script set_training_script('') log('Plugin: Training script path has been reseted') else: # change script to the path specified set_training_script(msg[9:]) log('Plugin: Training script path has been changed') elif msg.startswith('SETAREA '): # deletes empty spaces on right msg = msg[8:] if msg: # try to change to specified area name if set_training_area(msg): log('Plugin: Training area has been changed to ['+msg+']') else: log('Plugin: Training area ['+msg+'] not found in the list') elif msg == "SIT": log("Plugin: Sit/Stand") inject_joymax(0x704F,b'\x04',False) elif msg == "JUMP": # Just a funny emote lol log("Plugin: Jumping!") inject_joymax(0x3091,b'\x0c',False) elif msg.startswith("CAPE"): # deletes empty spaces on right msg = msg.rstrip() if msg == "CAPE": log("Plugin: Using PVP Cape by default (Yellow)") inject_joymax(0x7516,b'\x05',False) else: # get cape type normalized cape = msg[4:].split()[0].lower() if cape == "off": log("Plugin: Removing PVP Cape") inject_joymax(0x7516,b'\x00',False) elif cape == "red": log("Plugin: Using PVP Cape (Red)") inject_joymax(0x7516,b'\x01',False) elif cape == "gray": log("Plugin: Using PVP Cape (Gray)") inject_joymax(0x7516,b'\x02',False) elif cape == "blue": log("Plugin: Using PVP Cape (Blue)") inject_joymax(0x7516,b'\x03',False) elif cape == "white": log("Plugin: Using PVP Cape (White)") inject_joymax(0x7516,b'\x04',False) elif cape == "yellow": log("Plugin: Using PVP Cape (Yellow)") inject_joymax(0x7516,b'\x05',False) else: log("Plugin: Wrong PVP Cape color!") elif msg == "ZERK": log("Plugin: Using Berserker mode") inject_joymax(0x70A7,b'\x01',False) elif msg == "RETURN": # Quickly check if is dead character = get_character_data() if character['hp'] == 0: # RIP log('Plugin: Resurrecting at town...') inject_joymax(0x3053,b'\x01',False) else: log('Plugin: Trying to use return scroll...') # Avoid high CPU usage with too many chars at the same time Timer(random.uniform(0.5,2),use_return_scroll).start() elif msg.startswith("TP"): # deletes command header and whatever used as separator msg = msg[3:] if not msg: return # select split char split = ',' if ',' in msg else ' ' # extract arguments source_dest = msg.split(split) # needs to be at least two name points to try teleporting if len(source_dest) >= 2: inject_teleport(source_dest[0].strip(),source_dest[1].strip()) elif msg.startswith("INJECT "): msgPacket = msg[7:].split() msgPacketLen = len(msgPacket) if msgPacketLen == 0: log("Plugin: Incorrect structure to inject packet") return # Check packet structure opcode = int(msgPacket[0],16) data = bytearray() encrypted = False dataIndex = 1 if msgPacketLen >= 2: enc = msgPacket[1].lower() if enc == 'true' or enc == 'false': encrypted = enc == "true" dataIndex +=1 # Create packet data and inject it for i in range(dataIndex, msgPacketLen): data.append(int(msgPacket[i],16)) inject_joymax(opcode,data,encrypted) # Log the info log("Plugin: Injecting packet...\nOpcode: 0x"+'{:02X}'.format(opcode)+" - Encrypted: "+("Yes" if encrypted else "No")+"\nData: "+(' '.join('{:02X}'.format(int(msgPacket[x],16)) for x in range(dataIndex, msgPacketLen)) if len(data) else 'None')) elif msg.startswith("CHAT "): handleChatCommand(msg[5:]) elif msg.startswith("MOVEON"): if msg == "MOVEON": randomMovement() else: try: # split and parse movement radius radius = int(float(msg[6:].split()[0])) # to positive radius = (radius if radius > 0 else radius*-1) randomMovement(radius) except: log("Plugin: Movement maximum radius incorrect") elif msg.startswith("FOLLOW"): # default values charName = player distance = 10 if msg != "FOLLOW": # Check params msg = msg[6:].split() try: if len(msg) >= 1: charName = msg[0] if len(msg) >= 2: distance = float(msg[1]) except: log("Plugin: Follow distance incorrect") return # Start following if start_follow(charName,distance): log("Plugin: Starting to follow to ["+charName+"] using ["+str(distance)+"] as distance") elif msg == "NOFOLLOW": if stop_follow(): log("Plugin: Following stopped") elif msg.startswith("PROFILE"): if msg == "PROFILE": if set_profile('Default'): log("Plugin: Setting Default profile") else: msg = msg[7:] if set_profile(msg): log("Plugin: Setting "+msg+" profile") elif msg == "DC": log("Plugin: Disconnecting...") disconnect() elif msg.startswith("MOUNT"): # default value pet = "horse" if msg != "MOUNT": msg = msg[5:].split() if msg: pet = msg[0] # Try mount pet if MountPet(pet): log("Plugin: Mounting pet ["+pet+"]") elif msg.startswith("DISMOUNT"): # default value pet = "horse" if msg != "DISMOUNT": msg = msg[8:].split() if msg: pet = msg[0] # Try dismount pet if DismountPet(pet): log("Plugin: Dismounting pet ["+pet+"]") elif msg == "GETOUT": # Check if has party if get_party(): # Left it log("Plugin: Leaving the party..") inject_joymax(0x7061,b'',False) elif msg.startswith("RECALL "): msg = msg[7:] if msg: npcUID = GetNPCUniqueID(msg) if npcUID > 0: log("Plugin: Designating recall to \""+msg.title()+"\"...") inject_joymax(0x7059, struct.pack('I',npcUID), False) elif msg.startswith("EQUIP "): msg = msg[6:] if msg: # search item with similar name or exact server name item = GetItemByExpression(lambda n,s: msg in n or msg == s,13) if item: EquipItem(item) elif msg.startswith("UNEQUIP "): msg = msg[8:] if msg: # search item with similar name or exact server name item = GetItemByExpression(lambda n,s: msg in n or msg == s,0,12) if item: UnequipItem(item) elif msg.startswith("REVERSE "): # remove command msg = msg[8:] if msg: # check params msg = msg.split(' ',1) # param type if msg[0] == 'return': # try to use it if reverse_return(0,''): log('Plugin: Using reverse to the last return scroll location') elif msg[0] == 'death': # try to use it if reverse_return(1,''): log('Plugin: Using reverse to the last death location') elif msg[0] == 'player': # Check existing name if len(msg) >= 2: # try to use it if reverse_return(2,msg[1]): log('Plugin: Using reverse to player "'+msg[1]+'" location') elif msg[0] == 'zone': # Check existing zone if len(msg) >= 2: # try to use it if reverse_return(3,msg[1]): log('Plugin: Using reverse to zone "'+msg[1]+'" location') elif msg.startswith("USE "): # remove command msg = msg[4:] if msg: # search item with similar name or exact server name item = GetItemByExpression(lambda n,s: msg in n or msg == s,13) if item: UseItem(item) # Called every 500ms def event_loop(): if inGame and followActivated: player = near_party_player(followPlayer) # check if is near if not player: return # check distance to the player if followDistance > 0: p = get_position() playerDistance = round(GetDistance(p['x'],p['y'],player['x'],player['y']),2) # check if has to move if followDistance < playerDistance: # generate vector unit x_unit = (player['x'] - p['x']) / playerDistance y_unit = (player['y'] - p['y']) / playerDistance # distance to move movementDistance = playerDistance-followDistance log("Following "+followPlayer+"...") move_to(movementDistance * x_unit + p['x'],movementDistance * y_unit + p['y'],0) else: # Avoid negative numbers log("Following "+followPlayer+"...") move_to(player['x'],player['y'],0) # Plugin loaded log("Plugin: "+pName+" v"+pVersion+" successfully loaded") if os.path.exists(getPath()): # Adding RELOAD plugin support loadConfigs() else: # Creating configs folder os.makedirs(getPath()) log('Plugin: '+pName+' folder has been created')