Python 으로 만든 Record And Play 매크로


* 경고 *
해당 포스팅은 교육적인 목적입니다. 절대 악용, 남용 하면 아니되옵니다.

 

 

안녕하세요 라이프온룸 입니다. ㅎㅎ

오늘은 시중에 나와있는 매크로 프로그램의 대부분이 가지고 있는 기능인 Record And Play 기능을 Python으로 구현해 볼게요. Record And Play 기능 말그대로 사람의 마우스 키보드 입력을 녹화 후 플래이 하는 것입니다. 게임을 하다보면 반복적인 일이 많죠 ? 로스트아크 경우에는 생활도구를 만든다거나 할때 처럼요. 이런 일 들을 쉽게 처리 할 수 있죠.

근데 들어가기 전에 대부분의 매크로 프로그램이 가지고 있는 기능을 왜 개발하느냐 라는 매우 합리적인 의문이 있으실 수 있을거에요 ㅎㅎㅎ

일단 첫번째 이유는 공부 목적이구요 ! 두번 째 이유는 싸인코드라는 게임 전문 보안프로그램 때문입니다. 

유명한 매크로 프로그램을 실행시키고 로스트아크를 실행하면 아마 아래 아저씨를 만나면서 게임이 종료 될 것입니다. 

싸인코드라는 보안 프로그램이 컴퓨터에 돌고 있는 프로그램을 검사하여 비인가 프로그램이라고 판단하는 경우에는 게임을 종료 시켜 버리는 것 같습니다. 뿐만 아니라 win32api 호출 패턴, 소프트웨어, 하드웨어 메크로를 탐지할 수 있다고 나와 있네요 !! 자세한 내용은 아래 링크를 참조해 주세요 

https://www.wellbia.com/home/ko/pages/xigncode3/

싸인코드는 로스트아크 뿐 아니라 국내 많은 게임이 채택하고 있습니다. 그래서 이름 좀 있는 매크로 프로그램은 게임중에 사용이 힘들겠죠 ? 물론 우회하는 방법이 있지만 패치가 되면서 그 방법이 막힐 수도 있고 뭔가 불이익을 받을 수도 있겠죠 ? (이 부분은 순도 100% 뇌피셜입니다… 불이익이 전혀 없을 수도 있고 패치로도 막지 못할 수도 있어요 ㅎㅎ)

여튼 파이썬은 비인가 프로그램이 아니기 때문에 파이썬 으로 만든 Record And Play 매크로는 싸인코드가 돌아가는 게임에서도 잘 돌아갑니다. 그럼 한번 만들어 보죠 ㅎㅎ

1. 사용법 

먼저 만들어진 프로그램의 사용법을 보고 코드를 소개할게요 !

pycharm에서 코드를 실행시키면 아래와 같이 나옵니다. 

그럼 일단 입력하기 전에 macroFile 이라는 폴더를 해당 코드가 있는 폴더에 만들어 주세요 

이 폴더에 마우스와 키보드의 동작을 녹화한 Pickle 파일이 들어가게 됩니다. 

그 다음 1번을 타이핑하면 “press middle btn to start recording” 이라고 나옵니다. 이 때 마우스 휠 버튼을 누르면 녹화가 시작됩니다. 그리고 다시 한번 누르면 녹화가 끝나구요 

녹화가 끝나면 2번을 눌러 봅시다. 그럼 아래 그림과 같이 새로운 Macro 파일이 생성된 것을 보실 수 있을 거에요 이 파일 이름은 프로그램이 정해주는 것으로 수동으로 바꾸셔도 됩니다. 

여튼 아래 상태에서 0번을 누르면 몇 회를 실행 시킬지 물어봅니다. 

아래 처럼 1번을 입력하면 1번 만 Play 됩니다. 그리고 다시 처음 상태로 돌아가게 되죠 ! 그리고 메크로 Play 도 중 F12 키를 누르면 메크로가 종료 됩니다. 

아마 단축키나 한영키는 동작 안 할 건데요 .. 그래도 대부분의 키는 잘 될겁니다. ! 마우스도 정확히 돌구요 ㅎㅎ 

근데 주의 하실점이 있어요 !

저는 4K 모니터를 쓰는데 그냥쓰면 너무 작아서 .. 아래처럼 배율을 150% 정도 주고 사용하고 있습니다. 근데 이렇게 하고 윈도우 바탕화면에서 테스트를 하면 마우스 움직임이 녹화 했을 때랑 다를거에요 .. 아래 배율을 100% 해야 정확하게 움직입니다. 

참고로 게임상에서는 게임이 전체화면 모드 일 시 윈도우 배율과 상관 없이 동작 할겁니다. !

2. 라이브러리 설치 

사실 전부 언급한 라이브러리지만 다시 써보죠 아래 라이브러리를 설치해 주세요

pip install keyboard
pip install mouse
pip install pywin32

그리고 추가적으로 pickle 라이브러리를 사용할 것입니다. 요거는 아마 따로 설치 않해도 될거에요 !!!

pickle 에 대해 간단히 설명 드리자면 Python의 모든 자료형(list, dict, DataFrame) 등을 파일로 만들어 주는 라이브러리라고 생각하면 됩니다. !

 

3. 코드

코드입니다. 좀 길죠 ㅎㅎㅎ 

import mouse as mo
import keyboard as key
import time
import win32api
import pickle
import os
import queue

# 이 파일이 있는 디렉토리의 절대 경로
CUR_PATH = os.path.dirname(os.path.realpath(__file__))
# pickle 파일이 저장되는 장소
MACRO_FOLDER_PATH = CUR_PATH + '\\' + 'macroFile'

# 마우스 버튼에 따른 상태를 Return 하는 함수
def checkMouseState(key):
    val = win32api.GetKeyState(key)
    #btnType = ['left', 'right']
    return 'down' if val < 0 else ''

def inputFunc(string, val = None):
    if val == None:
        try:
            val = int(input(string))
            return val
        except:
            return 0
    else:
        print(string)
        return val


# 메뉴를 만드는 함수 Input Arg를 통해 메뉴선택 없이 매크로 Run 가능
def makeManu(choose=None, MacroFileIdx=None, playtime=None):
    menue = "---------------------\n"
    menue += "record (1) \n"
    menue += "play (2) \n"
    menue += "---------------------\n"
    menue += "\ninput : "
    val = inputFunc(menue, choose)

    if val == 1:
        return [val]

    elif val == 2:
        menue ='------Select------\n'
        dictMacroName = {}

        print('\n\n')
        for idx, f in enumerate(os.listdir(MACRO_FOLDER_PATH)):
            if os.path.isfile(MACRO_FOLDER_PATH + '\\' + f):
                if f.find('.pickle') > 0:
                    dictMacroName[idx] = f
                    menue += '\t' + str(idx) + '. ' + f + '\n'

        menue += '\ninput : '
        opt = inputFunc(menue, MacroFileIdx)
        playcnt = inputFunc("Input Play Time : ", playtime)
        playcnt = playcnt if playcnt > 0 else 1
        try:
            print(MACRO_FOLDER_PATH)
            macroName = MACRO_FOLDER_PATH + '\\' + dictMacroName[opt]
            return [val, macroName, playcnt]
        except:
            return [0]

    else:
        return [0]

# Record 한 매크로 파일에 이름을 부여하는 코드
def makeNewMacroFileName():
    haveInt = []

    for idx, f in enumerate(os.listdir(MACRO_FOLDER_PATH)):
        if os.path.isfile(MACRO_FOLDER_PATH + '\\' + f):

            if (f.find('NewMacroFile') == 0) and (f.find('.pickle') > 0):

                start = len('NewMacroFile')
                end = f.find('.pickle')
                haveInt.append(int(f[start:end]))

    for idx in range(100):
        dupFlag =0
        for i in haveInt:
            if idx == i:
                dupFlag = 1
                break
        if dupFlag == 0:
            return 'NewMacroFile' + str(idx) + '.pickle'

# Record 함수
def record():

    recorded = queue.Queue()
    m_button_state = checkMouseState(0x04)  # middle button down = 0 or 1. Button up = -127 or -128
    macroStart = False

    print('press middle btn to start recording')
    while not macroStart:
        new_m_button_state = checkMouseState(0x04)
        if new_m_button_state != m_button_state:
            m_button_state = new_m_button_state

            if new_m_button_state == '':

                print('Macro Recording ...')
                macroStart = True

    # global mouse 이벤트를 후킹
    mo.hook(recorded.put)
    # global Keyboard 이벤트를 후킹
    keyHooked = key.hook(recorded.put)

    until = False
    while not until:
        new_m_button_state = checkMouseState(0x04)
        if new_m_button_state != m_button_state:
            m_button_state = new_m_button_state
            if new_m_button_state == '':
                print('Macro Recording End')
                until = True

    # 후킹 종료
    mo.unhook(recorded.put)
    key.unhook(keyHooked)

    #return_list = [mo_first_pos] + list(recorded.queue)
    return_list = list(recorded.queue)
    return return_list

# play 함수 speed_factor를 통해 실행 속도 조절 가능
def play(events, speed_factor=1.0, include_clicks=True, include_moves=True, include_wheel=True):

    state = key.stash_state()
    last_time = None
    for event in events:

        if speed_factor > 0 and last_time is not None:
            time.sleep((event.time - last_time) / speed_factor)
        last_time = event.time

        # F12 를 누르면 play중 종료
        val = key.is_pressed('F12')
        if val == True:
            key.restore_modifiers(state)
            return

        if isinstance(event, mo.ButtonEvent) and include_clicks:
            if event.event_type == mo.UP:
                mo._os_mouse.release(event.button)
            else:
                mo._os_mouse.press(event.button)
        elif isinstance(event, mo.MoveEvent) and include_moves:
            mo._os_mouse.move_to(event.x, event.y)
        elif isinstance(event, mo.WheelEvent) and include_wheel:
            mo._os_mouse.wheel(event.delta)
        else:
            valkey = event.name
            key.press(valkey) if event.event_type == key.KEY_DOWN else key.release(valkey)

    key.restore_modifiers(state)

# main 함수
def runRecoderAndPlayer(choose=None, MacroFileIdx=None, playtime=None):

    while True:
        returnFlag = False
        if choose != None:
            returnFlag = True
        val = makeManu(choose, MacroFileIdx, playtime)
        print(val)
        choose = val[0]

        # 키보드, 마우스 동작 녹화 후 Pickle 파일로 저장
        if choose == 1:
            f_name = makeNewMacroFileName()
            path = MACRO_FOLDER_PATH + '\\' + f_name
            event = record()
            with open(path, 'wb') as f:
                pickle.dump(event, f)

        # pickle 파일을 불러와 키보드, 마우스 동작 수행
        elif choose == 2:
            filename = val[1]
            playcnt = val[2]

            with open(filename, 'rb') as f:
                event = pickle.load(f)
            print(f)
            print(event)

            for i in range(playcnt):
                time.sleep(1)
                play(event)
        else:
            return

        if returnFlag:
            return
        choose = None

if __name__ == "__main__":
    runRecoderAndPlayer()
    # Record 되있는 메크로 가 있다면 아래 코드로 메뉴없이 한번만 호출 가능
    #runRecoderAndPlayer(2, 0, 1)

 

코드에서 가장 중요한 함수는 당연히 record and play 함수 입니다. 

record 함수에서는 사용자의 마우스 입력을 받아서 global 마우스 이벤트를 후킹하는 리스너를 등록합니다. 리스너가 등록 되면 hook 함수에 입력했던 Queue로 발생한 마우스 이벤트가 모두 들어가게 되죠. 사용자가 녹화를 마치면 리스너를 해지하고 Queue를 리스트화 한 뒤 이를 pickle 파일로 저장합니다. 

play 함수에서는 pickle 파일에서 읽어온 리스트를 가지고 이벤트 종류 별로 액션을 취하는데요 

아래 화면에서 보면 MoveEvent(x=-1770, y=920, time=…) …….. 이 보이시나요 ? record 시 썻던 데이터입니다. 이 리스트로 된 데이터를 하나하나 읽어서 마우스를 이동시키고, 키를 누르고 하는 거지요 !!!


네 오늘 포스팅은 여기까지 입니다. 혹시 안되시는 분 있으시거나 설명이 필요한 부분 있으시면 댓글로 달아주세요 ㅎㅎ 성심 성의것 답변 드릴게요 ㅎㅎㅎㅎㅎ 

아 추가로 게임 내에서 특정 버튼은 소프트웨어적 매크로로 안눌리는 것 들이 있더라고요 ㅜㅜ 하지만 돈 워리 나중에 하드웨어 메크로에 대해서도 포스팅을 해볼게요 ㅎㅎ

자 그럼 다음 포스팅에서 뵙도록 하죠 ㅎㅎ 해버 오섬 데이 ㅎㅎ

 

You may also like...

22 Responses

  1. 잘보고있어요 댓글:

    안녕하세요 질문이있습니당…
    녹화한 리스트의 최대 길이가 정해져있나요?
    녹화한 양이 길어지니깐 저장이 제대로 안되고 pickle 파일의 용량이 9kb 이렇더라구요..
    그리구 pickle 파일 여러개를 한개의 pickle 파일로도 만들수있나요?

    • 호그 댓글:

      음 ㅜㅜ 죄송합니다. 길게 녹화해 보지는 않았습니다. ㅜㅜ pickle 파일 여러개를 읽어서 한개의 pickle파일로 저장하는 코드를 짜면 되지 않을까 싶습니다만 ㅎㅎ

  2. 하모씨 댓글:

    질문 있습니다!!
    작성자님 소스보고 따로 만들어보려고 하는데 키입력받는게 윈도우창만되고 특정 프로그램켜서 안에서 입력한것들은 기록이 안되는데 무슨 따로 기법이 있나요?? Keyboard 모듈에 키기록하는 함수가 따로 있더군요 그걸 이용해서 했는데 안되서 질문드립니다.

  3. 하모씨 댓글:

    작성하신 글 보고 만들어보려고 했는데 특정프로그램에서는 입력을 안받고 윈도우창에서만 받더라구요 keyboard모듈에서 기록하는 함수가 있길래 이용해서 했는데 다른 방식이 있는건가요?

    • 호그 댓글:

      혹시 관리자 권한으로 실행 하셨나요 ? 마우스 클릭의 경우에는 특정 프로그램에 따라 버튼이 안눌리는 등 현상을 봤습니다. 아마 키보드도 안되는 경우가 있을 수 있을 것 같아요 !

      • 하모씨 댓글:

        작성자님껄로 하면 되는데 제가 만들걸로 하면 안되더군요.. 기록할때 hook이라는걸 쓰던데 그걸 써야될까요?

        • 호그 댓글:

          제가 hook을 썻던 이유는 마우스와 키보드를 동시에 Recording 하기 위함이 었습니다. 키보드만 한다면 Record, Play 함수 만으로도 잘 됬던것 같은데요 .. ㅜ 참고로 코드에서 쓰이는 hook 함수도 Record 함수에 포함되어 있습니다.

  4. 매크로 반복 간격이 3초정도로 설정되어 있는거 같은데
    이걸 1초로 바꿀려면 어떻게 해야하나요
    speed_factor을 수정하라고 하셨는데 수정해도 변함이 없는거 같아서
    질문드립니다.

    • 호그 댓글:

      for i in range(playcnt):
      time.sleep(1)
      play(event)

      여기서 sleep 값을 조절하면 될 겁니다. ㅎㅎ speed_factor는 이벤트 사이의 실행 시간 을 조절하는 명령입니다.
      정말 빠르게 돌리고 싶다면 time.sleep(1) 을 빼보시기 바랍니다.

  5. 궁금함다 댓글:

    record로 키&마우스 후킹해서 피클로 저장할 때 후킹한 시간도 같이 저장되는건가요?

    138~140번째 줄 보면서 저장된 피클 파일 봐도 시간은 안보여서 질문드립니다

  6. 갈릭 댓글:

    백그라운드 윈도우로 메세지를 쏘고 싶은데요
    혹시 특정 윈도우로만 키보드&마우스 이벤트를 발생 시킬수 있나요?

    • 호그 댓글:

      그게 가능 할 것 같은데 저도 정확히는 모르겠습니다. ㅜㅜ 저도 방법을 찾아보다가 그냥 이렇게 하지 라고 했던 것 같네요 ㅠㅜ

  7. 잘보고 있습니다 댓글:

    안녕하세요
    혹시 특정 윈도우에 키보드&마우스 메세지를 보낼수 있을까요?
    다른 게시물도 보았는데 최상위 윈도우에만 보내는건가 해서요
    창을 내려놓고 메세지만 보내고 싶어서요

    항상 감사합니다

    • 호그 댓글:

      이 부분은 저도 잘 모르겠습니다. ㅜㅜ 저번에 찾다가 그냥 이렇게 하지 하고 말았던 것 같아요 ㅜ

  8. 파이썬 눕 댓글:

    안녕하세용, 파이썬 이뮬레이터 게임 실험하면서 간간히 배우고있는 직딩입니다! 혹시 win32.com.client 으로 다른 특정 executable 윈도우에 실행할수있게끔 가능 못할까요? 여기저기 찾다, 이걸로 저가 원하는 특정환 윈도우로 옮겨지긴 하는데, 키보드나 마우스 입력이 인식이 안돼요 (i.e. 마우스는 그냥 그쪽으로 가기만하고 클릭을 해서 인식을 안함요 ㅠ…). 혹시 이걸로 변합해서 실행할수있을까요?

    import win32com.client as comclt
    import pyautogui as pag

    wsh= comclt.Dispatch(“WScript.Shell”)
    if wsh.AppActivate(“원하는 앱”): # 원하는 앱
    pag.click(“”)

  9. 궁금증 댓글:

    파이썬으로 한번 실행해보려고했는데, mouse 모듈 관련한 명령어는 전부 오류가 뜨네요
    인스톨은 했는데 왜이런걸까요 ㅠㅠ?

  10. 궁금증 댓글:

    파이썬으로 실행해보려고했는데, 마우스 모듈 관련 명령어들은 죄다 오류가 떠버리네요. 인스톨은 분명 했는데 왜그럴까요? ㅠㅠ
    Module ‘mouse’ has no ‘hook’ member pylint(no-member)
    이런게 열개가량 뜨네요 답을알수있을까요 ?

    • 음음 댓글:

      “python.linting.pylintArgs”: [“–generate-members”]
      세팅에 추가하시면 작동은 되는데 pylint가 제대로 오류를 잡지 못할 수 있어요.
      그래서 디버깅이나 실행하기 전에는 오류를 쉽게 캐치하지 못 할 수 있습니다.

  11. 궁금해요 댓글:

    파이썬으로 실행해보려고했는데, 마우스 모듈 관련 명령어들은 죄다 오류가 떠버리네요. 인스톨은 분명 했는데 왜그럴까요? ㅠㅠ
    Module ‘mouse’ has no ‘hook’ member pylint(no-member)
    이런게 열개가량 뜨네요 답을알수있을까요 ?

  12. 댓글이자꾸삭제돼 ㅠㅠ 댓글:

    안녕하세요? 제가 이 코드를 그대로 복사해서 실행을 시켜봤는데,
    mouse 모듈과 관련된 모든 문장이 오류가 뜨더라고요.
    Module ‘mouse’ has no ‘hook’ member pylint(no-member)

    mouse 모듈은 분명이 다운받았는데, 저렇게 뜨니까 머리가 아프네요.
    혹시 이유를 아시나요?

  13. 반갑습니다 댓글:

    안녕하세요 파일 실행후 레코드를 누르면 module ‘mouse’ has no attribute ‘hook’ 라고 뜨는데 이유가 뭘까요?