csjosa 라이브러리를 활용해서 한국어 조사 처리를 해보도록 하겠습니다.
원본 텍스트엔 "(은)는" 형태로 조사가 기입되어 있어야 합니다.
0. 관련 툴 / 링크
csjosa 라이브러리(제작자 myevan): https://github.com/myevan/csjosa
GitHub - myevan/csjosa: c# 한글 조사 처리
c# 한글 조사 처리. Contribute to myevan/csjosa development by creating an account on GitHub.
github.com
DnSpyEX: https://github.com/dnSpyEx/dnSpy
GitHub - dnSpyEx/dnSpy: Unofficial revival of the well known .NET debugger and assembly editor, dnSpy
Unofficial revival of the well known .NET debugger and assembly editor, dnSpy - dnSpyEx/dnSpy
github.com
0. 테스트용 상황

UnityEngine.UI.Text, TextMeshPro
2가지 케이스로 진행해보겠습니다.
1. UnityEngine.UI.Text 파트

Managed 폴더의 UnityEngine.UI.dll 파일을 Dnspy에 로드한 후,
UnityEngine.UI 네임스페이스의 Text 클래스를 우클릭하여, Edit Class (C#) 버튼을 누릅니다.
일단, 제일 위 Using; 파트에 다음 코드를 추가합니다.
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;

using을 추가하셨으면
마지막 코드 부분에서 엔터를 한칸 친 후, 아래 코드를 붙여넣습니다.
private static class Csjosa
{
private class JosaPair
{
public JosaPair(string josa1, string josa2)
{
this.josa1 = josa1;
this.josa2 = josa2;
}
public string josa1 { get; private set; }
public string josa2 { get; private set; }
}
private static readonly System.Text.RegularExpressions.Regex _josaRegex = new System.Text.RegularExpressions.Regex(@"\(이\)가|\(와\)과|\(을\)를|\(은\)는|\(아\)야|\(이\)여|\(으\)로|\(이\)라");
private static readonly System.Collections.Generic.Dictionary<string, JosaPair> _josaPatternPaird = new System.Collections.Generic.Dictionary<string, JosaPair>
{
{ "(이)가", new JosaPair("이", "가") },
{ "(와)과", new JosaPair("과", "와") },
{ "(을)를", new JosaPair("을", "를") },
{ "(은)는", new JosaPair("은", "는") },
{ "(아)야", new JosaPair("아", "야") },
{ "(이)여", new JosaPair("이여", "여") },
{ "(으)로", new JosaPair("으로", "로") },
{ "(이)라", new JosaPair("이라", "라") },
};
public static string Process(string src)
{
if (string.IsNullOrEmpty(src))
{
return src;
}
var strBuilder = new System.Text.StringBuilder(src.Length);
var josaMatches = _josaRegex.Matches(src);
var lastHeadIndex = 0;
foreach (System.Text.RegularExpressions.Match josaMatch in josaMatches)
{
var josaPair = _josaPatternPaird[josaMatch.Value];
strBuilder.Append(src, lastHeadIndex, josaMatch.Index - lastHeadIndex);
if (josaMatch.Index > 0)
{
var prevChar = src[josaMatch.Index - 1];
if ((HasJong(prevChar) && josaMatch.Value != "(으)로") ||
(HasJongExceptRieul(prevChar) && josaMatch.Value == "(으)로"))
{
strBuilder.Append(josaPair.josa1);
}
else
{
strBuilder.Append(josaPair.josa2);
}
}
else
{
strBuilder.Append(josaPair.josa1);
}
lastHeadIndex = josaMatch.Index + josaMatch.Length;
}
strBuilder.Append(src, lastHeadIndex, src.Length - lastHeadIndex);
return strBuilder.ToString();
}
private static bool HasJong(char inChar)
{
if (inChar >= 0xAC00 && inChar <= 0xD7A3)
{
return (inChar - 0xAC00) % 28 > 0;
}
return false;
}
private static bool HasJongExceptRieul(char inChar)
{
if (inChar >= 0xAC00 && inChar <= 0xD7A3)
{
int jongCode = (inChar - 0xAC00) % 28;
return jongCode != 8 && jongCode != 0;
}
return false;
}
}

그 다음, "public virtual string text"를 검색하여, set 접근자를 수정합니다.
아래와 같이 수정하시면 됩니다.
set
{
string processedValue = Csjosa.Process(value);
if (!string.IsNullOrEmpty(processedValue))
{
if (this.m_Text != processedValue)
{
this.m_Text = processedValue;
this.SetVerticesDirty();
this.SetLayoutDirty();
}
return;
}
if (string.IsNullOrEmpty(this.m_Text))
{
return;
}
this.m_Text = "";
this.SetVerticesDirty();
}

마찬가지로 "protected override void OnEnable()"도 검색하셔서 아래와 같이 수정합니다.
protected override void OnEnable()
{
base.OnEnable();
if (!string.IsNullOrEmpty(this.m_Text))
{
this.m_Text = Csjosa.Process(this.m_Text);
}
this.cachedTextGenerator.Invalidate();
FontUpdateTracker.TrackText(this);
}
이후 컴파일을 누르신 다음, File - Save All을 누르신 다음 게임을 실행해보시면

일단 잘 뜹니다.
2. TextMeshPro 파트

Unity.TextMeshPro.dll을 dnspy에 로드시키신 후,
TMPro - TMP_Text를 Edit Class 하신 다음, 최상단에 아래 코드를 추가시키시고,
* using Debug = UnityEngine.Debug;를 추가해주셔야 합니다.
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Debug = UnityEngine.Debug;

커서 부분에 아래 코드를 추가합니다
.
private static class Csjosa
{
private class JosaPair
{
public JosaPair(string josa1, string josa2)
{
this.josa1 = josa1;
this.josa2 = josa2;
}
public string josa1 { get; private set; }
public string josa2 { get; private set; }
}
private static readonly System.Text.RegularExpressions.Regex _josaRegex = new System.Text.RegularExpressions.Regex(@"\(이\)가|\(와\)과|\(을\)를|\(은\)는|\(아\)야|\(이\)여|\(으\)로|\(이\)라");
private static readonly System.Collections.Generic.Dictionary<string, JosaPair> _josaPatternPaird = new System.Collections.Generic.Dictionary<string, JosaPair>
{
{ "(이)가", new JosaPair("이", "가") },
{ "(와)과", new JosaPair("과", "와") },
{ "(을)를", new JosaPair("을", "를") },
{ "(은)는", new JosaPair("은", "는") },
{ "(아)야", new JosaPair("아", "야") },
{ "(이)여", new JosaPair("이여", "여") },
{ "(으)로", new JosaPair("으로", "로") },
{ "(이)라", new JosaPair("이라", "라") },
};
public static string Process(string src)
{
if (string.IsNullOrEmpty(src))
{
return src;
}
var strBuilder = new System.Text.StringBuilder(src.Length);
var josaMatches = _josaRegex.Matches(src);
var lastHeadIndex = 0;
foreach (System.Text.RegularExpressions.Match josaMatch in josaMatches)
{
var josaPair = _josaPatternPaird[josaMatch.Value];
strBuilder.Append(src, lastHeadIndex, josaMatch.Index - lastHeadIndex);
if (josaMatch.Index > 0)
{
var prevChar = src[josaMatch.Index - 1];
if ((HasJong(prevChar) && josaMatch.Value != "(으)로") ||
(HasJongExceptRieul(prevChar) && josaMatch.Value == "(으)로"))
{
strBuilder.Append(josaPair.josa1);
}
else
{
strBuilder.Append(josaPair.josa2);
}
}
else
{
strBuilder.Append(josaPair.josa1);
}
lastHeadIndex = josaMatch.Index + josaMatch.Length;
}
strBuilder.Append(src, lastHeadIndex, src.Length - lastHeadIndex);
return strBuilder.ToString();
}
private static bool HasJong(char inChar)
{
if (inChar >= 0xAC00 && inChar <= 0xD7A3)
{
return (inChar - 0xAC00) % 28 > 0;
}
return false;
}
private static bool HasJongExceptRieul(char inChar)
{
if (inChar >= 0xAC00 && inChar <= 0xD7A3)
{
int jongCode = (inChar - 0xAC00) % 28;
return jongCode != 8 && jongCode != 0;
}
return false;
}
}
public virtual string text의 set 접근자 부분을 다음과 같이 수정합니다.
set
{
if (!this.m_IsTextBackingStringDirty && this.m_text != null && value != null && this.m_text.Length == value.Length && this.m_text == value)
{
return;
}
this.m_IsTextBackingStringDirty = false;
this.m_text = Csjosa.Process(value);
this.m_inputSource = TMP_Text.TextInputSources.TextString;
this.m_havePropertiesChanged = true;
this.SetVerticesDirty();
this.SetLayoutDirty();
}
public void SetText(string sourceText, bool syncTextInputBox = true) 메서드의 내용도 다음과 같이 수정합니다.
string sourceText = Csjosa.Process(sourceText);
int num = ((sourceText == null) ? 0 : sourceText.Length);
this.PopulateTextBackingArray(sourceText, 0, num);
this.m_text = sourceText;
this.m_inputSource = TMP_Text.TextInputSources.TextString;
this.PopulateTextProcessingArray();
this.m_havePropertiesChanged = true;
this.SetVerticesDirty();
this.SetLayoutDirty();
3. dnspy 오류 처리

이런 에러가 뜬다면 DnSpy를 재시작하세요.
'한글패치 관련 짧은 글들' 카테고리의 다른 글
| 유니티 SDF폰트 이식 관련 파이썬 스크립트 (0) | 2025.09.02 |
|---|---|
| UnityPy 텍스쳐 삽입 시 "params must be an instance of BC7CompressBlockParams" 오류 해결 (0) | 2025.03.31 |
| global-metadata.dat이 없는 경우의 덤프법 (frida 이용) (0) | 2025.03.23 |
| UnityPy를 이용한 유니티 게임 MonoBehaviour 특정 텍스트 필드 추출/삽입 (0) | 2025.03.04 |
| IoStore를 사용하며 sig우회가 안되는 언리얼 게임 모드 로딩 방지 우회하기 (6) | 2025.01.20 |
