-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathxControl.py
More file actions
1347 lines (1287 loc) · 43.9 KB
/
Copy pathxControl.py
File metadata and controls
1347 lines (1287 loc) · 43.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from phBot import *
from threading import Timer
import phBotChat
import QtBind
import struct
import random
import json
import os
import sqlite3
import time
import webbrowser
GITHUB_URL = 'https://github.com/Vette1123'
GITHUB_BTN_STYLE = (
'QPushButton{background:#ffd54a;color:#222;font-weight:bold;'
'border:1px solid #8b6b00;border-radius:6px;padding:2px 10px;}'
'QPushButton:hover{background:#ffe27a;}'
)
def btn_github_clicked():
try:
webbrowser.open(GITHUB_URL)
except Exception:
pass
def _try_style_github(btn):
for fn_name in ('setStyleSheet', 'setStylesheet', 'setStyle'):
fn = getattr(QtBind, fn_name, None)
if callable(fn):
try:
fn(gui, btn, GITHUB_BTN_STYLE)
return
except Exception:
pass
pName = 'xControl'
pVersion = '1.9.1'
pAuthor = 'JellyBitz, Vette1123 (Gado)'
pUrl = 'https://raw.githubusercontent.com/Vette1123/phbot-plugins/main/xControl.py'
# GitHub: https://github.com/Vette1123
# ______________________________ 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,'Syntax: COMMAND #required #optional? — scroll the list below for all commands',11,30)
# Leaders panel (top-right) — compact, labelled
QtBind.createLabel(gui,'── Party Leaders ──',525,11)
tbxLeaders = QtBind.createLineEdit(gui,"",525,30,110,20)
lstLeaders = QtBind.createList(gui,525,53,110,242)
btnAddLeader = QtBind.createButton(gui,'btnAddLeader_clicked'," Add ",635,29)
btnRemLeader = QtBind.createButton(gui,'btnRemLeader_clicked'," Remove ",635,52)
btnGithub = QtBind.createButton(gui,'btn_github_clicked',' ⭐ Gado ⭐ ',635,76)
_try_style_github(btnGithub)
# Scrollable command reference — single tall list, fills most of the panel.
QtBind.createLabel(gui,' ── Command Reference ──',11,48)
lstCommands = QtBind.createList(gui,11,65,500,230)
_commandHelp = [
'━━━━━━ BOT / SESSION ━━━━━━',
' START ► Start the bot',
' ex › START',
' STOP ► Stop the bot',
' ex › STOP',
' DC ► Disconnect from game',
' ex › DC',
' PROFILE #Name? ► Load profile (default: Default)',
' ex › PROFILE PvP',
' ex › PROFILE (loads "Default")',
' GETOUT ► Leave the party',
' ex › GETOUT',
'',
'━━━━━━ TRACE / FOLLOW ━━━━━━',
' TRACE #Player? ► Trace leader or specified player',
' ex › TRACE (traces the command sender)',
' ex › TRACE Vette',
' NOTRACE ► Stop trace',
' ex › NOTRACE',
' FOLLOW #Player? #Distance? ► Follow party player (no attack)',
' ex › FOLLOW (follow sender at distance 10)',
' ex › FOLLOW 0 (exact trace sender, distance 0)',
' ex › FOLLOW Vette 5',
' NOFOLLOW ► Stop following',
' ex › NOFOLLOW',
'',
'━━━━━━ MOVEMENT / TELEPORT ━━━━━━',
' MOVEON #Radius? ► Random movement (default radius 10)',
' ex › MOVEON (uses default radius)',
' ex › MOVEON 20',
' RETURN ► Use Return Scroll (or resurrect if dead)',
' ex › RETURN',
' RECALL #Town ► Set recall on city portal',
' ex › RECALL Constantinople',
' ex › RECALL Jangan',
' TP #Destination #Source? ► Teleport to destination',
' • Source auto-detected from nearby NPCs if omitted',
' ex › TP Hotan (omit source → scan nearby NPCs)',
' ex › TP Constantinople (auto-find Constantinople teleporter)',
' ex › TP Hotan, Jangan (explicit: from Jangan to Hotan)',
' ex › TP Hotan Jangan (space separator also works)',
' REVERSE #Type #Name? ► Reverse return',
' • types: return / death / player / zone',
' ex › REVERSE return',
' ex › REVERSE death',
' ex › REVERSE player Vette',
' ex › REVERSE zone Jangan',
' R #Player ► Shortcut for REVERSE player #Player',
' ex › R Vette',
' GETPOS ► Print current position (whispered back)',
' ex › GETPOS',
'',
'━━━━━━ TRAINING AREA ━━━━━━',
' SETPOS #X? #Y? #Region? #Z? ► Set training position',
' ex › SETPOS (uses your current position)',
' ex › SETPOS 6537 1234',
' ex › SETPOS 6537 1234 25000 50',
' SETRADIUS #Radius? ► Set training radius (default 35)',
' ex › SETRADIUS (resets to 35)',
' ex › SETRADIUS 50',
' SETSCRIPT #Path? ► Change training script path',
' ex › SETSCRIPT (clears the script)',
' ex › SETSCRIPT C:\\scripts\\hotan.txt',
' SETAREA #Name ► Change training area by config name',
' ex › SETAREA Hotan East',
'',
'━━━━━━ COMBAT / EMOTES ━━━━━━',
' ZERK ► Use Berserker mode',
' ex › ZERK',
' SIT ► Sit / stand up (toggle)',
' ex › SIT',
' JUMP ► Knockback emote',
' ex › JUMP',
' CAPE #Color? ► Use PVP Cape',
' • off / red / gray / blue / white / yellow (default: yellow)',
' ex › CAPE (uses yellow)',
' ex › CAPE red',
' ex › CAPE off',
'',
'━━━━━━ PETS / MOUNTS ━━━━━━',
' MOUNT #PetType? ► Mount horse (default) or pet by type',
' • types: horse / transport / attack / fellow',
' ex › MOUNT (omit → mounts horse)',
' ex › MOUNT horse',
' ex › MOUNT transport (trade/job caravan)',
' ex › MOUNT attack (attack pet)',
' ex › MOUNT fellow (fellow pet)',
' DISMOUNT #PetType? ► Dismount horse (default) or pet',
' ex › DISMOUNT (omit → dismounts horse)',
' ex › DISMOUNT horse',
' ex › DISMOUNT transport',
' ex › DISMOUNT attack',
' TERMINATE ► Unsummon transport pets (horse +',
' caravan/trade); keeps grab/attack pets',
' ex › TERMINATE (after mounting a trade caravan)',
' ex › TERMINATE (also kills your horse summon)',
'',
'━━━━━━ ITEMS ━━━━━━',
' EQUIP #ItemName ► Equip item (partial name OK)',
' • aliases: TRADER / HUNTER / THIEF / JOB',
' • partial name searches both display name & servername',
' ex › EQUIP Steel Sword (partial — first match equipped)',
' ex › EQUIP TRADER (equip trader job suit)',
' ex › EQUIP HUNTER (equip hunter job suit)',
' ex › EQUIP THIEF (equip thief job suit)',
' ex › EQUIP JOB (equip any job suit found)',
' UNEQUIP #ItemName ► Unequip item (partial name OK)',
' • aliases: TRADER / HUNTER / THIEF / JOB',
' ex › UNEQUIP Helmet',
' ex › UNEQUIP TRADER',
' ex › UNEQUIP HUNTER',
' ex › UNEQUIP THIEF',
' ex › UNEQUIP JOB (unequip any equipped job suit)',
' USE #ItemName ► Use item from inventory',
' • partial name; uses first matching item',
' ex › USE HP Potion',
' ex › USE Return Scroll',
' ex › USE Universal Pill',
' SORT ► Sort inventory (compact items)',
' ex › SORT',
'',
'━━━━━━ CHAT / PACKETS ━━━━━━',
' CHAT #Type #Message ► Send a chat message',
' • simple types: all / party / guild / union / global / stall',
' • with target: private #Name #Msg / note #Name #Msg',
' ex › CHAT all Hello world',
' ex › CHAT party Ready!',
' ex › CHAT guild Stocking up at house',
' ex › CHAT union United team!',
' ex › CHAT global LFG Hotan',
' ex › CHAT stall SoX gear cheap',
' ex › CHAT private Vette hi there',
' ex › CHAT note Vette see you tomorrow',
' INJECT #Opcode #Encrypted? #Data? ► Inject raw packet',
' • Opcode in hex (0x….). Encrypted flag = true/false (default false)',
' • Data is space-separated hex bytes (omit for empty payload)',
' ex › INJECT 0x704F false 04 (SIT/stand)',
' ex › INJECT 0x7516 false 01 (red PVP cape)',
' ex › INJECT 0x70A7 false 01 (toggle Berserker)',
' ex › INJECT 0x3091 false 0c (jump emote)',
' ex › INJECT 0x7061 (leave party — no data)',
'',
'━━━━━━ PLUGINS ━━━━━━',
' CARAVAN ON/OFF/STATUS/SCAN/GO ► Control xCaravan (auto-caravan) plugin',
' ex › CARAVAN ON',
' ex › CARAVAN GO',
]
for _line in _commandHelp:
QtBind.append(gui,lstCommands,_line)
# ______________________________ 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):
# Don't follow self (master sending FOLLOW would otherwise target himself)
me = get_character_data()
if me and player and me.get('name','').lower() == player.lower():
return False
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
# 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
# Terminate ridden transports (mounts, trade caravans, new job-system caravans, etc).
# Whitelist: never touches grab ('pick') or attack pets ('attack','fellow').
_KEEP_PET_TYPES = ('pick','attack','fellow')
def _TerminatePetByType(petType):
# Re-fetch pets fresh so we use the current uid (it can change after dismount)
pets = get_pets()
if not pets:
return
for uid,pet in pets.items():
if pet['type'] == petType:
# CLIENT_PET_TERMINATE: uint32 pet UID
inject_joymax(0x7116,struct.pack('I',uid),False)
return
def TerminatePet():
pets = get_pets()
count = 0
if pets:
for uid,pet in pets.items():
if pet['type'] in _KEEP_PET_TYPES:
continue
# Dismount first if currently mounted on it
p = b'\x00'
p += struct.pack('I',uid)
inject_joymax(0x70CB,p,False)
# Defer the terminate so the dismount packet round-trips first
# (sleeping here would block the chat callback thread)
Timer(0.6, _TerminatePetByType, (pet['type'],)).start()
count += 1
return count
# 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 = 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()
# Track messages this bot sent via the script `chat` command, so we don't act on our own echoes
_selfSentChat = []
# Cap the dedup list. Echoes return within a tick or two, so only the most
# recent entries can ever match. Unmatched entries (e.g. union chat, whose
# echo arrives with the guild prefix stripped) would otherwise accumulate
# forever — a slow memory leak. Keep just the last N.
_MAX_SELF_SENT = 32
def _remember_self_sent(key):
_selfSentChat.append(key)
if len(_selfSentChat) > _MAX_SELF_SENT:
del _selfSentChat[:-_MAX_SELF_SENT]
# Throttle the "Following ..." log so it doesn't spam the log file at 2 Hz
# while following. Movement itself is unchanged — only the log line is rate-limited.
_last_follow_log_at = 0.0
def _follow_log(message):
global _last_follow_log_at
now = time.time()
if now - _last_follow_log_at >= 5.0:
_last_follow_log_at = now
log(message)
# 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]
# Track our own outgoing chat so we don't double-process, but let the leader
# execute every command on himself. FOLLOW 0 self-target is blocked inside
# start_follow, not here.
key = (t, msg)
if key in _selfSentChat:
_selfSentChat.remove(key)
# 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
args = [a.strip() for a in msg.split(split) if a.strip()]
if len(args) >= 2:
# Explicit form: TP <source> <destination>
inject_teleport(args[0],args[1])
elif len(args) == 1:
# Auto form: TP <destination> — scan nearby NPCs for one that offers this destination
destination = args[0]
npcs = get_npcs()
found = False
if npcs:
for key,npc in npcs.items():
source = npc['name']
if get_teleport_data(source,destination):
inject_teleport(source,destination)
found = True
break
# also try servername in case map uses it
source = npc['servername']
if source and get_teleport_data(source,destination):
inject_teleport(source,destination)
found = True
break
if not found:
log('Plugin: No nearby teleporter offers ['+destination+']')
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 first arg is numeric, treat it as distance and follow the sender
if len(msg) >= 1:
try:
distance = float(msg[0])
# charName stays as the sender (player)
except ValueError:
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.startswith("TERMINATE"):
# Terminates transport horse and trade pet (caravan) — grab/attack pets are left alone
n = TerminatePet()
if n:
log("Plugin: Terminated "+str(n)+" transport(s)")
else:
log("Plugin: No transport pets to terminate")