POE 창고 정리 - POE chang-go jeongli

유저에 따라 다르겠지만 누군가에겐 재밌는 리그이고 누군가에겐 질리는 리그이거나 여지껏 해왔던 빌드가 똥트리를 타게 되면서 현타가 와버리는 리그가 될 수 있겠다. 나는 후자에 속한다. 

POE 창고 정리 - POE chang-go jeongli
스탠다드에서 가장 먼저 해야 할 일

POE 리그를 진행하다가 어느 날 현타가 와서 스탠다드로 넘어갔다면 첫 번째로 해야할 것은 접속하자마자 은신처로 간 뒤 자나에게 말을 걸어 감시자의 돌을 받아서 아틀라스 맵에 적용시켜 주어야 한다.

POE 창고 정리 - POE chang-go jeongli
자나에게서 감시자의 돌 받기

자나에게 말을 걸면 감시자의 돌을 보상이랍시고 2번처럼 창을 띄워주는데 이를 하나씩 컨트롤+좌클릭하여 인벤으로 옮겨준 뒤 아틀라스 맵에 하나씩 맞춰서 꽂아주자. 

POE 창고 정리 - POE chang-go jeongli
지도 변환

두 번째는 지도를 변환시키는 것인데 지도창을 열어 지도 하나를 선택하면 지도 변환이라는 아이콘이 아래에 생성되는데 3번을 눌러 줘야 흰색, 노랑, 빨간 맵을 모두 바꿔준다. 만약 지도 창고가 아닌 일반 창고 슬롯에 지도를 보관했었다면 상인에게 찾아가서 지도를 판매하면 1:1로 변환시켜 교환해준다.

POE 창고 정리 - POE chang-go jeongli
강탈 도면

세 번째로는 강탈 도면인데 보관함의 톱니 모양 설정을 눌러 제거만 가능 탭 숨기기를 체크 해제한 뒤 나타나는 목록에서 heist를 선택해야하는데 하나씩 수동으로 옮겨야 되어서 계약이나 도면이 많다면 생각보다 빡치는 수작업이 될 수 있겠다.

POE 창고 정리 - POE chang-go jeongli
제거만 가능한 강탈 도면

위의 이미지는 포탈을 타거나 마을 이동을 한 뒤 보관함을 열었을 때 무조건 처음에 뜨는 화면이다. 자꾸 보면 환공포증이 생길 것 같으니 강탈 계약과 도면은 되도록 정리 한 번 해주는 것을 추천한다....

폐지를 열심히 줍고 아이템을 내다 팔면 Currency가 엄청 너저분하게 쌓이죠 ㅜㅜㅜ 오늘은 그런 문제를 해결하고 빠른 폐지 줍기를 위해서 캐릭터 인벤토리에서 한번의 Keyboard 입력으로 모든 Currency를 창고, stash 죠 ~ 로 보내보겠습니다. 

 

POE 에서 마우스 커서를 아이템에 대고 ctrl + c를 누르면 아이템 정보가 Clipboard로 복사되는 기능이 있죠 ? 그 기능을 적극 사용했습니다. ! 자세한 동작 원리는 영상을 참조해 주세요 ~ ㅎㅎ

우선 어떤 라이브러리를 사용했는지 보겠습니다. 

1. 필요 라이브러리

pip install pywin32
pip install pillow
pip install numpy
pip install mss
pip install pyautogui==0.9.38
pip install opencv-python
pip install pprint

사실 POE 자동 물약 먹기 !! if 체력 반피시 … Post 에서 사용했던 라이브러리가 똑같이 필요합니다. 다만 pprint 를 새롭게 설치하는 데요. 이 라이브러리는 파이썬 자료형을 좀 더 보기좋게 Print 하기 위한 라이브러리입니다. 

2. 코드 

코드소개를 하기전에 해줘야 할 사전 작업이 있습니다. 캐릭터 인벤토리가 보이는 상태에서 아래 코드의 run변수를 1번부터 5번까지 차례대로 변경하신 후 실행을 하면 되요 !! 각 단계에 따른 설명은 아래 내용을 참고하세요 

run = 1. Inventory의 좌표를 구합니다. 저번 포스팅에서 했던 cvFunc.py 파일이 필요합니다. (파란색 네모 스크린샷)

POE 창고 정리 - POE chang-go jeongli

run = 2. Inventory 의 첫번째 그리드 스크린샷을 저장합니다. 이 스크린샷은 비어 있는 인벤토리 유닛을 찾을 때 사용됨으로 맨 첫번째 인벤토리는 비워 놓고 실행해야 합니다.  

run = 3. 비어 있는 인벤토리 유닛이 잘 검출 되는지 확인합니다. 혹시 검출이 잘 안된다면 config.cfg (밑에 설명 있습니다.)의 confidence 인자 값을 조절해 보세요 !! Clipboard 복사 시 시간이 꽤 들어가기 때문에 비어있는 유닛을 검출하여 예외 처리 합니다. 

POE 창고 정리 - POE chang-go jeongli

run = 4. 실제 Currency가 Stash로 잘 들어가는지 확인 합니다. 코드를 실행 시키고 3초 이내에  POE 게임화면 을 클릭해서 POE를 Active 윈도우로 만들어야 정상 동작 합니다. 

run = 5. 자 이제 준비 과정은 끝났습니다. 게임 중 F2 키를 누르면 캐릭터 인벤토리의 Currency를 전부 Stash로 옮깁니다. 

그리고 아래 함수에 보면 EXCEPT_CURRENCY 라는 전역 변수가 있습니다. 이 변수는 창고로 옮기지 않을 Currency에 대한 설정입니다. 

inventoryFunc.py

import win32clipboard
import pyautogui as pa
import mouse as mo
import keyboard as keys
import time
import numpy as np
import pprint
import cv2.cv2 as cv2
import mss
import os
from PIL import Image

K_RARENESS = '희귀도'
K_ITEMNAME = 'item_name'
V_CURRENCY = '화폐'
EXCEPT_CURRENCY = ['감정 주문서', '포탈 주문서']



class InventoryTool():
    itemInfoInInven = {}

    def __init__(self, inventorySize, listexceptCurr):
        # inventorySize x, y, w, h
        self.inventorySize = inventorySize
        self.inven = np.empty(shape=(12, 5), dtype=tuple)
        self.invenUnitSize = (int(inventorySize[2] / 12), int(inventorySize[2] / 12))
        self.invenUnitBox = (inventorySize[0] + 5, inventorySize[1] + 5, \
                               self.invenUnitSize[0]-5, self.invenUnitSize[1]-5)
        self.listexceptCurr = listexceptCurr
        for xunit in range(12):
            for yunit in range(5):
                x = self.inventorySize[0] + xunit * self.invenUnitSize[0]
                y = self.inventorySize[1] + yunit * self.invenUnitSize[0]
                cord = pa.center((x, y, self.invenUnitSize[0], self.invenUnitSize[1]))
                self.inven[xunit][yunit] = cord


    def realPoint(self, unitPointX, unitPointY):
        return (self.inven[unitPointX][unitPointY][0], self.inven[unitPointX][unitPointY][1])

    # get clipboard data
    def getItemInfoFromClipboard(self, unitPoint):
        itemInfo = {}
        keys.send("ctrl+c")
        time.sleep(0.03)
        try:
            win32clipboard.OpenClipboard()

            itemData = win32clipboard.GetClipboardData()
            win32clipboard.EmptyClipboard()
            win32clipboard.CloseClipboard()

            itemKinds = itemData.split('--------')[0].strip()
            rarenessAndName = itemKinds.split('\n')
            rareness_key = rarenessAndName[0].split(':')[0]
            rareness_value = rarenessAndName[0].split(':')[1]

            itemInfo[rareness_key] = rareness_value.strip()
            itemInfo[K_ITEMNAME] = rarenessAndName[1]
            self.itemInfoInInven[unitPoint] = itemInfo
            return itemInfo
        except:
            print('No Item ', unitPoint)
            return None


    def checkItemInInvertory(self, boxRegions):

        for x in range(np.shape(self.inven)[0]):
            for y in range(np.shape(self.inven)[1]):

                rpoint = self.realPoint(x, y)
                if self.checkEmptyUnitPoint(boxRegions, rpoint):
                    mo.move(rpoint[0], rpoint[1])
                    time.sleep(0.03)
                    self.getItemInfoFromClipboard((x, y))

    def moveCurrencyToStash(self):
        keys.press('ctrl')
        for unitPoint, iteminfo in self.itemInfoInInven.items():
            rpoint = self.realPoint(unitPoint[0], unitPoint[1])
            try:
                # print('move', iteminfo[K_ITEMNAME])
                if iteminfo[K_RARENESS] == V_CURRENCY \
                        and not iteminfo[K_ITEMNAME] in self.listexceptCurr:

                    mo.move(rpoint[0], rpoint[1])
                    time.sleep(0.05)
                    mo.click()
                    print('Move', iteminfo[K_ITEMNAME])
            except:
                pass

        keys.release('ctrl')

    def checkEmptyUnitPoint(self, boxRegions, centerRpoint):
        for br in boxRegions:
            x, y, w, h = br
            if (x < centerRpoint[0] < x + w) and (y < centerRpoint[1] < y + h):
                # nothing on inven
                return False
        # something on inven
        return True


    def findImage(self, templateName, show=0, confidence=0.6):
        boxRegions = []
        x, y, w, h = self.inventorySize
        mon = {'top': y, 'left': x, 'width': w, 'height': h}
        sct = mss.mss()
        sct.grab(mon)

        img = Image.frombytes('RGB', (w, h), sct.grab(mon).rgb)
        frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        imgPath = os.path.dirname(os.path.realpath(__file__)) + '\\' + templateName
        template = cv2.imread(imgPath, 0)

        w, h = template.shape[::-1]

        res = cv2.matchTemplate(gray, template, cv2.TM_CCOEFF_NORMED)
        threshold = confidence
        loc = np.where(res >= threshold)

        for pt in zip(*loc[::-1]):
            if show == 1:
                cv2.rectangle(frame, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2)
            realX = self.inventorySize[0] + pt[0]
            realY = self.inventorySize[1] + pt[1]
            boxRegions.append((realX, realY, w, h))

        if show == 1:
            cv2.imshow('image', frame)
            cv2.waitKey(0)
            cv2.destroyAllWindows()

        return boxRegions

    def makeTemplate(self, templateName, templatePoint):
        x, y, w, h = templatePoint
        mon = {'top': y, 'left': x, 'width': w, 'height': h}
        sct = mss.mss()
        sct.grab(mon)
        img = Image.frombytes('RGB', (w, h), sct.grab(mon).rgb)
        frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)


        imgPath = os.path.dirname(os.path.realpath(__file__)) + '\\' + templateName
        cv2.imwrite(imgPath, frame)

if __name__=="__main__":
    import configparser
    import cvFunc as cvf
    from ast import literal_eval

    configFile = os.path.dirname(os.path.realpath(__file__)) + '\\' + 'config.cfg'
    config = configparser.ConfigParser()
    config.read(configFile)
    inventory_size = literal_eval(config['inventory']['inven_region'])
    templateName = config['inventory']['inven_unit_empty_pic']
    templateConfidence = float(config['inventory']['confidence'])

    run = 1

    # -- screenshot whole inventory and get size
    if run == 1:
        ip = cvf.ScreenShot()
        ip.partScreenShot()

    # -- screenshot empty inventory unit and save
    elif run == 2:
        it = InventoryTool(inventory_size, EXCEPT_CURRENCY)
        it.makeTemplate(templateName, it.invenUnitBox)

    # -- find empty inventory unit image on whole inventory
    elif run == 3:
        it = InventoryTool(inventory_size, EXCEPT_CURRENCY)
        it.findImage(templateName, show=1, confidence=templateConfidence)

    # -- run 1 time
    elif run == 4:
        time.sleep(3)
        it = InventoryTool(inventory_size, EXCEPT_CURRENCY)
        boxRegions = it.findImage(templateName, 0, confidence=templateConfidence)
        # print(boxRegions)
        it.checkItemInInvertory(boxRegions)

        pprint.pprint(it.itemInfoInInven, indent=4)
        it.moveCurrencyToStash()

    # -- run forever
    elif run == 5:
        keyState = 0
        while True:
            time.sleep(0.001)
            value = keys.is_pressed('F2')
            if value == True:
                it = InventoryTool(inventory_size, EXCEPT_CURRENCY)
                boxRegions = it.findImage(templateName, 0, confidence=templateConfidence)
                # print(boxRegions)
                it.checkItemInInvertory(boxRegions)

                pprint.pprint(it.itemInfoInInven, indent=4)
                it.moveCurrencyToStash()
                keyState = value

 

 

3. Config.cfg

[inventory]
# 4K
# inven_region = (2599, 1152, 1213, 513)
# Full HD
inven_region = (1307, 576, 597, 250)
inven_unit_empty_pic = invenUnitEmpty.png
confidence = 0.8

 

inventory 섹션이 추가되었습니다. 

  • inven_region: inventory 의 x, y, w, h 를 의미합니다. run = 1 으로 얻을 수 있습니다.
  • inven_unit_empty_pic: run = 2 로 얻은 인벤토리 빈 공간 사진 이름입니다. 
  • confidence : OpenCV 에서 template Matching 시 어느정도의 비슷함을 허용할 것이냐에 대한 값입니다. 1에 가까워 질수록 주어진 Template 사진과 똑같은 영역만 검출됩니다. 

 

이렇게 하시고 inventoryFunc.py 를 실행하시면 됩니다. ! 

추가적으로 가끔씩 옮겨지지 않는 Currency가 생긴다면 getItemInfoFromClipboard 함수에서 time.sleep(0.03) time 을 0.05 정도로 늘려 주세요 ! 

ctrl+c 이후 너무 빠르게 Clipboard 내용을 읽게 되면 빠지는 아이템이 생기는 것 같습니다. 

 

넵 오늘 포스팅은 여기까지 입니다. 해당 코드는 엄청 기계적이고 빠른 마우스와 키보드 움직임을 보여주고 있습니다! 그만큼 걸리기 쉽다는 의미이기도 하니 역시 악용과 남용을 하시면 아니되겠죠 ???? ㅎㅎ

아 그나저나 듀얼리스트 내맘빌드로 빌드를 했는데 말라카이가 죽질 않네요 …. ㅜㅜ 나름 몹 빨리 죽길래 쎈캐인줄 알았는뎅 ㅜㅜ 여튼 모든 액린이분들 화이팅 입니다. !!! 좋은밤되세요 ~~ ㅎㅎ