forked from MertenNor/GameReader
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGameReader.py
More file actions
7656 lines (6627 loc) · 363 KB
/
GameReader.py
File metadata and controls
7656 lines (6627 loc) · 363 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
###
### I know.. This code is.. well not great, all made with AI, but it works. feel free to make any changes!
###
### FIXED: Right and Left Ctrl keys now properly distinguished using scan code detection
### The keyboard.is_pressed() function doesn't reliably distinguish left/right modifier keys
### on Windows, so we now use scan codes (29 for Left Ctrl, 157 for Right Ctrl) for accurate detection.
###
# Standard library imports
import datetime
import io
import json
import os
import re
import sys
import tempfile
import threading
import time
import webbrowser
from functools import partial
from tkinter import filedialog, messagebox, simpledialog, ttk
# Third-party imports
import keyboard
import mouse
import pyttsx3
import pytesseract
import requests
import tkinter as tk
import win32api
import win32com.client
import win32con
import win32gui
import win32ui
import win32process
from PIL import Image, ImageEnhance, ImageFilter, ImageGrab, ImageTk
import ctypes
import winsound
import asyncio
import queue
# Controller support
try:
import inputs
CONTROLLER_AVAILABLE = True
print("Controller support enabled - 'inputs' library loaded successfully")
except ImportError:
CONTROLLER_AVAILABLE = False
print("Warning: 'inputs' library not available. Controller support disabled.")
print("To enable controller support, install with: pip install inputs")
try:
# winsdk is the package name; modules generally import from winrt.*
import importlib
UWP_TTS_AVAILABLE = False
_uwp_import_error = None
try:
from winsdk.windows.media.speechsynthesis import SpeechSynthesizer
from winsdk.windows.storage.streams import DataReader
UWP_TTS_AVAILABLE = True
except Exception as e:
_uwp_import_error = e
try:
# Attempt to import winsdk meta package and retry
importlib.import_module('winsdk')
from winsdk.windows.media.speechsynthesis import SpeechSynthesizer
from winsdk.windows.storage.streams import DataReader
UWP_TTS_AVAILABLE = True
_uwp_import_error = None
except Exception as e2:
_uwp_import_error = e2
# As a last attempt, try alternate import path (rare)
try:
from winsdk.windows.media.speechsynthesis import SpeechSynthesizer # type: ignore
from winsdk.windows.storage.streams import DataReader # type: ignore
UWP_TTS_AVAILABLE = True
_uwp_import_error = None
except Exception as e3:
_uwp_import_error = e3
except Exception as _e_init:
UWP_TTS_AVAILABLE = False
_uwp_import_error = _e_init
def _ensure_uwp_available():
global UWP_TTS_AVAILABLE
if UWP_TTS_AVAILABLE:
return True
try:
import importlib
# Try both ways
try:
importlib.import_module('winsdk')
except Exception:
pass
try:
from winsdk.windows.media.speechsynthesis import SpeechSynthesizer as _SS # noqa: F401
from winsdk.windows.storage.streams import DataReader as _DR # noqa: F401
UWP_TTS_AVAILABLE = True
except Exception:
from winsdk.windows.media.speechsynthesis import SpeechSynthesizer as _SS # type: ignore # noqa: F401
from winsdk.windows.storage.streams import DataReader as _DR # type: ignore # noqa: F401
UWP_TTS_AVAILABLE = True
except Exception as e:
UWP_TTS_AVAILABLE = False
try:
print(f"UWP import error: {e}")
except Exception:
pass
return UWP_TTS_AVAILABLE
# Simple stub functions to replace removed complex functions
def get_current_keyboard_layout():
"""Stub function - always returns None for simplicity"""
return None
def normalize_key_name(key_name, scan_code=None):
"""Stub function - returns key_name as-is for simplicity"""
return key_name
def is_special_character(key_name):
"""Check if a key name contains special characters that may cause issues"""
if not key_name:
return False
# Check for Nordic/Special characters that commonly cause issues
special_chars = ['å', 'ä', 'ö', '¨', '´', '`', '~', '^', '°', '§', '±', 'µ', '¶', '·', '¸', '¹', '²', '³']
# Check for any special characters in the key name
for char in special_chars:
if char in key_name:
return True
# Check for other potentially problematic characters
if any(ord(char) > 127 for char in key_name): # Non-ASCII characters
return True
return False
def suggest_alternative_key(special_char):
"""Suggest alternative keys for special characters"""
alternatives = {
'å': 'a',
'ä': 'a',
'ö': 'o',
'¨': 'u',
'´': "'",
'`': "'",
'~': '~',
'^': '^',
'°': 'o',
'§': 's',
'±': '=',
'µ': 'u',
'¶': 'p',
'·': '.',
'¸': ',',
'¹': '1',
'²': '2',
'³': '3'
}
return alternatives.get(special_char, None)
def detect_ctrl_keys():
"""
Detect which Ctrl keys are currently pressed using scan code detection.
Returns a tuple of (left_ctrl_pressed, right_ctrl_pressed).
This function provides more reliable left/right distinction than keyboard.is_pressed().
"""
left_ctrl_pressed = False
right_ctrl_pressed = False
try:
# Check if any Ctrl key is pressed first
if keyboard.is_pressed('ctrl'):
# Use scan code to determine which one
for event in keyboard._listener.pressed_events:
if hasattr(event, 'scan_code'):
if event.scan_code == 29: # Left Ctrl
left_ctrl_pressed = True
elif event.scan_code == 157: # Right Ctrl
right_ctrl_pressed = True
# Fallback: if scan code detection fails, assume left
if not left_ctrl_pressed and not right_ctrl_pressed:
left_ctrl_pressed = True
except Exception:
# Fallback to basic detection
if keyboard.is_pressed('ctrl'):
left_ctrl_pressed = True
return left_ctrl_pressed, right_ctrl_pressed
# Try to import tkinterdnd2 for drag and drop functionality
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
TKDND_AVAILABLE = True
except ImportError:
TKDND_AVAILABLE = False
print("Warning: tkinterdnd2 not available. Drag and drop functionality will be disabled.")
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2) # FIX DPI ON WINDOWS
except AttributeError:
ctypes.windll.user32.SetProcessDPIAware()
except Exception as e:
print(f"Warning: Could not set DPI awareness: {e}")
APP_VERSION = "0.8.6"
CHANGELOG = """
0.8.6:
New Features:
- Controller support added.
- More secure layout loading.
- Custom hotkey combinations (e.g., Shift + S).
UI Improvements:
- The Info/Help window now more clearly indicates whether OCR is installed.
- The Info/Help window now includes information on how to add more voices.
- Changed the area name dialog to be more user friendly.
- Added numbers to the voice selection dropdown.
Bug Fixes:
- Fixed a race condition that allowed multiple area selection windows to open at once.
Thanks to everyone who has sent in feedback and bug reports!
Thanks for using GameReader!
"""
# Create a StringIO buffer to capture print statements
log_buffer = io.StringIO()
# Redirect standard output to the StringIO buffer
sys.stdout = log_buffer
# --- Custom Hotkey Conflict Warning Dialog (No Symbol, Styled OK Button) ---
def show_thinkr_warning(game_reader, area_name):
# Disable all hotkeys when dialog is shown
try:
keyboard.unhook_all()
mouse.unhook_all()
except Exception as e:
print(f"Error disabling hotkeys for warning dialog: {e}")
win = tk.Toplevel(game_reader.root)
win.title("Hotkey Conflict Detected!")
win.geometry("370x170")
win.resizable(False, False)
win.grab_set()
win.transient(game_reader.root)
# Set the window icon
try:
icon_path = os.path.join(os.path.dirname(__file__), 'Assets', 'icon.ico')
if os.path.exists(icon_path):
win.iconbitmap(icon_path)
except Exception as e:
print(f"Error setting warning dialog icon: {e}")
# Center the dialog
win.update_idletasks()
x = game_reader.root.winfo_rootx() + game_reader.root.winfo_width() // 2 - 185
y = game_reader.root.winfo_rooty() + game_reader.root.winfo_height() // 2 - 85
win.geometry(f"370x170+{x}+{y}")
# Remove the warning icon (if any)
for child in win.winfo_children():
if isinstance(child, tk.Label) and child.cget("image"):
child.destroy()
# Add a message label
msg = tk.Label(win, text=f"This key is already used by area:\n'{area_name}'.\n\nPlease choose a different hotkey.", font=("Helvetica", 12), wraplength=340, justify="center")
msg.pack(pady=(28, 6))
# Add OK button
btn = tk.Button(win, text="OK", width=12, height=1, font=("Helvetica", 11, "bold"), relief="raised", bd=2)
btn.pack(pady=(6, 10))
# Focus the button for keyboard users
btn.focus_set()
# Bind Enter key to OK
win.bind("<Return>", lambda e: win.destroy())
# Disable all hotkeys while the dialog is open
try:
keyboard.unhook_all()
mouse.unhook_all()
except Exception as e:
print(f"Error disabling hotkeys: {e}")
# Restore hotkeys when dialog is closed
def on_close():
try:
game_reader.restore_all_hotkeys()
except Exception as e:
print(f"Error restoring hotkeys: {e}")
win.destroy()
win.protocol("WM_DELETE_WINDOW", on_close)
# Also patch the OK button and <Return> binding to use on_close
btn.config(command=on_close)
win.bind("<Return>", lambda e: on_close())
class ConsoleWindow:
def __init__(self, root, log_buffer, layout_file_var, latest_images, latest_area_name_var):
self.window = tk.Toplevel(root)
self.window.title("Debug Console")
# Set the window icon
try:
icon_path = os.path.join(os.path.dirname(__file__), 'Assets', 'icon.ico')
if os.path.exists(icon_path):
self.window.iconbitmap(icon_path)
except Exception as e:
print(f"Error setting console window icon: {e}")
self.latest_images = latest_images
self.window.geometry("690x500") # Initial size, will adjust based on image
# Create a top frame for controls
top_frame = tk.Frame(self.window)
top_frame.pack(fill='x', padx=10, pady=5)
# Add checkbox for image display
self.show_image_var = tk.BooleanVar(value=True)
self.image_checkbox = tk.Checkbutton(
top_frame,
text="Show last processed image",
variable=self.show_image_var,
command=self.update_image_display
)
self.image_checkbox.pack(side='left')
# Add scale dropdown
scale_frame = tk.Frame(top_frame)
scale_frame.pack(side='left', padx=10)
tk.Label(scale_frame, text="Scale:").pack(side='left')
self.scale_var = tk.StringVar(value="100")
scales = [str(i) for i in range(10, 101, 10)] # Creates ["10", "20", ..., "100"]
scale_menu = tk.OptionMenu(scale_frame, self.scale_var, *scales, command=self.update_image_display)
scale_menu.pack(side='left')
tk.Label(scale_frame, text="%").pack(side='left')
# Add Save Log button
save_log_button = tk.Button(top_frame, text="Save Log", command=self.save_log)
save_log_button.pack(side='left', padx=(10, 0))
# Add Clear Console button
clear_console_button = tk.Button(top_frame, text="Clear Console", command=self.clear_console)
clear_console_button.pack(side='left', padx=(10, 0))
# Add Save Image button
save_image_button = tk.Button(top_frame, text="Save Image", command=self.save_image)
save_image_button.pack(side='left', padx=(10, 0))
# Create a middle frame for image display
image_frame = tk.Frame(self.window)
image_frame.pack(fill='x', padx=10, pady=5)
# Add image label to the middle frame
self.image_label = tk.Label(image_frame)
self.image_label.pack(fill='x')
# Create a bottom frame for the log output
log_frame = tk.Frame(self.window)
log_frame.pack(fill='both', expand=True, padx=10, pady=5)
# Add text widget for log output
self.text_widget = tk.Text(log_frame)
self.text_widget.pack(fill='both', expand=True)
self.text_widget.config(state=tk.DISABLED)
# Enable mouse wheel scrolling for the debug log
def _on_mousewheel_debug(event):
self.text_widget.yview_scroll(int(-1 * (event.delta / 120)), 'units')
return "break"
def _bind_mousewheel_debug(event):
self.text_widget.bind_all('<MouseWheel>', _on_mousewheel_debug)
def _unbind_mousewheel_debug(event):
self.text_widget.unbind_all('<MouseWheel>')
self.text_widget.bind('<Enter>', _bind_mousewheel_debug)
self.text_widget.bind('<Leave>', _unbind_mousewheel_debug)
# Add right-click context menu
self.context_menu = tk.Menu(self.text_widget, tearoff=0)
self.context_menu.add_command(label="Copy", command=self.copy_selection)
self.context_menu.add_command(label="Select All", command=self.select_all)
self.text_widget.bind("<Button-3>", self.show_context_menu)
self.log_buffer = log_buffer
self.layout_file_var = layout_file_var
self.latest_area_name_var = latest_area_name_var
self.photo = None # Keep a reference to prevent garbage collection
# Add line limit constant
self.MAX_LINES = 250
self.update_console()
def show_context_menu(self, event):
"""Show the context menu at the mouse position."""
try:
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
def copy_selection(self):
"""Copy selected text to clipboard."""
try:
selected_text = self.text_widget.get("sel.first", "sel.last")
self.window.clipboard_clear()
self.window.clipboard_append(selected_text)
except tk.TclError:
pass # No text selected
def select_all(self):
"""Select all text in the widget."""
self.text_widget.tag_add("sel", "1.0", "end")
def update_image_display(self, *args):
if not self.window.winfo_exists():
return
area_name = self.latest_area_name_var.get()
if self.show_image_var.get() and area_name in self.latest_images:
image = self.latest_images[area_name]
# Clean up previous photo if it exists
if hasattr(self, 'photo'):
del self.photo
# Scale the image according to the selected percentage
scale_factor = int(self.scale_var.get()) / 100
if scale_factor != 1:
new_width = int(image.width * scale_factor)
new_height = int(image.height * scale_factor)
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Calculate new window height based on scaled image height
window_height = image.height + 300 # Add space for controls and log
window_height = max(500, window_height)
# Get current window position and width
window_x = self.window.winfo_x()
window_y = self.window.winfo_y()
window_width = self.window.winfo_width()
# Update window geometry
self.window.geometry(f"{window_width}x{window_height}+{window_x}+{window_y}")
self.photo = ImageTk.PhotoImage(image)
if self.image_label.winfo_exists():
self.image_label.config(image=self.photo)
else:
if self.image_label.winfo_exists():
self.image_label.config(image='')
if hasattr(self, 'photo'):
del self.photo
def update_console(self):
if not hasattr(self, 'text_widget') or not self.text_widget.winfo_exists():
return
self.text_widget.config(state=tk.NORMAL)
# Get all text and split into lines
text = self.log_buffer.getvalue()
lines = text.splitlines()
# Keep only the last MAX_LINES
if len(lines) > self.MAX_LINES:
# Join the last MAX_LINES with newlines
text = '\n'.join(lines[-self.MAX_LINES:]) + '\n'
# Update the buffer with truncated text
self.log_buffer.truncate(0)
self.log_buffer.seek(0)
self.log_buffer.write(text)
# Update the text widget
self.text_widget.delete(1.0, tk.END)
self.text_widget.insert(tk.END, text)
self.text_widget.config(state=tk.DISABLED)
self.text_widget.see(tk.END)
def write(self, message):
"""Write to the console window if it exists"""
if not self.window.winfo_exists():
return
self.log_buffer.write(message) # Write to the buffer
self.update_console() # Update the console window with line limit
if self.show_image_var.get(): # Update image if checkbox is checked
self.update_image_display()
def flush(self):
pass
def save_log(self):
# Get the current date and time
current_time = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# Get the name of the save file
save_file_name = self.layout_file_var.get().split('/')[-1].split('.')[0]
# Suggest a file name
suggested_name = f"Log_{save_file_name}_{current_time}.txt"
file_path = filedialog.asksaveasfilename(defaultextension=".txt", initialfile=suggested_name, filetypes=[("Text files", "*.txt")])
if file_path:
with open(file_path, 'w') as f:
f.write(self.log_buffer.getvalue())
print(f"Log saved to {file_path}\n--------------------------")
def save_image(self):
"""Save the currently displayed image"""
if not self.window.winfo_exists():
return
area_name = self.latest_area_name_var.get()
latest_image = self.latest_images.get(area_name) # Access the image for the current area
if not isinstance(latest_image, Image.Image):
messagebox.showerror("Error", "No image to save.")
return
current_time = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
suggested_name = f"{area_name}_{current_time}.png"
file_path = filedialog.asksaveasfilename(
defaultextension=".png",
initialfile=suggested_name,
filetypes=[("PNG files", "*.png")]
)
if file_path:
latest_image.save(file_path, "PNG")
print(f"Image saved to {file_path}\n--------------------------")
def clear_console(self):
"""Clear the console text widget and log buffer"""
if not self.window.winfo_exists():
return
# Clear the text widget
self.text_widget.config(state=tk.NORMAL)
self.text_widget.delete(1.0, tk.END)
self.text_widget.config(state=tk.DISABLED)
# Clear the log buffer
self.log_buffer.seek(0)
self.log_buffer.truncate(0)
# Add a confirmation message
print("Console cleared\n--------------------------")
class ImageProcessingWindow:
def __init__(self, root, area_name, latest_images, settings, game_text_reader):
self.window = tk.Toplevel(root)
self.window.title(f"Image Processing for: {area_name}")
# Set the window icon
try:
icon_path = os.path.join(os.path.dirname(__file__), 'Assets', 'icon.ico')
if os.path.exists(icon_path):
self.window.iconbitmap(icon_path)
except Exception as e:
print(f"Error setting image processing window icon: {e}")
self.area_name = area_name
self.latest_images = latest_images
self.settings = settings
self.game_text_reader = game_text_reader
# Set up protocol to re-enable hotkeys when window closes
self.window.protocol("WM_DELETE_WINDOW", self.on_close)
# Check if there is an image for the area
if area_name not in latest_images:
messagebox.showerror("Error", "No image to process, generate an image by pressing the hotkey.")
self.window.destroy()
return
self.image = latest_images[area_name]
self.processed_image = self.image.copy()
# Add note about hotkeys being disabled
hotkey_note = ttk.Label(self.window, text="Note: Hotkeys (including controller hotkeys) are disabled while this window is open.",
font=("Helvetica", 10, "bold"), foreground='#666666')
hotkey_note.grid(row=0, column=0, columnspan=5, padx=10, pady=(10, 5), sticky='w')
# Disable hotkeys when this window opens
self.game_text_reader.disable_all_hotkeys()
# Create a canvas to display the image
self.image_frame = ttk.Frame(self.window)
self.image_frame.grid(row=1, column=0, columnspan=5, padx=10, pady=5)
self.canvas = tk.Canvas(self.image_frame, width=self.image.width, height=self.image.height)
self.canvas.pack()
# Display the image on the canvas
self.photo_image = ImageTk.PhotoImage(self.image)
self.image_on_canvas = self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo_image)
# Add a label under the image with larger text - centered
info_text = f"Showing previous image captured in area: {area_name}\n\nProcessing applies to unprocessed images; results may differ if the preview is already processed."
info_label = ttk.Label(self.image_frame, text=info_text, font=("Helvetica", 12), justify='center')
info_label.pack(pady=(10, 0), fill='x')
# Create a frame for bottom controls
control_frame = ttk.Frame(self.window)
control_frame.grid(row=2, column=0, columnspan=5, pady=10)
# Add scale dropdown
scale_frame = ttk.Frame(control_frame)
scale_frame.pack(side='left', padx=10)
ttk.Label(scale_frame, text="Preview Scale:").pack(side='left')
self.scale_var = tk.StringVar(value="100")
scales = [str(i) for i in range(10, 101, 10)]
scale_menu = tk.OptionMenu(scale_frame, self.scale_var, *scales, command=self.update_preview)
scale_menu.pack(side='left')
ttk.Label(scale_frame, text="%").pack(side='left')
# Add buttons
ttk.Button(control_frame, text="Apply img. processing", command=self.save_settings).pack(side='left', padx=10)
ttk.Button(control_frame, text="Reset to default", command=self.reset_all).pack(side='left', padx=10)
# Add sliders for image processing
self.brightness_var = tk.DoubleVar(value=settings.get('brightness', 1.0))
self.contrast_var = tk.DoubleVar(value=settings.get('contrast', 1.0))
self.saturation_var = tk.DoubleVar(value=settings.get('saturation', 1.0))
self.sharpness_var = tk.DoubleVar(value=settings.get('sharpness', 1.0))
self.blur_var = tk.DoubleVar(value=settings.get('blur', 0.0))
self.threshold_var = tk.IntVar(value=settings.get('threshold', 128))
self.hue_var = tk.DoubleVar(value=settings.get('hue', 0.0))
self.exposure_var = tk.DoubleVar(value=settings.get('exposure', 1.0))
self.threshold_enabled_var = tk.BooleanVar(value=settings.get('threshold_enabled', False))
self.create_slider("Brightness", self.brightness_var, 0.1, 2.0, 1.0, 3, 0)
self.create_slider("Contrast", self.contrast_var, 0.1, 2.0, 1.0, 3, 1)
self.create_slider("Saturation", self.saturation_var, 0.1, 2.0, 1.0, 3, 2)
self.create_slider("Sharpness", self.sharpness_var, 0.1, 2.0, 1.0, 3, 3)
self.create_slider("Blur", self.blur_var, 0.0, 10.0, 0.0, 3, 4)
self.create_slider("Threshold", self.threshold_var, 0, 255, 128, 4, 0, self.threshold_enabled_var)
self.create_slider("Hue", self.hue_var, -1.0, 1.0, 0.0, 4, 1)
self.create_slider("Exposure", self.exposure_var, 0.1, 2.0, 1.0, 4, 2)
def create_slider(self, label, variable, from_, to, initial, row, col, enabled_var=None):
frame = ttk.Frame(self.window)
frame.grid(row=row, column=col, padx=10, pady=5)
# Use a label frame for consistent structure
label_frame = ttk.LabelFrame(frame, text=label)
label_frame.pack(fill='both', expand=True)
ttk.Label(label_frame, text=label).pack()
entry_var = tk.StringVar(value=f'{initial:.2f}')
# Add trace to variable to update entry field
variable.trace_add('write', lambda *args: entry_var.set(f'{variable.get():.2f}'))
slider = ttk.Scale(label_frame, from_=from_, to=to, orient='horizontal', variable=variable, command=self.update_image)
slider.set(initial)
slider.pack()
# Create entry with context menu
entry = ttk.Entry(label_frame, textvariable=entry_var)
entry.pack()
# Add context menu for copy/paste
entry_menu = tk.Menu(entry, tearoff=0)
entry_menu.add_command(label="Cut", command=lambda: entry.event_generate('<<Cut>>'))
entry_menu.add_command(label="Copy", command=lambda: entry.event_generate('<<Copy>>'))
entry_menu.add_command(label="Paste", command=lambda: entry.event_generate('<<Paste>>'))
entry_menu.add_separator()
entry_menu.add_command(label="Select All", command=lambda: entry.selection_range(0, 'end'))
def show_entry_menu(event):
entry_menu.post(event.x_root, event.y_root)
entry.bind('<Button-3>', show_entry_menu)
ttk.Button(label_frame, text="Reset", command=lambda: self.reset_slider(slider, entry, initial, variable)).pack()
# Create checkbox for threshold slider
if label == "Threshold":
checkbox_frame = ttk.Frame(label_frame)
checkbox_frame.pack(anchor='w')
checkbox = ttk.Checkbutton(checkbox_frame, variable=enabled_var, command=self.update_image)
checkbox.pack(side=tk.LEFT)
ttk.Label(checkbox_frame, text="Enabled").pack(side=tk.LEFT, padx=(5, 0))
setattr(self, f"{label.lower()}_slider", frame)
frame.slider, frame.entry = slider, entry
def reset_slider(self, slider, entry, initial, variable):
slider.set(initial)
variable.set(initial)
entry.delete(0, tk.END)
entry.insert(0, str(round(float(initial), 2)))
self.update_image()
def reset_all(self):
self.brightness_var.set(1.0)
self.contrast_var.set(1.0)
self.saturation_var.set(1.0)
self.sharpness_var.set(1.0)
self.blur_var.set(0.0)
self.threshold_var.set(128)
self.hue_var.set(0.0)
self.exposure_var.set(1.0)
self.threshold_enabled_var.set(False)
self.update_image()
def update_image(self, _=None):
if self.image:
# Clean up previous processed image if it exists
if self.processed_image:
self.processed_image.close()
self.processed_image = self.image.copy()
# Apply brightness
enhancer = ImageEnhance.Brightness(self.processed_image)
self.processed_image = enhancer.enhance(self.brightness_var.get())
# Apply contrast
enhancer = ImageEnhance.Contrast(self.processed_image)
self.processed_image = enhancer.enhance(self.contrast_var.get())
# Apply saturation
enhancer = ImageEnhance.Color(self.processed_image)
self.processed_image = enhancer.enhance(self.saturation_var.get())
# Apply sharpness
enhancer = ImageEnhance.Sharpness(self.processed_image)
self.processed_image = enhancer.enhance(self.sharpness_var.get())
# Apply blur
if self.blur_var.get() > 0:
self.processed_image = self.processed_image.filter(ImageFilter.GaussianBlur(self.blur_var.get()))
# Apply threshold if enabled
if self.threshold_enabled_var.get():
self.processed_image = self.processed_image.point(lambda p: p > self.threshold_var.get() and 255)
# Apply hue (simplified, for demonstration purposes)
self.processed_image = self.processed_image.convert('HSV')
channels = list(self.processed_image.split())
channels[0] = channels[0].point(lambda p: (p + int(self.hue_var.get() * 255)) % 256)
self.processed_image = Image.merge('HSV', channels).convert('RGB')
# Apply exposure (simplified, for demonstration purposes)
enhancer = ImageEnhance.Brightness(self.processed_image)
self.processed_image = enhancer.enhance(self.exposure_var.get())
# Clean up previous photo_image if it exists
if self.photo_image:
del self.photo_image
self.photo_image = ImageTk.PhotoImage(self.processed_image)
self.canvas.itemconfig(self.image_on_canvas, image=self.photo_image)
def save_settings(self):
# First, update all settings in the processing_settings dictionary
self.settings['brightness'] = self.brightness_var.get()
self.settings['contrast'] = self.contrast_var.get()
self.settings['saturation'] = self.saturation_var.get()
self.settings['sharpness'] = self.sharpness_var.get()
self.settings['blur'] = self.blur_var.get()
self.settings['hue'] = self.hue_var.get()
self.settings['exposure'] = self.exposure_var.get()
if self.threshold_enabled_var.get():
self.settings['threshold'] = self.threshold_var.get()
else:
self.settings['threshold'] = None
self.settings['threshold_enabled'] = self.threshold_enabled_var.get()
# Ensure the settings are properly stored in the game_text_reader's processing_settings
area_name = self.area_name
self.game_text_reader.processing_settings[area_name] = self.settings.copy()
# Save Auto Read settings to file immediately
if area_name == "Auto Read":
import json
import os
import tempfile
# First save the processing settings
self.game_text_reader.processing_settings[area_name] = self.settings.copy()
# Call the existing save_auto_read_settings function to save all settings
# This will include hotkey, checkboxes, and other settings
if hasattr(self.game_text_reader, 'save_auto_read_settings'):
# Get a reference to the save_auto_read_settings function
save_func = None
for area in self.game_text_reader.areas:
area_frame2, _, _, area_name_var2, _, _, _ = area
if area_name_var2.get() == "Auto Read":
# This is a bit of a hack - we're accessing the nested function through the frame's children
for child in area[0].winfo_children():
if hasattr(child, '_name') and child._name == 'save_auto_read_settings':
save_func = child
break
if save_func:
break
if save_func:
# Call the save function
save_func()
# Show feedback in status label if available
if hasattr(self.game_text_reader, 'status_label'):
self.game_text_reader.status_label.config(text="Auto Read settings saved")
if hasattr(self.game_text_reader, '_feedback_timer') and self.game_text_reader._feedback_timer:
self.game_text_reader.root.after_cancel(self.game_text_reader._feedback_timer)
self.game_text_reader._feedback_timer = self.game_text_reader.root.after(2000,
lambda: self.game_text_reader.status_label.config(text=""))
# Find and enable the preprocess checkbox for this area
for area_frame, _, _, area_name_var, preprocess_var, _, _ in self.game_text_reader.areas:
if area_name_var.get() == area_name:
preprocess_var.set(True) # Enable the checkbox
break
# Check if this is the Auto Read area or if there's a layout file
is_auto_read = self.area_name == "Auto Read"
has_layout_file = bool(self.game_text_reader.layout_file.get())
if not has_layout_file and not is_auto_read:
# Create custom dialog for non-Auto Read areas without a layout file
dialog = tk.Toplevel(self.window)
dialog.title("No Save File")
dialog.geometry("400x150")
# Set the window icon
try:
icon_path = os.path.join(os.path.dirname(__file__), 'Assets', 'icon.ico')
if os.path.exists(icon_path):
dialog.iconbitmap(icon_path)
except Exception as e:
print(f"Error setting dialog icon: {e}")
dialog.transient(self.window) # Make dialog modal
dialog.grab_set() # Make dialog modal
# Center the dialog on the screen
dialog.geometry("+%d+%d" % (
self.window.winfo_rootx() + self.window.winfo_width()/2 - 200,
self.window.winfo_rooty() + self.window.winfo_height()/2 - 75))
# Add message
message = tk.Label(dialog,
text="No save file exists. You need to save the layout\nto preserve these settings.\n\nCreate save file now?",
pady=20)
message.pack()
# Add buttons frame
button_frame = tk.Frame(dialog)
button_frame.pack(pady=10)
# Create Yes button
def on_yes():
dialog.destroy()
self.game_text_reader.save_layout()
# Create No button
def on_no():
dialog.destroy()
return
yes_button = tk.Button(button_frame, text="Yes", command=on_yes, width=10)
yes_button.pack(side='left', padx=10)
no_button = tk.Button(button_frame, text="No", command=on_no, width=10)
no_button.pack(side='left', padx=10)
# Center the dialog on the screen
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry(f'{width}x{height}+{x}+{y}')
# Make the dialog modal
dialog.transient(self.window)
dialog.grab_set()
# Wait for dialog to close
self.window.wait_window(dialog)
# If we get here, the user closed the dialog without clicking a button
return
# Store a reference to game_text_reader before destroying window
game_text_reader = self.game_text_reader
# --- AUTO SAVE for Auto Read area ---
if area_name == "Auto Read":
import tempfile, os, json
# Try to get the preprocess, voice, and speed settings for Auto Read area
preprocess = None
voice = None
speed = None
for area_frame, _, _, area_name_var, preprocess_var, voice_var, speed_var in game_text_reader.areas:
if area_name_var.get() == area_name:
preprocess = preprocess_var.get() if hasattr(preprocess_var, 'get') else preprocess_var
voice = voice_var.get() if hasattr(voice_var, 'get') else voice_var
speed = speed_var.get() if hasattr(speed_var, 'get') else speed_var
break
# Find the hotkey for the Auto Read area
hotkey = None
for area_frame2, hotkey_button2, _, area_name_var2, _, _, _ in game_text_reader.areas:
if area_name_var2.get() == area_name:
hotkey = getattr(hotkey_button2, 'hotkey', None)
break
# Save to temp file
settings = {
'preprocess': preprocess,
'voice': voice,
'speed': speed,
'brightness': self.brightness_var.get(),
'contrast': self.contrast_var.get(),
'saturation': self.saturation_var.get(),
'sharpness': self.sharpness_var.get(),
'blur': self.blur_var.get(),
'hue': self.hue_var.get(),
'exposure': self.exposure_var.get(),
'threshold': self.threshold_var.get() if self.threshold_enabled_var.get() else None,
'threshold_enabled': self.threshold_enabled_var.get(),
'hotkey': hotkey,
'stop_read_on_select': getattr(game_text_reader, 'interrupt_on_new_scan_var', tk.BooleanVar(value=True)).get() if hasattr(game_text_reader, 'interrupt_on_new_scan_var') else True,
}
temp_path = os.path.join(tempfile.gettempdir(), 'auto_read_settings.json')
with open(temp_path, 'w') as f:
json.dump(settings, f)
# Show status message if available
if hasattr(game_text_reader, 'status_label'):
game_text_reader.status_label.config(text="Auto Read area settings saved (auto)")
if hasattr(game_text_reader, '_feedback_timer') and game_text_reader._feedback_timer:
game_text_reader.root.after_cancel(game_text_reader._feedback_timer)
game_text_reader._feedback_timer = game_text_reader.root.after(2000, lambda: game_text_reader.status_label.config(text=""))
# Destroy window (if not already destroyed)
self.window.destroy()
return
# For all other areas, continue with manual/dialog save logic
# Destroy window
self.window.destroy()
# Now that everything is properly synchronized, save the layout
game_text_reader.save_layout()
def update_preview(self, *args):
"""Update the preview with current settings and scale"""
# Apply current processing settings
self.processed_image = preprocess_image(
self.image,
brightness=self.brightness_var.get(),
contrast=self.contrast_var.get(),
saturation=self.saturation_var.get(),
sharpness=self.sharpness_var.get(),
blur=self.blur_var.get(),
threshold=self.threshold_var.get() if self.threshold_enabled_var.get() else None,
hue=self.hue_var.get(),
exposure=self.exposure_var.get()
)
# Scale the image according to the selected percentage
scale_factor = int(self.scale_var.get()) / 100
if scale_factor != 1:
new_width = int(self.processed_image.width * scale_factor)
new_height = int(self.processed_image.height * scale_factor)
display_image = self.processed_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
else:
display_image = self.processed_image
# Update the canvas size
self.canvas.config(width=display_image.width, height=display_image.height)
# Update the displayed image
self.photo_image = ImageTk.PhotoImage(display_image)
self.canvas.itemconfig(self.image_on_canvas, image=self.photo_image)
def on_close(self):
"""Re-enable hotkeys when the window is closed"""
self.game_text_reader.restore_all_hotkeys()
self.window.destroy()
def preprocess_image(image, brightness=1.0, contrast=1.0, saturation=1.0, sharpness=1.0, blur=0.0, threshold=None, hue=0.0, exposure=1.0):
print("Preprocessing image...")
# Apply brightness