-
Notifications
You must be signed in to change notification settings - Fork 34
/
Copy pathPOE_QOL2.py
768 lines (704 loc) · 46.2 KB
/
POE_QOL2.py
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
import tkinter as tk
import tkinter.messagebox as Msg
from tkinter import filedialog
import pygubu, pyautogui
from math import floor
import requests, json, configparser
from pygubu.builder import ttkstdwidgets
import os, time
from pathlib import Path
from math import ceil
# import win32con, win32gui
from tkinter import font
import datetime
import pyperclip
def debug_app(debug_bool):
import sys
sys.stdout = open('poeqol2_logfile.txt', 'w')
import pprint
pp = pprint.PrettyPrinter(indent=4)
return pp
def click_item(a, b, c):
"""
Used for when the user clicks on the highlight. Destroys the highlight and passes through the click action.
Wish I knew how to make it 'click-through able'
"""
a.destroy()
# exec(f"app.{a}.destroy()") #legacy -notaspy 14-9-2020
x, y = pyautogui.position()
pyautogui.click(x=x, y=y)
class MyApplication(pygubu.TkApplication):
def __init__(self, master=None):
"""
This seems fine. -notaspy 14-9-2020
"""
self.config = configparser.ConfigParser()
self.config.read('setup.ini')
DEBUG = (self.config['Config']['debug'].lower() == 'true' or 'y' in self.config['Config']['debug'].lower() or self.config['Config']['debug'].lower() != 0)
if DEBUG:
self.DEBUG = True
self.pp = debug_app(debug_bool=DEBUG)
self.pp.pprint("Setting up App")
self.pp.pprint("Initializing App")
else:
self.DEBUG = False
super().__init__(master=master)
def _create_ui(self):
if self.DEBUG:
self.pp.pprint("Creating UI")
self.builder = builder = pygubu.Builder()
builder.add_resource_path('.')
builder.add_from_file('Gui_Button_V2.ui')
self.mainwindow = builder.get_object('Frame_1', self.master)
self.font = font.Font(self.master, family="Times", size=20, weight="bold")
builder.connect_callbacks(self)
# TODO: Create a method that reloads the Setup.ini file before executing certain methods so it can be updated on the fly
# TODO: Validate the Setup.ini file contents and formatting and instruct user how to fix it if necessary
# TODO: Validate the chaos_items_filter.filter file contents and formatting and instruct user how to fix it if necessary
# TODO: Restore the original main filter file one exit. I really am bad at handling exit callbacks
# TODO: The overlay widget doesn't seem to sync with the local stash record (self.latest_stash)
# self.check_filter() # This is legacy, to set the self.active_status parameter. I don't think that is needed anymore
self.setup_app()
def setup_app(self):
"""
We run this on initialization. This is a separate method so that we can reload the settings while app is running. It *might* cause some undesired effects. TBD
"""
if self.DEBUG:
self.pp.pprint("Setting Up App")
#Note to self, from trying a bunch of different resolutions and 3 monitors i found that,
# stash/inv tabs had a fixed width to height ratio of 886/1440 (~0.6153)that must be obeyed.
self.screen_res = [int(dim) for dim in self.config['Config']['screen_res'].split('x')]
if len(self.screen_res) != 2:
raise ValueError("Screen Resolution was not given correctly. Use no spaces and only a single 'x'.")
# self.tab_width_frac = 888/1440 * self.screen_res[1] / self.screen_res[0] # Not actually used in the end.
# from same experiments, stash starts 22 pixels away from edge, or 22/1440 fraction of screen width, and top is 215/1440 fraction.
self.tab_origin = 22/1440 * self.screen_res[1], 215/1440 * self.screen_res[1]
# apply similar rules to ending coordinates -notaspy 14-9-2020
self.tab_end = 864/1440 * self.screen_res[1], 1057/1440 * self.screen_res[1]
# scale the size of a stash tab box depending on if it is quad or not.
# TODO: currently set by user, but can actually get this from the site request
if self.config['Config']['quad_tab'].lower() == 'true':
box_density_scalar = 24
else:
box_density_scalar = 12
# store the dimensions of an individual stash tab box (could be rectangular for some resolutions, so we store width and height)
self.box_width = (self.tab_end[0] - self.tab_origin[0]) / (box_density_scalar)
self.box_height = (self.tab_end[1] - self.tab_origin[1]) / (box_density_scalar)
# Store some meta-data about each item slot
# Probably better to use another data-structure other than a list of dicts
# scheme is [normalized width,
# normalized height,
# highlight color (can use any tk names color for now),
# order user should add item to inventory to avoid inventory tetris fail situations,
# threshold of how many items before dynamic filter editor starts to hide this item slot
#]
# TODO: We can get the sizes of the items directly from the site, rather than hard coding them as below
self.item_details = dict(
Rings=[1, 1, '#33bbee', '4', int(self.config['Config']['threshold'])*2],
OneHandWeapons=[1, 3, '#bbbbbb', '1', int(self.config['Config']['threshold'])*2],
BodyArmours=[2, 3, '#ee3377', '1', int(self.config['Config']['threshold'])],
Helmets=[2, 2, '#cc3311', '2', int(self.config['Config']['threshold'])],
Gloves=[2, 2, '#ee7733', '2', int(self.config['Config']['threshold'])],
Boots=[2, 2, '#009988', '2', int(self.config['Config']['threshold'])],
Belts=[2, 1, '#0077bb', '3', int(self.config['Config']['threshold'])],
Amulets=[1, 1, '#33bbee', '4', int(self.config['Config']['threshold'])],
)
# below is legacy code for when the screen resolution was hard-coded -notaspy 14-9-2020
## if self.config['Config']['screen_res'] == '1920x1080':
## for win in getWindowSizes():
## print(win)
## if 'Path of Exile' in win[2]:
## win32gui.SetWindowPos(win[0], win32con.HWND_NOTOPMOST, 0, 0, 1920, 1081, 0)
# Here is where things start to get convoluted with me trying to re-do the algorithm. I'd like to streamline this.
# I'll try to comment as best I can here and elsewhere
# This is legacy, but works okay, so left it. stash_finder returns a dict of item slots and thier coordinates in the stash for unid'd and id'd items.
# id'd items are basically ignored in this code, so not sure why they are tracked. Could be useful in future iterations.
# self.unident and self.ident represent the remote inventory last time it was checked. These values should not be changed other than when the remote has changed.
#TODO: Use data about identified items
self.unident, self.ident = self.stash_finder()
# Since the app has asynchronous knowledge of the items in tab, we want to have some local record. We'll call that latest_stash and allow it to be changed.
# ~~~IMPORTANT~~~~: The remote snapshot and the local record are two separate dict parameters and a length-2 list of dicts. I need to change this for consistency
# TODO: refactor remote snapshot and local record to be either both lists or both separate dicts for unidentified and identified items
self.latest_stash = list((self.unident.copy(), self.ident.copy()))
# initialize data for app's filter algorithm
self.chaos_items_filter_sections = self.read_default_chaos_filter_sections() # the custom formatting for items that match the chaos recipe
self.pre_process_item_filter()
# initial dynamic filter update
self.update_filter()
# check if the local and remote inventories are synchronized. Uses the refresh rate (in seconds) set in the Setup.ini file.
# I don't know the actual refresh rate of the website; seems random.
# Probably fine to assume that the local record is most accurate for 60s since it should take about that long to vendor everything.
self.check_inventory_sync() # Can't remember why I do this here, but it doesn't hurt anything (lol only one day later and I can't remember yikes)
# Because of all the wonky `exec` calls, I am keeping track of the highlight overlay objects created by the self.chaos_recipe() method
self.highlighted_items = []
if self.DEBUG:
self.pp.pprint("Done setting up App")
def run(self):
"""Run the main loop. Self explanatory."""
self.mainwindow.mainloop()
def remove_highlights(self, update_local_record=True):
"""
In case the user wants to manually remove the highlights on screen. By default it resets to local record to be synced with the remote snapshot.
We assume the user did not click on items if they removed all the highlights.
This is prone to errors if a user clicks on some, but not all of the highlights and then clicks this button.
TODO: handle half-removed highlights in combination with this method.
"""
if self.highlighted_items: # test that highlight actually exist that need deletion
for highlight in self.highlighted_items: # delete them
highlight.destroy()
if update_local_record: # update the snapshot and local record if requested
self.unident, self.ident = self.stash_finder()
self.latest_stash = list((self.unident.copy(), self.ident.copy()))
return True
else:
return False
def chaos_recipe(self):
"""
The meat of the program. Based on the number of complete sets, create top-level geometries that highlight areas of the screens for each item in the set.
TODO: Make it so that the item is removed from local inventor ONLY if the user clicks on the highlight box. I am sure someone will click it without actually removing the item and it will not be recognize and user will complain.
# TODO: HIGH Priority: figure out why sometimes the same initial areas are highlighted. I may have fixed this by checking the inventory sync (and for left over highlights) first thing
"""
# if any previous highlights still exist, destroy them.
# If we don't do this, the way it is written below, if user doesn't manually click each highlight, they become non-interactive.
# So, just killing everything is the fast and dirty way I decided wipe the screen clear if needed.
if self.check_inventory_sync():
self.remove_highlights(update_local_record=False)
else:
self.remove_highlights(update_local_record=True)
# get a dictionary of the LOCAL complete sets items.
# this will be sync'd with the online stash if this is the first time this method has been called since last remote refresh
# If user has clicked on a highlighted item, it gets removed locally, but the remote won't know that for a little.
# Dict keys are the slot name and values are the normalized positions.
# the positions are lists of length-2 lists:eg [[x0, y0], [x1, y1]]
unident = self.check_complete_set()
# unident will be an empty dict if there's no complete sets left, and will inform user
# TODO: This should work better
if not unident:
Msg.showinfo(title='POE QoL', message='Not enough Chaos Recipe Items')
# if we have sets, go into the highlighting logic
else:
# loop through each item slot (key)
for x in unident:
if self.DEBUG:
self.pp.pprint(('Item Slot:', x))
self.pp.pprint(('Item coordinates', unident[x]))
# we will count from the top-left origin
x_off = self.tab_origin[0]
y_off = self.tab_origin[1]
# cord_x, cord_y = self.unident[x].pop(0) # Leaving this here so you can see the previous method was to pop items from the list. It was problematic. -notaspy 14-9-2020
for i in range(len(unident[x])):
# reimplemented this as a loop over the items that make up the number of complete sets
# The execs are legacy. I don't like them, and could probably re-do it, but won't atm
#TODO: refactor exec usage
cord_x, cord_y = unident[x][i] # get coordinates of entry
cord_x = cord_x * self.box_width + x_off # convert coordinates to pixels
cord_y = cord_y * self.box_height + y_off
if self.DEBUG:
self.pp.pprint(('Screen Coordinates:',(cord_x, cord_y)))
box_width = self.box_width * self.item_details[x][0] # based on the meta-data about item size in self.item_details, make appropriate size box
box_height = self.box_height * self.item_details[x][1]
if self.DEBUG:
self.pp.pprint(('Box dimensions (pixels):',(box_width, box_height)))
# below is legacy
# basically it creates a semi-transparent top level window that disappears when it is clicked. I decided to use different colors by item slot
exec(f"self.{x + str(i)} = tk.Toplevel(self.mainwindow)")
exec(f'self.{x + str(i)}.attributes("-alpha", 0.65)')
exec(f'self.{x + str(i)}.config(background="{self.item_details[x][2]}")')
exec(f"self.{x + str(i)}.overrideredirect(1)")
exec(f'self.{x + str(i)}.attributes("-topmost", 1)')
exec(f'self.{x + str(i)}.geometry("{ceil(box_width)}x{ceil(box_height)}+{ceil(cord_x)}+{ceil(cord_y)}")')
exec(f'self.box = self.{x + str(i)}')
# was able to get rid of the legacy exec call below. Using the actual object fixes errors in destroying the highlight if highlighting over and over
# exec(f'self.{x + str(i)}.bind("<Button-1>",lambda command, a=x,b=cord_x,c=cord_y: click_item(a,b,c))') #legacy -notaspy 14-9-2020
self.box.bind("<Button-1>",lambda command, a=self.box,b=cord_x,c=cord_y: click_item(a,b,c)) #bind click command to object
# make sure the highlight objects persist so they are all interactive and can be deleted when method is run (see above, before loops)
self.highlighted_items.append(self.box)
def check_inventory_sync(self):
"""
This is kinda useful. Checks if the local and remote stashes are the same OR if the user-give refresh interval has elapsed.
Sets and returns a bool. I made this. -notaspy 14-9-2020
"""
t_check = datetime.datetime.now() # get current time
t_previous_check = self.last_update # we need to have this here since it is reset by the next call to self.stash_finder()
# compare local and remote stash inventories. short circuits if the refresh time has not elapsed
remote_inventory_unident, remote_inventory_ident = self.stash_finder()
if (t_check - t_previous_check) < datetime.timedelta(seconds=float(self.config['Config']['refresh_time'])) and remote_inventory_unident == self.unident:
self.synced = True
else:
self.synced = False
if self.DEBUG:
self.pp.pprint(f"Synced?: {self.synced}")
return self.synced
def check_complete_set(self):
"""
This is kind-of a Frakenstein code between legacy and my own.
I did my best to re-implement the logic to handle the local/remote problem. -notaspy 14-9-2020
"""
# If the local inventory and the last snapshot are not sync'd, update the remote snap shot and also make it the latest local stash inventory
if not self.check_inventory_sync():
self.unident, self.ident = self.stash_finder()
self.latest_stash = (self.unident, self.ident)
if self.DEBUG:
self.pp.pprint(self.unident)
# legacy test for existance of the attributes. kinda tried to refactor it. Functional but not pretty.
# Notice the different syntax for the remote snapshot and the local record (ie local is a list of dicts)
try:
self.unident
self.latest_stash[0]
except AttributeError:
self.unident, self.ident = self.stash_finder()
self.latest_stash[0] = (self.unident, self.ident)
else:
# Some more meat. Check for peices of a complete chaos set.
# first, if we don't have enough rings or one-handed weapons, just return false
if len(self.latest_stash[0]["Rings"]) < 2 or len(self.latest_stash[0]['OneHandWeapons']) < 2:
return False
# If we do, continue to determine how many sets we can make
else:
# Find the limiting two-slot item
two_slot_max_sets = min((floor(len(self.latest_stash[0]["Rings"]) / 2), floor(len(self.latest_stash[0]["OneHandWeapons"]) / 2)))
# Find limiting one-slot items
one_slot_max_sets = min([len(self.latest_stash[0][_key]) for _key in self.latest_stash[0].keys() if _key not in ["Rings", "OneHandWeapons"]])
# Find out if we are limited by two-slot, one-slot, or the user set maximum number of highlighted sets
max_sets = min((two_slot_max_sets, one_slot_max_sets, int(self.config['Config']['highlight_max_num_sets'])))
# if we have 1 or more sets, create a dictionary of the items that make up the sets
# this only works for unidentified sets
# TODO: do same for identified items?
if max_sets:
unident_sets = {_key:[] for _key in self.item_details.keys()} # create a dictionary of empty lists to fill in and return
# loop through each slot and find the maximum index in the list of coordinates for each item. We use this to only take the valid items.
for key in self.item_details.keys():
if self.DEBUG:
self.pp.pprint(f"Item Slot name for item in {max_sets} complete sets: {key}")
if key in ["Rings", "OneHandWeapons"]: # we need two of these, so the maximum index is twice that of the other items
max_index = 2 * max_sets
else:
max_index = max_sets
# Grab the items and their coordinates up the the maximum. These are dicts with the type of slot as the key, and a list of length-2 coordinates lists
# I think we don't want to 'pop' the item from the list because the loop will then try to access indexes that are outside the list length
for i in range(max_index):
unident_sets[key].append(self.latest_stash[0][key][i].copy())
if self.DEBUG:
self.pp.pprint(f"Item of slot {key}, item number {i} passed to highlighting method self.chaos_recipe(): {unident_sets[key][-1]}")
# Now remove these from the local inventory record. This could be more efficient by combining with the above, I am sure
# TODO: This logic needs testing and scrutiny. I am not 100% sure it is doing what I think it is.
for key in self.item_details.keys():
indices_to_delete = []
if self.DEBUG:
self.pp.pprint((f"self.latest_stash entry of {key}:", self.latest_stash[0][key]))
for i in range(len(self.latest_stash[0][key])):
if self.latest_stash[0][key][i] in unident_sets[key]:
indices_to_delete.append(i)
continue
# only keep the items that are not about to be highlighted
self.latest_stash[0][key] = [_ for j, _ in enumerate(self.latest_stash[0][key]) if j not in indices_to_delete]
return unident_sets
else:
# If we didn't have enough items for a complete set, return False
return False
def show_chaos(self):
"""
This is all legacy. It creates and shows the overlay that has a running counter of items in each stash
I did not make changes other than to comment out the error being raised if the monitor was not 1920x1080
I honestly don't know what the buttons are even for, they never show up?
It uses this bizzare and obscure pygubu library.
It also relies on some html file that comes with this code (or ccs? idk some web language)
"""
self.builder2 = pygubu.Builder()
self.builder2.add_from_file('Gui_Button_V2.ui')
self.top3 = tk.Toplevel(self.mainwindow)
# self.top3 = tk.Toplevel(self.mainwindow)
self.frame3 = self.builder2.get_object('Frame_2', self.top3)
self.builder2.connect_callbacks(self)
self.top3.overrideredirect(1)
print(self.top3.__dict__)
# I went ahead and put this at bottom center
overlay_location = f'+{self.screen_res[0] // 2 - 130}+{floor(self.screen_res[1] * (1 - 80/1080))}'
self.top3.geometry(overlay_location)
self.top3._offsetx = 260
self.top3._offsety = 80
def clickwin(event):
self.top3._offsetx = event.x
self.top3._offsety = event.y
def move_overlay(event):
x = round(self.top3.winfo_pointerx() - self.top3._offsetx)
y = round(self.top3.winfo_pointery() - self.top3._offsety)
print(x, y)
self.top3.geometry('+{x}+{y}'.format(x=x,y=y))
self.top3.clickwin = clickwin
self.top3.move_overlay = move_overlay
self.top3.bind('<Button-1>',self.top3.clickwin)
self.top3.bind('<B1-Motion>',self.top3.move_overlay)
if self.DEBUG:
self.pp.pprint(f'Overlay Location:{overlay_location}')
# if self.config['Config']['screen_res'] == '1920x1018':
# self.top3.geometry('+1180+900')
# elif self.config['Config']['screen_res'] == '1920x1080':
# self.top3.geometry('+1180+940')
# else:
# Msg.showinfo(title='POE QoL', message='Wrong Resolution msg: macr0s on Discord')
self.top3.attributes('-topmost', 1)
self.refresh_me()
def close_overlay(self):
# more legacy for overlay
self.top3.destroy()
# def move_overlay(self, event):
# x = self.top3.winfo_pointerx() - self.top3._offsetx
# y = self.top3.winfo_pointery() - self.top3._offsety
# print(x, y)
# self.top3.geometry('+{x}+{y}'.format(x=x,y=y))
def refresh_me(self):
# Refreshes the running count of unidentified and identified items in the stash tab.
# Fails silently if inventories are considered synced
# more legacy. Tried harder to make this work better with the syncing scheme
self.check_inventory_sync()
if not self.synced:
self.unident, self.ident = self.stash_finder()
self.latest_stash = list((self.unident.copy(), self.ident.copy()))
if self.DEBUG:
self.pp.pprint("Refreshing filter within refresh me.")
# unident, ident = self.stash_finder()
for key, value in self.unident.items():
alternative = len(self.ident.get(key, 0))
exec(f'self.builder2.get_object("{key}").configure(text="{key[:4]}: {len(value)}/{alternative}")')
if self.DEBUG:
self.pp.pprint("Printing Out Latest Stash:")
self.pp.pprint(self.latest_stash)
self.pp.pprint("Printing Out Latest Snapshot:")
self.pp.pprint(self.unident)
self.update_filter()
def check_filter(self):
"""
Legacy dynamic filter code. This doesn't work as far as I can tell. I am in the process of re-implementing this.
Right now all I can tell it is good for is setting the self.active_status parameter. Other methods looks for this.
This is called in the init method.
"""
rewrite = 0
with open(self.config['Config']['filter'], 'r') as f:
lines = f.readlines()
if '# Chaos Recipe Ring' not in lines[0]:
rewrite = 1
if rewrite == 1:
with open(self.config['Config']['filter'], 'w') as f:
filterfile = open(self.config['Config']['filter'])
f.write(filterfile.read())
filterfile.close()
for line in lines:
f.writelines(line)
self.active_status = {'Rings':[1, lines[1]],
'Belts':[15, lines[15]],
'Amulets':[28, lines[28]],
'Boots':[41, lines[41]],
'Gloves':[55, lines[55]],
'Helmets':[69, lines[69]],
'BodyArmours':[83, lines[83]],
'OneHandWeapons':[97, lines[97]]
}
def stash_finder(self):
"""
Legacy code. This works well enough.
Grabs the json object of the stash tab. Takes only the items, and grabs their stash position if they are unidentified.
TODO: Right now, the items are disordered. They should be ordered to register from top-left to bottom right for efficiency
"""
pos_last_unid = {'BodyArmours':[], 'Helmets':[], 'OneHandWeapons':[], 'Gloves':[], 'Boots':[], 'Amulets':[], 'Belts':[], 'Rings':[]}
pos_last_id = {'BodyArmours':[], 'Helmets':[], 'OneHandWeapons':[], 'Gloves':[], 'Boots':[], 'Amulets':[], 'Belts':[], 'Rings':[]}
stash_tab = f"https://www.pathofexile.com/character-window/get-stash-items?league={self.config['Config']['league']}&tabIndex={self.config['Config']['tab']}&accountName={self.config['Config']['account']}"
payload = {
'league': self.config['Config']['league'],
'tabIndex': self.config['Config']['tab'],
'accountName': self.config['Config']['account'].encode('utf-8'),
}
if self.DEBUG:
self.pp.pprint("Pulling from pathofexile.com")
self.pp.pprint("trying payload: {}".format(payload))
self.pp.pprint(f"trying: {stash_tab}")
try:
a = requests.get(stash_tab, cookies=dict(POESESSID=(self.config['Config']['POESESSID'])), params=payload)
except requests.HTTPError as exception:
Msg.showinfo(title='POE QoL', message='Could not connect to pathofexile.com.')
if self.DEBUG:
try:
self.pp.pprint("json retrieved:")
self.pp.pprint(json.loads(a.text)['items'])
except: # I have no clue what error types might return here, but I want to output something to log file
self.pp.pprint("JSON could not be output to logfile.")
self.last_update = datetime.datetime.now() #added by notaspy 14-9-2020
# I am not sure the logic here. It is able to find the item coordinates, but it looks like it does it twice. Didn't mess with it
try:
json.loads(a.text)['items']
except KeyError:
Msg.showinfo(title='POE QoL', message='Bad Response from pathofexile.com. Please check your Setup file that the accoud and POESESSID are correct and current as a first step (and restart app if they needed to be changed).')
#TODO: log this error in a log file
for x in json.loads(a.text)['items']:
if x['name'] == '' and x['frameType'] != 3:
if 'BodyArmours' in x['icon']:
pos_last_unid['BodyArmours'].append([x['x'], x['y']])
elif 'Helmets' in x['icon']:
pos_last_unid['Helmets'].append([x['x'], x['y']])
elif 'OneHandWeapons' in x['icon']:
pos_last_unid['OneHandWeapons'].append([x['x'], x['y']])
elif 'Gloves' in x['icon']:
pos_last_unid['Gloves'].append([x['x'], x['y']])
elif 'Boots' in x['icon']:
pos_last_unid['Boots'].append([x['x'], x['y']])
elif 'Amulets' in x['icon']:
pos_last_unid['Amulets'].append([x['x'], x['y']])
elif 'Belts' in x['icon']:
pos_last_unid['Belts'].append([x['x'], x['y']])
elif 'Rings' in x['icon']:
pos_last_unid['Rings'].append([x['x'], x['y']])
else:
#TODO: This is what handles unknown/new items i think. It is 99% probably responsible for the download short circuit on alternate artwork
if x['frameType'] == 3: # I dont know what this is fore
pass
else:
if 'BodyArmours' in x['icon']:
pos_last_id['BodyArmours'].append([x['x'], x['y']])
else:
if 'Helmets' in x['icon']:
pos_last_id['Helmets'].append([x['x'], x['y']])
else:
if 'OneHandWeapons' in x['icon']:
pos_last_id['OneHandWeapons'].append([x['x'], x['y']])
else:
if 'Gloves' in x['icon']:
pos_last_id['Gloves'].append([x['x'], x['y']])
else:
if 'Boots' in x['icon']:
pos_last_id['Boots'].append([x['x'], x['y']])
else:
if 'Amulets' in x['icon']:
pos_last_id['Amulets'].append([x['x'], x['y']])
else:
if 'Belts' in x['icon']:
pos_last_id['Belts'].append([x['x'], x['y']])
else:
if 'Rings' in x['icon']:
pos_last_id['Rings'].append([x['x'], x['y']])
else:
return (pos_last_unid, pos_last_id)
# below is some half-implemented code for dynamically updating a main filter file. Idea is to be able to use your normal filter along with this helper.
# I could use some help/optimization here. -notaspy 14-9-2020
def read_default_chaos_filter_sections(self):
"""
User can use the filter that comes with this program, or customize each slot to their liking.
Only important things are that each section starts with a '#' and has the correct item slot name in that line
Correct item slots are give in the self.item_details parameter. This should be the last word in the comment line.
"""
with open(self.config['Config']['chaos_items_filter'], 'r') as fil:
chaos_filter = fil.readlines() # read whole file into memory. each line is stored as a string in a list
section_lines_start_end = [] # need a place to store where sections start and end
section_starts = []
for i, line in enumerate(chaos_filter): # loop through the lines
_line = line.lstrip() # remove any leading white space
# If the line is a comment, record that as the start of an item slot section
# We need to protect from empty lines which are stored as zero-length lists
if self.DEBUG:
self.pp.pprint(("Default Filter Line as read:", _line))
self.pp.pprint(("Result of bool test for empty line:", not _line))
if _line:
self.pp.pprint(("Result of bool test for comment:", not _line[0] == "#"))
if not _line or not _line[0] == "#":
continue
elif _line and _line[0] == "#": # I shouldn't need to, but I double check that the line is a comment anyway
section_starts.append(i)
# each section ends where the next begins. The last section goes to the last line in the list, so concatenate that to the other ending indicies
section_ends = [i for i in section_starts[1:]] + [len(chaos_filter)+1]
# create empty dictionary for storing the text of each section
sections = {}
# store the text for each section in the dictionary. The key for each section is the last word in the first line, chaos_filter[i].split(" ")[-1].rstrip(). This is maybe a dumb way of doing this and prone to user error.
# TODO: Find a better way to get the section keys -- Update, trying this below now.
for i, j in zip(section_starts, section_ends):
# sections[chaos_filter[i].split(" ")[-1].rstrip()] = chaos_filter[i:j] # for each key, separate line into list of words, ensure whitespace is stripped. Text is from starting to ending indices
for k in range(i, j-1): #loop through all the lines in the section
print(k)
linelistcopy = chaos_filter[k][:].split(" ") # create a copy to work with and remove white space and make a list
linelistcopy = [str(_).rstrip().replace("'", '') for _ in linelistcopy] # convert to strings wihtout quotes...?
linelistcopy = [_.replace('"', '') for _ in linelistcopy] # convert to strings wihtout quotes...?
self.pp.pprint(linelistcopy)
if linelistcopy[0].lower() == 'class':
if "One" in linelistcopy and "Hand" in linelistcopy:
section_class_key = "OneHandWeapons"
elif linelistcopy[1].lower() == 'body':
section_class_key = "BodyArmours"
else:
section_class_key = linelistcopy[1]
sections[section_class_key.rstrip()] = chaos_filter[i:j]
if self.DEBUG:
self.pp.pprint(sections)
return sections
def pre_process_item_filter(self):
"""
This accomplishes a few tasks that only need to be performed once on start up.
1) Determine if the included filter is in the users My Games\\Path of Exile\\ directory. If it exists we open it, and if not, we open the included default.
2) Open that file and find the starting and ending lines of the chaos recipe items and remember those for updating. These are bound by the random strings '234hn50987sd' and '2345ina8dsf7' respectively.
3) Store the parts of the item filter that don't change into memory. The current filter is small (~350kB), so we don't need to worry about memory. This saves from needing to ever read it again.
4) Insert the chaos_items_filter.filter contents into the filter text and write the file to the user's USERPROFILE\\Documents\\My Games\\Path of Exile\\ directory
We want to be reading and searching this file once.
"""
#TODO: Maybe someone has a custom location for their item filters, so this search path probably shouldn't be hard coded like this
# self.main_filter_path = os.path.join(os.environ['USERPROFILE'], "Documents", "My Games", "Path of Exile", self.config['Config']['filter'])
user_path = Path.home() # Get the user home directory to look for the POE filters directory
if os.path.isabs(self.config['Config']['filter']): # if the path given is absolute, lets try it as is, else let stry to figure some more information about the location
self.main_filter_path = self.config['Config']['filter']
path_pre = os.path.split(self.main_filter_path)
else: # if not lets try the default PoE path
# path_suf = os.path.relpath(os.path.join("My Games", "Path of Exile", self.config['Config']['filter']), start=user_path) # handle if the user put in a full path or other extra information
path_pre = os.path.join(user_path, "Documents", "My Games", "Path of Exile") # handle if the user put in a full path or other extra information
path_to_filter = os.path.join(path_pre, self.config['Config']['filter'])
self.main_filter_path = path_to_filter
if self.DEBUG:
self.pp.pprint(f"Path Prefix: {path_pre}\n")
self.pp.pprint(f"Searching for filter:\n {self.main_filter_path}\n")
filter_exists = os.path.isfile(self.main_filter_path)
def_filter_path = os.path.abspath(self.main_filter_path)
if filter_exists:
with open(def_filter_path, 'r') as fil:
self.main_filter = fil.readlines() # read default file into memory
if self.DEBUG:
self.pp.pprint(f"Found filter:")
# self.pp.pprint(self.main_filter)
else:
# If it didn't exist, we will write it at the end of this method.
# Use the included default for now.
Msg.showinfo('POE QoL', f'POE QoL could not automatically find a filter at {self.main_filter_path}.\n\nPlease select the folder where your pathofexile fitlers are located to create a new filter named "POEQOL_Base.filter" based on the filter of the same name in this directory.')
self.main_filter_path = os.path.normpath(os.path.join(filedialog.askdirectory(), 'POEQOL_Base.filter'))
with open('POEQOL_Base.filter', 'r') as fil:
if self.DEBUG:
self.pp.pprint(f"Could not find the filter as indicated in config file at: {def_filter_path}")
self.pp.pprint("User has selected {} to write the default filter:".format(self.main_filter_path))
self.main_filter = fil.readlines() # read default filter file into memory
# There was way too much trouble with reading files from a setup file and with users not following directions, so I am going to try walking them through an initial setup.
# we are now just going to update the setup file with what the user says to avoid errors in the future
config_file_updates = {'filter':{'path': self.main_filter_path, 'lino': None, 'field':'filter='}}
with open('Setup.ini', 'r', encoding='utf-8') as configfile_in:
contents0 = configfile_in.readlines()
for lino, l in enumerate(contents0):
if l[0:7] == 'filter=':
config_file_updates['filter']['lino'] = lino
else:
continue
with open('Setup.ini', 'w', encoding='utf-8') as configfile_out:
contents0[config_file_updates['filter']['lino']] = config_file_updates['filter']['field'] + config_file_updates['filter']['path'] + "\n" # encode it at utf-8 for international players
for l in contents0:
configfile_out.write(l)
self.chaos_items_sections_start_line = 0 # start a line counter to find the section in the filter where we should insert the dynamic text from the chaos_items_filter file (see read_default_chaos_filter_sections())
self.chaos_items_sections_end_line = len(self.main_filter)
for i, line in enumerate(self.main_filter):
# I use a random string to find where the chaos recipe section begins and ends
# break after the end of the section has been found
if line[0] != "#": # If the line isn't a comment, we can just move on
continue
elif '234hn50987sd' in line:
self.chaos_items_sections_start_line = i + 1
if self.DEBUG:
self.pp.pprint(f"Start of chaos recipe section found at line {self.chaos_items_sections_start_line}")
continue
elif '2345ina8dsf7' in line:
self.chaos_items_sections_end_line = i
if self.DEBUG:
self.pp.pprint(f"End of chaos recipe section found at line {self.chaos_items_sections_end_line}")
break
#TODO: This else clause should be implemented, but doesn't work right now
# else:
# # If we cannot find the section. alert the user... with some vague, unhelpful instructions and return False. Didn't raise an error here, idk if I should
# Msg.showinfo(title='POE QoL', message='Cannot find the chaos recipe section in your main filter.\n' +
# 'It should start with "# 234hn50987sd End Chaos Recipe Auto-Update Section" and end in "# 2345ina8dsf7 End Chaos Recipe Auto-Update Section".\n'+
# 'Msg @notaspy#6561 for help. 14-09-2020 \n')
# return False
if self.DEBUG:
self.pp.pprint("The entire filter file was looped through. This should not happen.")
# take everything before and after the chaos recipe section from the original filter file. It shouldnt be changed ever. We will make changes between these two sections on each update.
self.main_filter0 = self.main_filter[0:self.chaos_items_sections_start_line] + ['\n']
self.main_filter1 = ['\n'] + self.main_filter[self.chaos_items_sections_end_line:] + ['\n']
if not filter_exists:
with open(self.main_filter_path, 'w') as fil:
for line in self.main_filter:
fil.write(line)
return self.main_filter
def update_filter(self):
"""
Attempt to update the main filter with showing/hiding recipe item slots that have reached the threshold.
It is inefficient, since it loops through a very large filter blade file, and re-writes text that should not change.
I re-insert all the text from the chaos_items_filter just to be safe, but wouldn't need to if this is implemented in a better way.
This will not hide any items set to be ignored in the Setup.ini file.
"""
if self.DEBUG:
self.pp.pprint("Trying to update Filter.")
assert(self.main_filter) # assert that a main filter was loaded
assert(self.main_filter0) # assert that a main filter prefix exists
assert(self.main_filter1) # assert that a main filter suffix exists
if self.DEBUG:
self.pp.pprint("Found necessary filter files.")
# go through the item slots and their meta-data (which has the threshold for items set by user)
for slot, details in self.item_details.items():
try:
# if the slot is on the ignor list or if the number of items is not greater than the threshold, keep it in the filter
if slot in self.config['Config']['ignore_threshold'] or len(self.latest_stash[0][slot]) < details[4]:
self.chaos_items_filter_sections[slot][1] = "Show\n" # The show/hide flag is the second entry in the filter section text (see chaos_items_filter in Setup.ini)
if self.DEBUG:
self.pp.pprint(f"Found {len(self.latest_stash[0][slot])} items and ignore_threshold is set to {self.config['Config']['ignore_threshold']}. The filter will now show items of {slot} slot.")
else: # Otherwise hide that slot
self.chaos_items_filter_sections[slot][1] = "Hide\n"
if self.DEBUG:
self.pp.pprint(f"Found {len(self.latest_stash[0][slot])} items and ignore_threshold is set to {self.config['Config']['ignore_threshold']}. The filter will now show items of {slot} slot.")
except (AttributeError, ValueError): # Try to catch some errors. Not sure if this will work, don't have time to test the string formatting and message box
# TODO: Test this error message
Msg.showinfo(title='POE QoL', message=f'Check default filter formatting. There should be a valid entry for each item slot. The last word in each line should be one of the following: {[str(_[0]) for _ in self.item_details]}')
# flatten the list of lists for the lines that should be added to the filter file
new_filter_lines = [l for slt in self.chaos_items_filter_sections.values() for l in slt]
if self.DEBUG:
self.pp.pprint(f"Text to be inserted into the user's main filter file between lines {self.chaos_items_sections_start_line} and {self.chaos_items_sections_end_line}")
self.pp.pprint(new_filter_lines)
new_main_filter = self.main_filter0 + new_filter_lines + self.main_filter1
if self.DEBUG:
self.pp.pprint((len(self.main_filter0), len(self.main_filter1), len(new_filter_lines)))
# TODO:enable the writing after testing
with open(self.main_filter_path, 'w') as fil:
for line in new_main_filter:
fil.write(line)
return True
#Below are just methods that will search the stash tab for common things. didn't mess with these -notaspy 14-9-2020
def search(self, text):
pyperclip.copy(text)
x, y = pyautogui.position()
pyautogui.click(x= floor(self.tab_end[0] * 19/24), y=floor(self.tab_end[1] * 1183/1057))
pyautogui.moveTo(x=x, y=y)
pyautogui.hotkey('ctrl', 'f')
pyautogui.hotkey('ctrl', 'v')
def currency(self):
self.search('"currency"')
def essence(self):
self.search('"essence of"')
def divcard(self):
self.search('"divination"')
def fragment(self):
self.search('"can be used in a personal Map device"')
def splinter(self):
self.search('"splinter"')
def delve(self):
self.search('"fossil"')
def incubator(self):
self.search('"incubator"')
def map(self):
self.search('"map""tier"')
def blight_map(self):
self.search('"blighted" "tier"')
def veiled(self):
self.search('"veiled"')
def rare(self):
self.search('"rare"')
def unique(self):
self.search('"unique"')
def prophecy(self):
self.search('"prophecy"')
def gem(self):
self.search('"gem"')
def unid(self):
self.search('"unid"')
if __name__ == '__main__':
# legacy. Run the applet.
root = tk.Tk()
root.title('Path of Exile - Quality of Life (POE-QOL)')
app = MyApplication(root)
app.run()