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.9'
pUrl = ''

# ______________________________ 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('<I', key), False)
				# Start a timer to teleport in 2.0 seconds
				Timer(2.0, inject_joymax, (0x705A,struct.pack('<IBI', key, 2, t[1]),False)).start()
				Timer(2.0, log, ("Plugin: Teleporting to ["+destination+"]")).start()
				return
		log('Plugin: NPC not found. Wrong NPC name or servername')
	else:
		log('Plugin: Teleport data not found. Wrong teleport name or servername')

# Send message, Ex. "All Hello World!" or "private JellyBitz Hi!"
def handleChatCommand(msg):
	# Try to split message
	args = msg.split(' ',1)
	# Check if the format is correct and is not empty
	if len(args) != 2 or not args[0] or not args[1]:
		return
	# Split correctly the message
	t = args[0].lower()
	if t == 'private' or t == 'note':
		# then check message is not empty
		argsExtra = args[1].split(' ',1)
		if len(argsExtra) != 2 or not argsExtra[0] or not argsExtra[1]:
			return
		args.pop(1)
		args += argsExtra
	# Check message type
	sent = False
	if t == "all":
		sent = phBotChat.All(args[1])
	elif t == "private":
		sent = phBotChat.Private(args[1],args[2])
	elif t == "party":
		sent = phBotChat.Party(args[1])
	elif t == "guild":
		sent = phBotChat.Guild(args[1])
	elif t == "union":
		sent = phBotChat.Union(args[1])
	elif t == "note":
		sent = phBotChat.Note(args[1],args[2])
	elif t == "stall":
		sent = phBotChat.Stall(args[1])
	elif t == "global":
		sent = phBotChat.Global(args[1])
	if sent:
		log('Plugin: Message "'+t+'" sent successfully!')

# Move to a random position from the actual position using a maximum radius
def randomMovement(radiusMax=10):
	# Generating a random new point
	pX = random.uniform(-radiusMax,radiusMax)
	pY = random.uniform(-radiusMax,radiusMax)
	# Mixing with the actual position
	p = get_position()
	pX = pX + p["x"]
	pY = pY + p["y"]
	# Moving to new position
	move_to(pX,pY,p["z"])
	log("Plugin: Random movement to (X:%.1f,Y:%.1f)"%(pX,pY))

# Follow a player using distance. Return success
def start_follow(player,distance):
	if party_player(player):
		global followActivated,followPlayer,followDistance
		followPlayer = player
		followDistance = distance
		followActivated = True
		return True
	return False

# Return True if the player is in the party
def party_player(player):
	players = get_party()
	if players:
		for p in players:
			if players[p]['name'] == player:
				return True
	return False

# Find a party member name (case insensitive). Returns the real name from party or None
def find_party_player_name(player):
	players = get_party()
	if players:
		player = player.lower()
		for p in players:
			if players[p]['name'].lower() == player:
				return players[p]['name']
	return None

# Teleport info is saved here, separately for each game server:
# { "ServerName": { "mode": "custom" | "standard", "items": [servernames] } }
def getTeleportItemFile():
	return getPath()+"teleport_items.json"

# Current game server name ('' if not in game)
def getServerName():
	data = get_character_data()
	if data and data.get('server'):
		return data['server']
	return ''

def loadAllTeleportData():
	try:
		with open(getTeleportItemFile(),"r") as f:
			data = json.load(f)
		if isinstance(data, dict):
			return data
	except:
		pass
	return {}

# Returns teleport info for the current server
def loadServerTeleport():
	info = loadAllTeleportData().get(getServerName())
	# old format: only a list of items (learned from custom packet)
	if isinstance(info, list):
		return {"mode":"custom","items":info}
	if isinstance(info, dict):
		return {"mode":info.get("mode",""),"items":info.get("items",[])}
	return {"mode":"","items":[]}

def saveServerTeleport(info):
	server = getServerName()
	if not server:
		return
	data = loadAllTeleportData()
	data[server] = info
	try:
		with open(getTeleportItemFile(),"w") as f:
			f.write(json.dumps(data, indent=4, sort_keys=True))
	except:
		pass

# Saves which packet type works on this server
def setServerTeleportMode(mode, itemServername=None):
	info = loadServerTeleport()
	changed = info["mode"] != mode
	info["mode"] = mode
	if itemServername and itemServername not in info["items"]:
		info["items"].append(itemServername)
		changed = True
	if changed:
		saveServerTeleport(info)
		# Only the custom method is announced, the standard one is saved silently
		if mode == 'custom':
			log('Plugin: Bu sunucu icin ozel isinlanma paketi kaydedildi [sunucu: '+getServerName()+']')

# Finds a reverse scroll in inventory: learned items first, then any item with "REVERSE" in its servername
def findReverseItem():
	known = loadServerTeleport()["items"]
	item = None
	if known:
		item = GetItemByExpression(lambda n,s: s in known,13)
	if not item:
		item = GetItemByExpression(lambda n,s: 'REVERSE' in s.upper(),13)
	return item

# Sends the custom teleport packet: 0xA459 [uint16 name length][name][uint8 inventory slot of the reverse scroll]
def SendCustomTeleport(name, item):
	try:
		nameBytes = name.encode('cp1254')
	except:
		nameBytes = name.encode('ascii','ignore')
	p = struct.pack('<H',len(nameBytes))
	p += nameBytes
	p += struct.pack('<B',item['slot'])
	inject_joymax(0xA459,p,True)

# Auto detection state (used only while the server's method is unknown)
teleportProbe = None # {'step': 'standard'|'custom', 'name': ..., 'item': servername}
teleportStarted = False

# Teleport to player. Uses the method saved for this server.
# If unknown, tries standard reverse (0x704C) first, then custom packet (0xA459), and saves the one that works.
def TeleportToPlayer(name):
	global teleportProbe, teleportStarted
	mode = loadServerTeleport()["mode"]
	if mode == 'standard':
		if reverse_return(2,name):
			log('Plugin: Isinlaniliyor -> "'+name+'"')
		else:
			log('Plugin: Reverse kullanilamadi (scroll yok olabilir)')
		return
	if mode == 'custom':
		item = findReverseItem()
		if not item:
			log('Plugin: Envanterde reverse scroll bulunamadi!')
			return
		SendCustomTeleport(name, item)
		log('Plugin: Isinlaniliyor -> "'+name+'" ('+item['name']+')')
		return
	# Unknown server: auto detect
	if teleportProbe:
		log('Plugin: Onceki isinlanma hala deneniyor, bekle...')
		return
	teleportStarted = False
	if reverse_return(2,name):
		teleportProbe = {'step':'standard','name':name}
		log('Plugin: Isinlaniliyor -> "'+name+'"')
		Timer(3.0, checkTeleportProbe).start()
	else:
		# phBot could not use it, go directly to the custom packet
		tryCustomTeleportProbe(name)

def tryCustomTeleportProbe(name):
	global teleportProbe, teleportStarted
	item = findReverseItem()
	if not item:
		teleportProbe = None
		log('Plugin: Envanterde reverse scroll bulunamadi!')
		return
	teleportStarted = False
	teleportProbe = {'step':'custom','name':name,'item':item['servername']}
	SendCustomTeleport(name, item)
	log('Plugin: Standart reverse calismadi, ozel paket deneniyor -> "'+name+'" ('+item['name']+')')
	Timer(3.0, checkTeleportProbe).start()

# Called 3 seconds after a test teleport
def checkTeleportProbe():
	global teleportProbe
	probe = teleportProbe
	if not probe:
		return
	if teleportStarted:
		teleportProbe = None
		if probe['step'] == 'standard':
			setServerTeleportMode('standard')
		else:
			setServerTeleportMode('custom', probe.get('item'))
		return
	if probe['step'] == 'standard':
		tryCustomTeleportProbe(probe['name'])
	else:
		teleportProbe = None
		log('Plugin: Isinlanma gerceklesmedi. Oyuncu adi dogru mu, oyunda mi?')

# Return point [X,Y] if player is in the party and near, otherwise return None
def near_party_player(player):
	players = get_party()
	if players:
		for p in players:
			if players[p]['name'] == player and players[p]['player_id'] > 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('<B',movementType)
	p += struct.pack('<B',slotInitial)
	p += struct.pack('<B',slotFinal)
	p += struct.pack('<H',quantity)
	log('Plugin: Moving item "'+logItemName+'"...')
	# CLIENT_INVENTORY_ITEM_MOVEMENT
	inject_joymax(0x7034,p,False)

# Try to equip item
def EquipItem(item):
	itemData = get_item(item['model'])
	# Check equipables only
	if itemData['tid1'] != 1:
		log('Plugin: '+item['name']+' cannot be equiped!')
		return
	# Check equipable type
	t = itemData['tid2']
	# garment, protector, armor, robe, light, heavy
	if t == 1 or t == 2 or t == 3 or t == 9 or t == 10 or t == 11:
		t = itemData['tid3']
		# head
		if t == 1:
			Inject_InventoryMovement(0,item['slot'],0,item['name'])
		# shoulders
		elif t == 2:
			Inject_InventoryMovement(0,item['slot'],2,item['name'])
		# chest
		elif t == 3:
			Inject_InventoryMovement(0,item['slot'],1,item['name'])
		# pants
		elif t == 4:
			Inject_InventoryMovement(0,item['slot'],4,item['name'])
		# gloves
		elif t == 5:
			Inject_InventoryMovement(0,item['slot'],3,item['name'])
		# boots
		elif t == 6:
			Inject_InventoryMovement(0,item['slot'],5,item['name'])
	# shields
	elif t == 4:
		Inject_InventoryMovement(0,item['slot'],7,item['name'])
	# accesories ch/eu
	elif t == 5 or t == 12:
		t = itemData['tid3']
		# earring
		if t == 1:
			Inject_InventoryMovement(0,item['slot'],9,item['name'])
		# necklace
		elif t == 2:
			Inject_InventoryMovement(0,item['slot'],10,item['name'])
		# ring
		elif t == 3:
			# Check if second ring slot is empty
			if not GetItemByExpression(lambda s,n: True,11):
				Inject_InventoryMovement(0,item['slot'],12,item['name'])
			else:
				Inject_InventoryMovement(0,item['slot'],11,item['name'])
	# weapon ch/eu
	elif t == 6:
		Inject_InventoryMovement(0,item['slot'],6,item['name'])
	# job
	elif t == 7:
		Inject_InventoryMovement(0,item['slot'],8,item['name'])
	# avatar
	elif t == 13:
		t = itemData['tid3']
		# hat
		if t == 1:
			Inject_InventoryMovement(36,item['slot'],0,item['name'])
		# dress
		elif t == 2:
			Inject_InventoryMovement(36,item['slot'],1,item['name'])
		# accesory
		elif t == 3:
			Inject_InventoryMovement(36,item['slot'],2,item['name'])
		# flag
		elif t == 4:
			Inject_InventoryMovement(36,item['slot'],3,item['name'])
	# devil spirit
	elif t == 14:
		Inject_InventoryMovement(36,item['slot'],4,item['name'])

# Try to unequip item
def UnequipItem(item):
	# find an empty slot
	slot = GetEmptySlot()
	if slot != -1:
		Inject_InventoryMovement(0,item['slot'],slot,item['name'])

# Try to use the item specified
def UseItem(item):
	# Create packet and inject it
	p = struct.pack('<B',item['slot'])
	loc = get_locale()

	tid = GetTIDFromItem(item['model'])
	if loc == 22: # vsro
		p += struct.pack('<H',tid)
	else:
		p += struct.pack('<I',tid)

	log('Plugin: Using item "'+item['name']+'"...')
	# CLIENT_INVENTORY_ITEM_USE
	inject_joymax(0x704C,p,True)

# Get Type ID from item
def GetTIDFromItem(itemId):
	conn = GetDatabaseConnection()
	c = conn.cursor()
	c.execute('SELECT cash_item, tid1, tid2, tid3 FROM items WHERE id=?',(itemId,))
	result = c.fetchone()
	# calculate TID
	result = result[0] + (3 * 4) + (result[1] * 32) + (result[2] * 128) + (result[3] * 2048)
	conn.close()
	return result

# Create a connection to database
def GetDatabaseConnection():
	bot_path = os.getcwd()
	# Load the server info
	data = {}
	locale = get_locale()
	# vSRO
	if locale == 22:
		with open(bot_path+"/vSRO.json","r") as f:
			data = json.load(f)
		# Match data with the current server name
		server = get_character_data()['server']
		for k in data:
			servers = data[k]['servers']
			# Check if servers is in list
			if server in servers:
				# Scan data folder
				for path in os.scandir(bot_path+"/Data"):
					# Check databases only
					if path.is_file() and path.name.endswith(".db3"):
						# Connect to check if the data matches
						conn = sqlite3.connect(bot_path+"/Data/"+path.name)
						c = conn.cursor()
						c.execute('SELECT * FROM data WHERE k="path" AND v=?',(data[k]['path'],))
						if c.fetchone():
							# match found
							return conn
						else:
							conn.close()
	# iSRO
	elif locale == 18:
		return sqlite3.connect(bot_path+"/Data/iSRO.db3")
	# TrSRO
	elif locale == 56:
		return sqlite3.connect(bot_path+"/Data/TRSRO.db3")
	return None

# ______________________________ Events ______________________________ #

# Called when the bot successfully connects to the game server
def connected():
	global inGame
	inGame = None

# Called when the character enters the game world
def joined_game():
	loadConfigs()

# All packets sent from client to server are sent to this function
def handle_silkroad(opcode, data):
	# Manual teleport to player with the custom packet: this server uses 0xA459.
	# Last byte is the inventory slot of the reverse scroll, remember which item it is.
	if opcode == 0xA459 and data:
		try:
			item = get_inventory()['items'][data[-1]]
			setServerTeleportMode('custom', item['servername'] if item else None)
		except:
			pass
	return True

# All packets received from server are sent to this function
def handle_joymax(opcode, data):
	global teleportStarted
	# Server starts a teleport (loading screen)
	if opcode == 0x34B5 and teleportProbe:
		teleportStarted = True
	return True

# All chat messages received are sent to this function
def handle_chat(t,player,msg):
	# Remove guild name from union chat messages
	if t == 11:
		msg = msg.split(': ',1)[1]
	# Check player at leader list or a Discord message
	if player and lstLeaders_exist(player) or t == 100:
		# Parsing message command
		if msg == "START":
			start_bot()
			log("Plugin: Bot started")
		elif msg == "STOP":
			stop_bot()
			log("Plugin: Bot stopped")
		elif msg.startswith("TRACE"):
			# deletes empty spaces on right
			msg = msg.rstrip()
			if msg == "TRACE":
				if start_trace(player):
					log("Plugin: Starting trace to ["+player+"]")
			else:
				msg = msg[5:].split()[0]
				if start_trace(msg):
					log("Plugin: Starting trace to ["+msg+"]")
		elif msg == "NOTRACE":
			stop_trace()
			log("Plugin: Trace stopped")
		elif msg.startswith("SETPOS"):
			# deletes empty spaces on right
			msg = msg.rstrip()
			if msg == "SETPOS":
				p = get_position()
				set_training_position(p['region'], p['x'], p['y'],p['z'])
				log("Plugin: Training area set to current position (X:%.1f,Y:%.1f)"%(p['x'],p['y']))
			else:
				try:
					# check arguments
					p = msg[6:].split()
					x = float(p[0])
					y = float(p[1])
					# auto calculated if is not specified
					region = int(p[2]) if len(p) >= 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:].strip()
				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 and extra spaces
			msg = msg[8:].strip()
			if msg:
				# check params
				msg = msg.split(' ',1)
				rType = msg[0].lower()
				if rType == 'return':
					if reverse_return(0,''):
						log('Plugin: Reverse paketi gonderildi (son return noktasi)')
					else:
						log('Plugin: Reverse kullanilamadi (scroll yok olabilir)')
				elif rType == 'death':
					if reverse_return(1,''):
						log('Plugin: Reverse paketi gonderildi (son olum noktasi)')
					else:
						log('Plugin: Reverse kullanilamadi (scroll yok olabilir)')
				elif rType == 'player':
					if len(msg) >= 2:
						target = msg[1].strip().split()[0]
						# Use the real spelling if the player is in party
						realName = find_party_player_name(target)
						if realName:
							target = realName
						TeleportToPlayer(target)
					else:
						log('Plugin: Oyuncu adi yazilmadi. Ornek: REVERSE player OyuncuAdi')
				elif rType == 'zone':
					if len(msg) >= 2:
						zone = msg[1].strip()
						if reverse_return(3,zone):
							log('Plugin: Reverse paketi gonderildi -> bolge "'+zone+'"')
						else:
							log('Plugin: Reverse kullanilamadi')
				else:
					log('Plugin: Gecersiz reverse tipi! (return / death / player / zone)')
		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')
