├── LICENSE.md
├── README.md
├── char.jpg
├── headers.txt
├── main.py
├── metin.jpg
├── stones.txt
├── uriel.jpg
└── windowcapture.py
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 FatihAvdan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Metin2 Opencv Metin Kesme Botu
2 |
3 | Opencv matchTemplate, Win32api kullanılarak yapılmıştır. Görsel tanıma ile çalışıp ekrandaki metinleri, karakterin konumunu bulur ve karaktere en yakın metnin üzerine tıklar. Uriel hile korumasını algılayıp otomatik geçer.
4 | 
5 |
6 |
7 |
8 | # Gerekli Düzenlemeler
9 |
10 | 1-Karakterinizin ismini char.jpg gibi ekran görüntüsü alıp kaydediniz.
11 | 2-Kesmek istediğiniz metnin ismini metin.jpg deki gibi ekran görüntüsü alıp kaydediniz.
12 | 3-headers.txt dosyasına main.py deki ListWindowNames fonksiyonu kullanarak yakalanacak ekranın ismini ekleyiniz. Birden fazla ekran ismi varsa alt alta ekleyiniz.
13 | 4-stone.txt dosyasına kesmek istediğiniz metinlerin ekran götüntülerinin isimlerini satır satır ekleyiniz. ( a.jpg , b.jpg gibi)
14 | 5-Başlatmak için "Start" , Test için "Test" , ekran isimleri için "listWindowNames" fonksiyonunu yorumdan çıkarınız.
15 |
16 |
17 | Kaynak:
18 | https://www.youtube.com/watch?v=KecMlLUuiE4&list=PL1m2M8LQlzfKtkKq2lK5xko4X-8EZzFPI
19 |
--------------------------------------------------------------------------------
/char.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FatihAvdan/metin2-opencv-bot/e784f84efd0fbb21d27eca00ac3f3372735a97ba/char.jpg
--------------------------------------------------------------------------------
/headers.txt:
--------------------------------------------------------------------------------
1 | Find window name with listallwindows function and paste like this
2 | Find window name with listallwindows function and paste like this
3 | Find window name with listallwindows function and paste like this
4 | Find window name with listallwindows function and paste like this
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from math import pi
2 | from operator import truediv
3 | from string import printable
4 | from windowcapture import WindowCapture
5 | import cv2 as cv
6 | import numpy as np
7 | import os
8 | import os.path
9 | import win32api, win32con
10 | from numpy.lib.shape_base import tile
11 | from PIL import ImageGrab
12 | from windowcapture import WindowCapture
13 | import keyboard
14 | import time
15 | from math import sqrt
16 | import random
17 | import win32gui, win32ui, win32con
18 |
19 |
20 | def euqli_dist(p, q, squared=False):
21 | # Calculates the euclidean distance, the "ordinary" distance between two
22 | # points
23 | #
24 | # The standard Euclidean distance can be squared in order to place
25 | # progressively greater weight on objects that are farther apart. This
26 | # frequently used in optimization problems in which distances only have
27 | # to be compared.
28 | if squared:
29 | return ((p[0] - q[0]) ** 2) + ((p[1] - q[1]) ** 2)
30 | else:
31 | return sqrt(((p[0] - q[0]) ** 2) + ((p[1] - q[1]) ** 2))
32 |
33 | def closest(cur_pos, positions):
34 | low_dist = float('inf')
35 | closest_pos = None
36 | for pos in positions:
37 | dist = euqli_dist(cur_pos,pos)
38 | if dist < low_dist:
39 | low_dist = dist
40 | closest_pos = pos
41 | return closest_pos
42 |
43 |
44 | os.chdir(os.path.dirname(os.path.abspath(__file__)))
45 |
46 | # List all windows headers for headers.txt
47 | def ListWindowNames():
48 | def winEnumHandler(hwnd, ctx):
49 | if win32gui.IsWindowVisible(hwnd):
50 | print(win32gui.GetWindowText(hwnd))
51 | win32gui.EnumWindows(winEnumHandler, None)
52 |
53 |
54 | def click(x,y):
55 | win32api.SetCursorPos((x,y))
56 | win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
57 | time.sleep(0.02) #This pauses the script for 0.02 seconds
58 | win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
59 |
60 |
61 | def findClickPositions(needle_img_path, haystack_img, threshold, debug_mode=None):
62 |
63 | # https://docs.opencv.org/4.2.0/d4/da8/group__imgcodecs.html
64 |
65 | needle_img = cv.imread(needle_img_path, cv.IMREAD_UNCHANGED)
66 | locations = []
67 | # Save the dimensions of the needle image
68 |
69 | needle_w = needle_img.shape[1]
70 | needle_h = needle_img.shape[0]
71 |
72 |
73 |
74 |
75 | # There are 6 methods to choose from:
76 | # TM_CCOEFF, TM_CCOEFF_NORMED, TM_CCORR, TM_CCORR_NORMED, TM_SQDIFF, TM_SQDIFF_NORMED
77 | method = cv.TM_CCOEFF_NORMED
78 | result = cv.matchTemplate(haystack_img, needle_img, method)
79 |
80 | # Get the all the positions from the match result that exceed our threshold
81 | locations = np.where(result >= threshold)
82 | locations = list(zip(*locations[::-1]))
83 | # print(locations)
84 |
85 | # You'll notice a lot of overlapping rectangles get drawn. We can eliminate those redundant
86 | # locations by using groupRectangles().
87 | # First we need to create the list of [x, y, w, h] rectangles
88 | rectangles = []
89 | for loc in locations:
90 | rect = [int(loc[0]), int(loc[1]), needle_w, needle_h]
91 | # Add every box to the list twice in order to retain single (non-overlapping) boxes
92 | rectangles.append(rect)
93 | rectangles.append(rect)
94 | # Apply group rectangles.
95 | # The groupThreshold parameter should usually be 1. If you put it at 0 then no grouping is
96 | # done. If you put it at 2 then an object needs at least 3 overlapping rectangles to appear
97 | # in the result. I've set eps to 0.5, which is:
98 | # "Relative difference between sides of the rectangles to merge them into a group."
99 | rectangles, weights = cv.groupRectangles(rectangles, groupThreshold=1, eps=0.5)
100 | #print(rectangles)
101 |
102 | points = []
103 | if len(rectangles):
104 | #print('Found needle.')
105 |
106 | line_color = (0, 255, 0)
107 | line_type = cv.LINE_4
108 | marker_color = (255, 0, 255)
109 | marker_type = cv.MARKER_CROSS
110 |
111 | # Loop over all the rectangles
112 | for (x, y, w, h) in rectangles:
113 |
114 | # Determine the center position
115 | center_x = x + int(w/2)
116 | center_y = y + int(h/2)
117 | # Save the points
118 | points.append((center_x, center_y))
119 |
120 | if debug_mode == 'rectangles':
121 | # Determine the box position
122 | top_left = (x, y)
123 | bottom_right = (x + w, y + h)
124 | # Draw the box
125 | cv.rectangle(haystack_img, top_left, bottom_right, color=line_color,
126 | lineType=line_type, thickness=2)
127 | elif debug_mode == 'points':
128 | # Draw the center point
129 | cv.drawMarker(haystack_img, (center_x, center_y),
130 | color=marker_color, markerType=marker_type,
131 | markerSize=20, thickness=2)
132 |
133 | #if debug_mode:
134 | # cv.imshow('Matches', haystack_img)
135 | # cv.waitKey()
136 | # cv.imwrite('result_click_point.jpg', haystack_img)
137 |
138 | return points
139 |
140 |
141 | def tryAllHeaders():
142 | file = open("headers.txt","r")
143 | headers = []
144 | for header in file:
145 | clearheader = header.replace('\n','')
146 | headers.append(clearheader)
147 | # print(headers)
148 | for header in headers:
149 | try:
150 | wincap = WindowCapture(header)
151 | return header
152 | break
153 | except:
154 | continue
155 |
156 | def findCorrectStone(wincap,header,stoneTreshold):
157 | file = open("stones.txt","r")
158 | stones = []
159 | for stone in file:
160 | clearstone = stone.replace('\n','')
161 | stones.append(clearstone)
162 | for stone in stones:
163 | screen = wincap.get_screenshot()
164 | points = findClickPositions(stone, screen,stoneTreshold, debug_mode='points' )
165 | if (len(points)>0):
166 | return stone
167 | break
168 |
169 | def urielPass(Uriel,missingWindowHeight):
170 | urielX = Uriel[0][0]
171 | urielY = Uriel[0][1]+missingWindowHeight
172 | urielPossibleCords=[(urielX,urielY+100),(urielX+100,urielY+100),(urielX+250,urielY+100),
173 | (urielX,urielY+200),(urielX+100,urielY+200),(urielX+250,urielY+200)]
174 | randomSelect = random.choice(urielPossibleCords)
175 | click(randomSelect[0],randomSelect[1])
176 | time.sleep(1)
177 |
178 | def Start():
179 | char = "char.jpg"
180 | uriel = 'uriel.jpg'
181 | i = 0
182 | missingWindowHeight = 110
183 | stoneTreshold = 0.6
184 | playerTreshold = 0.6
185 | urielTreshold = 0.6
186 | pause = True
187 | while keyboard.is_pressed('end') == False:
188 | if (keyboard.is_pressed('f12') == True):
189 | pause = True
190 | print("Bot Started")
191 | time.sleep(1)
192 |
193 | if pause !=True:
194 | time.sleep(1)
195 |
196 | if pause:
197 | # Try All Headers
198 | header = tryAllHeaders()
199 | wincap = WindowCapture(header)
200 |
201 | # Find Correct Stone
202 | correctStone = findCorrectStone(wincap,header,stoneTreshold)
203 |
204 | while pause:
205 |
206 | if (keyboard.is_pressed('f12') == True):
207 | pause = False
208 | print("Bot Stopped")
209 | time.sleep(1)
210 | break
211 |
212 | if (keyboard.is_pressed('end') == True):
213 | break
214 |
215 | try:
216 |
217 | screen = wincap.get_screenshot()
218 |
219 | StonePoints = findClickPositions(correctStone, screen,stoneTreshold)
220 | if(i%10 ==0):
221 | playerLocation = findClickPositions(char, screen,playerTreshold)
222 | if(i%100 ==0):
223 | Uriel = findClickPositions(uriel, screen,urielTreshold)
224 | if(len(Uriel)>0):
225 | isActive = True
226 | while isActive:
227 | screen = wincap.get_screenshot()
228 | Uriel = findClickPositions(uriel, screen,urielTreshold)
229 | if (len(Uriel)>0):
230 | urielPass(Uriel,missingWindowHeight)
231 | else:
232 | isActive=False
233 |
234 | stoneX = StonePoints[0][0]
235 | stoneY = StonePoints[0][1]+missingWindowHeight
236 |
237 | playerX = playerLocation[0][0]
238 | playerY = playerLocation[0][1]+missingWindowHeight
239 |
240 | if(i%20 ==0):
241 | print("I'm Going To The Stone Nearest To You")
242 |
243 |
244 | closestStone = []
245 | closestStone.append(closest((playerX, playerY), StonePoints))
246 |
247 | closestStoneX = closestStone[0][0]
248 | closestStoneY = closestStone[0][1]+missingWindowHeight
249 |
250 | click(closestStoneX,closestStoneY)
251 |
252 | i = i+1
253 |
254 | except:
255 | i = i+1
256 | Uriel =[]
257 | if(i%100 ==0):
258 | Uriel = findClickPositions(uriel, screen,urielTreshold, debug_mode='rectangles')
259 | if(len(Uriel)>0):
260 | isActive = True
261 | while isActive:
262 | screen = wincap.get_screenshot()
263 | Uriel = findClickPositions(uriel, screen,urielTreshold, debug_mode='rectangles')
264 | if (len(Uriel)>0):
265 | urielPass(Uriel,missingWindowHeight)
266 | else:
267 | isActive=False
268 |
269 | StonePoints = findClickPositions(correctStone, screen,stoneTreshold, debug_mode='rectangles' )
270 | playerLocation = findClickPositions(char, screen,playerTreshold, debug_mode='points')
271 | if(len(StonePoints)>0):
272 | stoneX = StonePoints[0][0]
273 | stoneY = StonePoints[0][1]+missingWindowHeight
274 | click(stoneX,stoneY)
275 | else:
276 | click(500,500)
277 |
278 | if(len(playerLocation)==0):
279 | print('Cant find Character')
280 | if(len(StonePoints)==0):
281 | print('Cant Find Stone')
282 |
283 |
284 | def Test():
285 | char = "char.jpg"
286 | i = 0
287 | missingWindowHeight = 110
288 | stoneTreshold = 0.6
289 | playerTreshold = 0.6
290 |
291 | #Try All headers
292 | header = tryAllHeaders()
293 | wincap = WindowCapture(header)
294 | # Find Correct Stone
295 | correctStone = findCorrectStone(wincap,header,stoneTreshold)
296 | loop_time = time.time()
297 | while keyboard.is_pressed('end') == False:
298 | try:
299 | # FPS printer
300 | fps = format(1 / (time.time() - loop_time))
301 | loop_time = time.time()
302 | print(fps)
303 |
304 | screen = wincap.get_screenshot()
305 |
306 | StonePoints = findClickPositions(correctStone, screen, stoneTreshold, debug_mode='rectangles')
307 | playerLocation = findClickPositions(char, screen, playerTreshold, debug_mode='points')
308 | cv.imshow("Points",screen)
309 | cv.waitKey(5)
310 | i+=1
311 |
312 | except:
313 | print("Cant find stone or character")
314 |
315 |
316 | # ListWindowNames()
317 | # Start()
318 | Test()
319 |
320 |
321 |
322 |
323 |
324 |
325 |
--------------------------------------------------------------------------------
/metin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FatihAvdan/metin2-opencv-bot/e784f84efd0fbb21d27eca00ac3f3372735a97ba/metin.jpg
--------------------------------------------------------------------------------
/stones.txt:
--------------------------------------------------------------------------------
1 | metin.jpg
--------------------------------------------------------------------------------
/uriel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FatihAvdan/metin2-opencv-bot/e784f84efd0fbb21d27eca00ac3f3372735a97ba/uriel.jpg
--------------------------------------------------------------------------------
/windowcapture.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import win32gui, win32ui, win32con
3 |
4 |
5 |
6 | class WindowCapture:
7 |
8 | # properties
9 | w = 0
10 | h = 0
11 | hwnd = None
12 | cropped_x = 0
13 | cropped_y = 0
14 | offset_x = 0
15 | offset_y = 0
16 |
17 | # constructor
18 | def __init__(self, window_name):
19 | # find the handle for the window we want to capture
20 | self.hwnd = win32gui.FindWindow(None, window_name)
21 | if not self.hwnd:
22 | raise Exception('Window not found: {}'.format(window_name))
23 |
24 | # get the window size
25 | window_rect = win32gui.GetWindowRect(self.hwnd)
26 | self.w = window_rect[2] - window_rect[0]-150
27 | self.h = window_rect[3] - window_rect[1]-55
28 |
29 | # account for the window border and titlebar and cut them off
30 | border_pixels = 1
31 | titlebar_pixels = 100
32 | self.w = self.w - (border_pixels * 2)
33 | self.h = self.h - titlebar_pixels - border_pixels
34 | self.cropped_x = border_pixels
35 | self.cropped_y = titlebar_pixels
36 |
37 | # set the cropped coordinates offset so we can translate screenshot
38 | # images into actual screen positions
39 | self.offset_x = window_rect[0] + self.cropped_x
40 | self.offset_y = window_rect[1] + self.cropped_y
41 |
42 | def get_screenshot(self):
43 |
44 | # get the window image data
45 | wDC = win32gui.GetWindowDC(self.hwnd)
46 | dcObj = win32ui.CreateDCFromHandle(wDC)
47 | cDC = dcObj.CreateCompatibleDC()
48 | dataBitMap = win32ui.CreateBitmap()
49 | dataBitMap.CreateCompatibleBitmap(dcObj, self.w, self.h)
50 | cDC.SelectObject(dataBitMap)
51 | cDC.BitBlt((0, 0), (self.w, self.h), dcObj, (self.cropped_x, self.cropped_y), win32con.SRCCOPY)
52 |
53 | # convert the raw data into a format opencv can read
54 | #dataBitMap.SaveBitmapFile(cDC, 'debug.bmp')
55 | signedIntsArray = dataBitMap.GetBitmapBits(True)
56 | img = np.fromstring(signedIntsArray, dtype='uint8')
57 | img.shape = (self.h, self.w, 4)
58 |
59 | # free resources
60 | dcObj.DeleteDC()
61 | cDC.DeleteDC()
62 | win32gui.ReleaseDC(self.hwnd, wDC)
63 | win32gui.DeleteObject(dataBitMap.GetHandle())
64 |
65 | # drop the alpha channel, or cv.matchTemplate() will throw an error like:
66 | # error: (-215:Assertion failed) (depth == CV_8U || depth == CV_32F) && type == _templ.type()
67 | # && _img.dims() <= 2 in function 'cv::matchTemplate'
68 | img = img[...,:3]
69 |
70 | # make image C_CONTIGUOUS to avoid errors that look like:
71 | # File ... in draw_rectangles
72 | # TypeError: an integer is required (got type tuple)
73 | # see the discussion here:
74 | # https://github.com/opencv/opencv/issues/14866#issuecomment-580207109
75 | img = np.ascontiguousarray(img)
76 |
77 | return img
78 |
79 | # find the name of the window you're interested in.
80 | # once you have it, update window_capture()
81 | # https://stackoverflow.com/questions/55547940/how-to-get-a-list-of-the-name-of-every-open-window
82 | def list_window_names(self):
83 | def winEnumHandler(hwnd, ctx):
84 | if win32gui.IsWindowVisible(hwnd):
85 | print(hex(hwnd), win32gui.GetWindowText(hwnd))
86 | win32gui.EnumWindows(winEnumHandler, None)
87 |
88 | # translate a pixel position on a screenshot image to a pixel position on the screen.
89 | # pos = (x, y)
90 | # WARNING: if you move the window being captured after execution is started, this will
91 | # return incorrect coordinates, because the window position is only calculated in
92 | # the __init__ constructor.
93 | def get_screen_position(self, pos):
94 | return (pos[0] + self.offset_x, pos[1] + self.offset_y)
95 |
--------------------------------------------------------------------------------