[C#] 자동화 플러그인 수정

2023. 10. 17. 18:03·Study/C++ & C#

근래에 FFXIV 무인도 컨텐츠의 자동화를 도와주는 플러그인을 발견했기에 당장 써보기로 결정했다.

재미있지도 않은 미친 노가다를 안 할 수 있다니 혹하지 않을 수 있을까.

채집 노드를 알아서 따라가며 지정한 루트는 잘 돌아주지만 다른 문제가 있었으니...

 

1. 클라이언트 언어의 차이

무인도엔 "워크샵"이라는 컨텐츠가 있고, 각 사이클마다 상품 생산을 예약하고 생산 및 판매해 고유 화폐를 얻는 식으로 구성되어 있다. 최고 효율을 얻는 상품 생산 구성을 매 사이클마다 제공해 주는 디스코드가 있는데, 아이템 이름을 영어로만 제공한다. 

 

저 포맷을 그대로 복사해 플러그인에 붙여 넣기 하면, 플러그인이 알아서 아이템 이름만 뽑아내 게임 데이터와 일치하는지 확인한 후 생산 예약에 등록하는 식으로 작동한다.

 

알아서 넣어주는 식

# 위는 플러그인을 수정한 상태로 원래대로라면 작동이 되지 않아야 한다.

 

이것이 문제가 되는 것이다. 플러그인 구현 상 내가 일본어 클라이언트를 사용한다면 영문으로 입력된 정보와 비교가 안되기 때문이다. 아래는 해당 코드다.

private void ImportFromOC(int workshopIndices)
{
    List<MJICraftworksObject> rows = new();
    foreach (var item in ImGui.GetClipboardText().Split('\n', '\r'))
    {
        // expected format: ':OC_ItemName: Item Name (4h)'; first part is optional
        // strip off everything before last ':' and everything after first '(', then strip off spaces
        var actualItem = item.Substring(item.LastIndexOf(':') + 1);
        if (actualItem.IndexOf('(') is var tail && tail >= 0)
            actualItem = actualItem.Substring(0, tail);
        actualItem = actualItem.Trim();
        if (actualItem.Length == 0)
            continue;

        var matchingRows = Service.LuminaGameData.GetExcelSheet<MJICraftworksObject>()!.Where(row => row.Item.Value?.Name.ToString().Contains(actualItem, StringComparison.InvariantCultureIgnoreCase) ?? false).ToList();
        if (matchingRows.Count != 1)
        {
            var error = $"Failed to import schedule: {matchingRows.Count} items matching row '{item}'";
            Service.ChatGui.PrintError(error);
            Service.Log.Error(error);
            return;
        }
        rows.Add(matchingRows.First());
    }

    foreach (var row in rows)
        AddToSchedule(row, workshopIndices);
}

Lumina를 통해 MJICraftworksObject라는 시트를 참조해 아이템 이름과 비교한 후 일치하면 스케줄에 추가하는 식이다.

이 작업을 수행하기 위해 Lumina라는 라이브러리가 사용되는데, 아직 이 라이브러리에 대해 잘 모르므로 다른 방법을 생각해 냈다.

 

 

2. 우회

깔끔한 방법은 아니지만 이런 방법을 사용하기로 했다.

 

  1. csv 형식의 데이터를 사용함
  2. 일어 및 영어 모두의 MJICraftworksObject.csv를 Export
  3. 고유 키 값이 있으므로 키 값을 비교해 일치하면 텍스트를 일어로 교체
  4. 교체된 값을 플러그인에서 사용

하면 될거 같았다.

 

아래는 개념을 확인하기 위한 파이썬 코드이다.

import csv
import re
import pyperclip

def create_dict_from_csv(file_path):
    extracted_dict = {}
    with open(file_path, newline='', encoding='utf-8') as csvfile:
        csv_reader = csv.reader(csvfile)
        header = next(csv_reader)  # skip header line
        for row in csv_reader:
            try:
                key = int(row[0])
            except:
                continue

            if row[1]:
                item_name = row[1]
            else:
                continue

            extracted_dict[key] = item_name
    return extracted_dict

extracted_dict_en = create_dict_from_csv('MJICraftworksObject_en.csv')
extracted_dict_ja = create_dict_from_csv('MJICraftworksObject_ja.csv')

clipboard_string = pyperclip.paste()
print(clipboard_string)

pattern = r":OC_(\w+): (.+?) \((\d+h)\)"
matches = re.findall(pattern, clipboard_string)

# clipboard_string을 라인으로 분할
lines = clipboard_string.split('\n')

# 각 라인에 대해 replace 작업 수행
for idx, line in enumerate(lines):
    updated_line = line

    for match in matches:
        emoji = match[0]
        english_value = match[1]

        japanese_value = ''
        for key, value in extracted_dict_en.items():
            if english_value in value:
                japanese_value = extracted_dict_ja.get(key)
                break

        # 라인의 끝에 있는 줄임표시를 고려하여 비교
        if f":OC_{emoji}: {english_value} ({match[2]})" in line:
            updated_line = updated_line.replace(f":OC_{emoji}: {english_value} ({match[2]})", f":OC_{emoji}: {japanese_value} ({match[2]})")

    # 업데이트된 라인을 다시 할당
    lines[idx] = updated_line

# 라인들을 다시 합치기
clipboard_string = '\n'.join(lines)

print(clipboard_string)


### Input
:OC_CulinaryKnife: Culinary Knife (4h)
:OC_SharkOil: Shark Oil (8h)
:OC_CulinaryKnife: Culinary Knife (4h)
:OC_SharkOil: Shark Oil (8h)

### Output
:OC_CulinaryKnife: アイルクリナリーナイフ (4h)
:OC_SharkOil: 開拓工房のシャークオイル (8h)
:OC_CulinaryKnife: アイルクリナリーナイフ (4h)
:OC_SharkOil: 開拓工房のシャークオイル (8h)

 

원하는 대로 동작하는 거 같으니 실제 플러그인에 이 로직을 넣어보자.

 

 

 

private Dictionary<int, string> CreateDictionaryFromCsv(string filePath)
{
    Dictionary<int, string> extractedDict = new Dictionary<int, string>();
    using (StreamReader reader = new StreamReader(filePath))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            string[] parts = line.Split(',');
            if (int.TryParse(parts[0], out int key) && !string.IsNullOrEmpty(parts[1]))
            {
                extractedDict[key] = parts[1];
            }
        }
    }
    return extractedDict;
}

private string TranslateToJA(string clipboard)
{
    Dictionary<int, string> extractedDictEn = CreateDictionaryFromCsv("MJICraftworksObject_en.csv");
    Dictionary<int, string> extractedDictJa = CreateDictionaryFromCsv("MJICraftworksObject_ja.csv");

    string clipboardString = clipboard;

    string pattern = @":OC_(\w+): (.+?) \((\d+h)\)";
    MatchCollection matches = Regex.Matches(clipboardString, pattern);

    // clipboardString을 라인으로 분할
    string[] lines = clipboardString.Split('\n');

    // 각 라인에 대해 replace 작업 수행
    for (int i = 0; i < lines.Length; i++)
    {
        string updatedLine = lines[i];

        foreach (Match match in matches)
        {
            string emoji = match.Groups[1].Value;
            string englishValue = match.Groups[2].Value;
            string duration = match.Groups[3].Value;

            string japaneseValue = "";
            foreach (var entry in extractedDictEn)
            {
                if (entry.Value.Contains(englishValue))
                {
                    if (extractedDictJa.TryGetValue(entry.Key, out string jaValue))
                    {
                        japaneseValue = jaValue;
                        Service.Log.Info($"Found {jaValue} in dictionary");
                        break;
                    }
                }
            }

            // 라인의 끝에 있는 줄임표시를 고려하여 비교
            if (lines[i].Contains($":OC_{emoji}: {englishValue} ({duration})"))
            {
                updatedLine = updatedLine.Replace($":OC_{emoji}: {englishValue} ({duration})", $":OC_{emoji}: {japaneseValue.Trim('"')} ({duration})");
                Service.Log.Info($"Replaced {englishValue} with {japaneseValue}");
            }
        }

        // 업데이트된 라인을 다시 할당
        lines[i] = updatedLine;
    }

    // 라인들을 다시 합치기
    clipboardString = string.Join("\n", lines);

    return clipboardString;
}

왜인지 모르겟지만 일어 스트링에 큰따옴표가 생겨서 Trim()으로 제거해 줬다.

 

이렇게 교체된 스트링은 아래와 같이 ImportFromOC()에 들어간다.

private void ImportFromOC(int workshopIndices)
{	
    // 교체된 스트링을 대신 넣는다
    string clipboardTranslated = TranslateToJA(ImGui.GetClipboardText());

    List<MJICraftworksObject> rows = new();
    foreach (var item in clipboardTranslated.Split('\n', '\r'))
    {
        // expected format: ':OC_ItemName: Item Name (4h)'; first part is optional
        // strip off everything before last ':' and everything after first '(', then strip off spaces
        var actualItem = item.Substring(item.LastIndexOf(':') + 1);
        if (actualItem.IndexOf('(') is var tail && tail >= 0)
            actualItem = actualItem.Substring(0, tail);
        actualItem = actualItem.Trim();
        if (actualItem.Length == 0)
            continue;
    // ....

 

이렇게 빌드해서 플러그인을 실행하고, 필요한 csv 파일도 ffxiv_dx11.exe가 있는 경로에 대충 던져두면 끝.

 

3. 결과

 

위의 이미지 재탕이긴 한데, 여하튼 Overseas Casuals에서 제공하는 영문 아이템 명을 그대로 가져오더라도

실행에 전혀 문제가 없는 것을 확인할 수 있다.

 

영클라를 쓰면 이런 과정 없이 해결될 문제지만 일클라를 버릴 순 없지.

 

4. 생각해볼 점

플러그인 내부에서 Lumina만을 활용하여 해당 기능이 동작해야 맞는 게 아닐까?

Lumina를 통해 해당 기능을 구현할 수 있는진 모르겠다.

'Study > C++ & C#' 카테고리의 다른 글

[C#] 심플한 게임 런처  (0) 2024.03.06
[C#] ###Clicker  (0) 2024.02.29
[C++ / Python / DB] ProcedureGenerator  (0) 2023.09.01
[C++ / DB] ORM  (0) 2023.09.01
[C++ / DB] XML Parser  (0) 2023.08.31
'Study/C++ & C#' 카테고리의 다른 글
  • [C#] 심플한 게임 런처
  • [C#] ###Clicker
  • [C++ / Python / DB] ProcedureGenerator
  • [C++ / DB] ORM
BVM
BVM
  • BVM
    E:\
    BVM
  • 전체
    오늘
    어제
    • 분류 전체보기 (168)
      • Thoughts (14)
      • Study (69)
        • Japanese (3)
        • C++ & C# (46)
        • Javascript (3)
        • Python (14)
        • Others (3)
      • Play (1)
        • Battlefield (1)
      • Others (11)
      • Camp (73)
        • T.I.L. (57)
        • Temp (1)
        • Standard (10)
        • Challenge (3)
        • Project (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 본 블로그 개설의 목적
  • 인기 글

  • 태그

    IOCP
    네트워크
    포인터
    Dalamud
    c#
    JS
    cloudtype
    암호화
    스타필드
    docker
    discord py
    Python
    Selenium
    로깅
    C++
    Network
    OSI
    서버
    boost
    7계층
    Server
    Asio
    FF14
    네트워크 프로그래밍
    bot
    프로그래머스
    discord
    db
    클라우드
    베데스다
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
BVM
[C#] 자동화 플러그인 수정
상단으로

티스토리툴바