Guest User

MeishoujoKorLauncher.cs

a guest
Jun 19th, 2026
440
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 64.49 KB | None | 0 0
  1. using System;
  2. using System.ComponentModel;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Runtime.InteropServices;
  6. using System.Text;
  7. using System.Threading;
  8. using System.Windows.Forms;
  9.  
  10. internal static class MeishoujoKorLauncher
  11. {
  12. // 실행 중인 원본 프로세스의 메모리에 쓸 패치 단위들이다.
  13. // RawOffset은 파일 오프셋 기준이며, 실제 VA는 RawToVaDelta를 더해 계산한다.
  14. private struct PatchRange
  15. {
  16. public readonly int RawOffset;
  17. public readonly int Size;
  18.  
  19. public PatchRange(int rawOffset, int size)
  20. {
  21. RawOffset = rawOffset;
  22. Size = size;
  23. }
  24. }
  25.  
  26. private struct TextPatch
  27. {
  28. public readonly int RawOffset;
  29. public readonly int Size;
  30. public readonly string Text;
  31.  
  32. public TextPatch(int rawOffset, int size, string text)
  33. {
  34. RawOffset = rawOffset;
  35. Size = size;
  36. Text = text;
  37. }
  38. }
  39.  
  40. private struct WideTextPatch
  41. {
  42. public readonly int RawOffset;
  43. public readonly int Size;
  44. public readonly string Text;
  45.  
  46. public WideTextPatch(int rawOffset, int size, string text)
  47. {
  48. RawOffset = rawOffset;
  49. Size = size;
  50. Text = text;
  51. }
  52. }
  53.  
  54. private struct HexPatch
  55. {
  56. public readonly int RawOffset;
  57. public readonly string Hex;
  58.  
  59. public HexPatch(int rawOffset, string hex)
  60. {
  61. RawOffset = rawOffset;
  62. Hex = hex;
  63. }
  64. }
  65.  
  66. private struct TextReplacement
  67. {
  68. public readonly string Source;
  69. public readonly string Target;
  70.  
  71. public TextReplacement(string source, string target)
  72. {
  73. Source = source;
  74. Target = target;
  75. }
  76. }
  77.  
  78. [StructLayout(LayoutKind.Sequential)]
  79. private struct Rect
  80. {
  81. public int Left;
  82. public int Top;
  83. public int Right;
  84. public int Bottom;
  85. }
  86.  
  87. private struct BytePatchPattern
  88. {
  89. public readonly byte[] Pattern;
  90. public readonly int PatchOffset;
  91. public readonly byte Value;
  92. public readonly string Name;
  93.  
  94. public BytePatchPattern(byte[] pattern, int patchOffset, byte value, string name)
  95. {
  96. Pattern = pattern;
  97. PatchOffset = patchOffset;
  98. Value = value;
  99. Name = name;
  100. }
  101. }
  102.  
  103. private const uint ProcessQueryInformation = 0x0400;
  104. private const uint ProcessVmOperation = 0x0008;
  105. private const uint ProcessVmRead = 0x0010;
  106. private const uint ProcessVmWrite = 0x0020;
  107. private const uint PageExecuteReadWrite = 0x40;
  108.  
  109. private const int ImageBase = 0x400000;
  110. private const int ImageSize = 0x5AF000;
  111. private const int ScanChunkSize = 0x10000;
  112. // 언패킹된 메모리 배치에서 raw 파일 오프셋을 가상 주소로 바꾸기 위한 보정값.
  113. private const int RawToVaDelta = 0x400C00;
  114. private const int TimeoutMs = 10000;
  115. private const int PollIntervalMs = 10;
  116. private const int PatchDelayMs = 50;
  117. private const int TitleWaitMs = 2000;
  118. private const int ProductNamePatchTimeoutMs = 3000;
  119. // 창 제목 "조카소녀"는 소스 파일 인코딩에 흔들리지 않도록 유니코드 이스케이프로 보관한다.
  120. private const string KoreanTitle = "\uC870\uCE74\uC18C\uB140";
  121.  
  122. private const uint EventObjectCreate = 0x8000;
  123. private const uint EventObjectNameChange = 0x800C;
  124. private const uint WinEventOutOfContext = 0x0000;
  125. private const int CbGetCount = 0x0146;
  126. private const int CbGetCurSel = 0x0147;
  127. private const int CbGetLbText = 0x0148;
  128. private const int CbGetLbTextLen = 0x0149;
  129. private const int CbAddString = 0x0143;
  130. private const int CbResetContent = 0x014B;
  131. private const int CbSetCurSel = 0x014E;
  132.  
  133. private static readonly byte[] OriginalPattern = { 0xC6, 0x44, 0x24, 0x47, 0x80 };
  134. // Shift-JIS의 "姪少女" 원본 바이트. 저장 경로 생성 전에 CP949 바이트로 먼저 바꿔야
  135. // "뽂룺룛"처럼 잘못 해석된 폴더명이 만들어지는 레이스를 피할 수 있다.
  136. private static readonly byte[] ProductNameCp932Bytes = { 0x96, 0xC3, 0x8F, 0xAD, 0x8F, 0x97 };
  137. private static readonly int[] ProductNameRawOffsets = { 0x3ECF90, 0x3F3688, 0x3F370C };
  138.  
  139. // GDI 폰트 생성부의 charset을 일본어(0x80)에서 한글(0x81)로 바꾸는 패턴.
  140. // 고정 오프셋보다 패턴 검색이 안정적이라 언패킹 완료 후 메모리에서 직접 찾는다.
  141. private static readonly BytePatchPattern[] CharsetPatterns =
  142. {
  143. new BytePatchPattern(new byte[] { 0xC6, 0x44, 0x24, 0x47, 0x80 }, 4, 0x81, "stack LOGFONTA charset"),
  144. new BytePatchPattern(new byte[] { 0xC6, 0x46, 0x37, 0x80 }, 3, 0x81, "font object default charset"),
  145. };
  146.  
  147. // CP949로 표시되는 메뉴, 메시지, 타이틀 계열 문자열 패치.
  148. // 한국어 문자열은 유니코드 이스케이프로 저장하고, 실제 패치 시 CP949 바이트로 변환한다.
  149. private static readonly TextPatch[] TextPatches =
  150. {
  151. new TextPatch(0x3ED450, 0xD, "\uB098\uB214\uACE0\uB515"),
  152. new TextPatch(0x3ED750, 0x2D, "\uC2DC\uB098\uB9AC\uC624 \uC624\uB958\uB85C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."),
  153. new TextPatch(0x3ED780, 0x3C, "\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.\n\uD30C\uC77C\uC774 \uC190\uC0C1\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4."),
  154. new TextPatch(0x3ED7D8, 0x3D, "\uC11C\uBE0C \uD0C0\uC774\uD2C0 \uBCF5\uADC0\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uD0C0\uC774\uD2C0 \uD654\uBA74\uC73C\uB85C \uB3CC\uC544\uAC11\uB2C8\uB2E4."),
  155. new TextPatch(0x3ED88C, 0x25, "\uD68C\uC0C1\uC744 \uC885\uB8CC\uD569\uB2C8\uB2E4. \uACC4\uC18D\uD560\uAE4C\uC694?"),
  156. new TextPatch(0x3ED8B4, 0x24, "\uD0C0\uC774\uD2C0\uC5D0 \uB3CC\uC544\uC635\uB2C8\uB2E4. \uC88B\uC2B5\uB2C8\uAE4C\uFF1F"),
  157. new TextPatch(0x3ED8DC, 0x2B, "\uD035 \uC138\uC774\uBE0C \uB370\uC774\uD130%2d\uBC88\uC5D0 \uC138\uC774\uBE0C\uD588\uC2B5\uB2C8\uB2E4."),
  158. new TextPatch(0x3ED908, 0x40, "\uD035 \uC138\uC774\uBE0C \uB370\uC774\uD130 %2d\uBC88\uC5D0 \uB36E\uC5B4\uC501\uB2C8\uB2E4. \uACC4\uC18D\uD560\uAE4C\uC694?"),
  159. new TextPatch(0x3ED958, 0x3A, "\uD035 \uC138\uC774\uBE0C \uB370\uC774\uD130 %2d\uBC88\uC744 \uB85C\uB4DC\uD569\uB2C8\uB2E4. \uACC4\uC18D\uD560\uAE4C\uC694?"),
  160. new TextPatch(0x3ED994, 0x26, "\uD035 \uB85C\uB4DC\uD569\uB2C8\uB2E4. \uC88B\uC2B5\uB2C8\uAE4C\uFF1F"),
  161. new TextPatch(0x3EDDF0, 0x25, "\uD6A8\uACFC \uC885\uB8CC\uB97C \uAE30\uB2E4\uB824\uC57C \uD569\uB2C8\uB2E4."),
  162. new TextPatch(0x3EF438, 0x5, "\uC0AC\uC57C"),
  163. new TextPatch(0x3EF440, 0x5, "\uC544\uC57C"),
  164. new TextPatch(0x3EF448, 0x7, "\uC8FC\uC778\uACF5"),
  165. new TextPatch(0x3EFE70, 0xA, "name/\uC0AC\uC57C"),
  166. new TextPatch(0x3EFE7C, 0xA, "name/\uC544\uC57C"),
  167. new TextPatch(0x3EFE88, 0x9, "\uC548\uB0AE\uCDA4"),
  168. new TextPatch(0x3EFE94, 0x7, "\uB0AE\uCDA4"),
  169. new TextPatch(0x3EFEA4, 0xD, "\uB9CE\uC774\uB0AE\uCDA4"),
  170. new TextPatch(0x3EFDC0, 0x1F, "\uACE0\uC815 \uB4DC\uB77C\uC774\uBE0C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4."),
  171. new TextPatch(0x3EFDE0, 0x17, "\uC2DC\uC2A4\uD15C \uD3F4\uB354\uC785\uB2C8\uB2E4."),
  172. new TextPatch(0x3EFDF8, 0x19, "\uC77D\uAE30 \uC804\uC6A9 \uD3F4\uB354\uC785\uB2C8\uB2E4."),
  173. new TextPatch(0x3EFE14, 0x23, "\uC720\uD6A8\uD55C \uD3F4\uB354\uB97C \uC120\uD0DD\uD558\uC138\uC694."),
  174. new TextPatch(0x3EFE38, 0x1F, "\uD3F4\uB354 \uC18D\uC131\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."),
  175. new TextPatch(0x3EFF20, 0x11, "\uCD5C\uACE0\uD654\uC9C8"),
  176. new TextPatch(0x3EFF3C, 0x9, "\uACE0\uD654\uC9C8"),
  177. new TextPatch(0x3EFF50, 0xD, "\uD45C\uC900"),
  178. new TextPatch(0x3EFF6C, 0xB, "\uC808\uC57D"),
  179. new TextPatch(0x3EFFA0, 0x33, "\uC138\uC774\uBE0C \uB370\uC774\uD130\uB97C \uC800\uC7A5\uD560 \uD3F4\uB354\uB97C \uC9C0\uC815\uD558\uC138\uC694."),
  180. new TextPatch(0x3EFFD8, 0x6D, "\uC138\uC774\uBE0C \uC800\uC7A5 \uD3F4\uB354\uAC00 \uC798\uBABB\uB418\uC5C8\uAC70\uB098 \uC5C6\uC2B5\uB2C8\uB2E4.\n\uC720\uD6A8\uD55C \uD3F4\uB354\uB97C \uC9C0\uC815\uD558\uC138\uC694.\n\t%s"),
  181. new TextPatch(0x3F0050, 0x5, "\uB192\uC74C"),
  182. new TextPatch(0x3F005C, 0x5, "\uB192\uAC8C"),
  183. new TextPatch(0x3F006C, 0x5, "\uB0AE\uAC8C"),
  184. new TextPatch(0x3F0078, 0x5, "\uB0AE\uC74C"),
  185. new TextPatch(0x3F00A0, 0x9, "\uC74C\uC18C\uAC70"),
  186. new TextPatch(0x3F00B8, 0xD, "\uC790\uB3D9\uC9C4\uD589"),
  187. new TextPatch(0x3F00C8, 0x17, "\uB2E4\uC74C \uC120\uD0DD\uC9C0\uAE4C\uC9C0 \uC2A4\uD0B5"),
  188. new TextPatch(0x3F00E0, 0xE, "\uC708\uB3C4\uC6B0 \uC18C\uAC70"),
  189. new TextPatch(0x3F00FC, 0x11, "\uC2DC\uC2A4\uD15C \uBA54\uB274"),
  190. new TextPatch(0x3F011C, 0x13, "\uBA54\uC2DC\uC9C0 \uC2A4\uD0B5"),
  191. new TextPatch(0x3F0F60, 0x1A, "%-02d\uC6D4%02d\uC77C%02d\uC2DC%02d\uBD84"),
  192. new TextPatch(0x3F0140, 0xB, "\uC228\uAE30\uC9C0 \uC54A\uC74C"),
  193. new TextPatch(0x3F014C, 0x5, "10\uCD08"),
  194. new TextPatch(0x3F015C, 0x4, "5\uCD08"),
  195. new TextPatch(0x3F0168, 0x4, "2\uCD08"),
  196. new TextPatch(0x3F0238, 0x5, "\uC5C6\uC74C"),
  197. new TextPatch(0x3F0248, 0x5, "\uBCF4\uD1B5"),
  198. new TextPatch(0x3F0258, 0x5, "\uBE60\uB984"),
  199. new TextPatch(0x3F0278, 0xF, "\uC804\uCCB4\uD654\uBA74"),
  200. new TextPatch(0x3F0290, 0x11, "\uCC3D \uBAA8\uB4DC"),
  201. new TextPatch(0x3F02BC, 0x4, "\uC124\uC815"),
  202. new TextPatch(0x3F02C4, 0x8, "\uC0AC\uC6B4\uB4DC"),
  203. new TextPatch(0x3F02D0, 0x8, "\uC2DC\uC2A4\uD15C"),
  204. new TextPatch(0x3F02DC, 0x4, "\uC870\uC791"),
  205. new TextPatch(0x3F02E4, 0x4, "\uD45C\uC2DC"),
  206. new TextPatch(0x3F1260, 0x16, "\uC138\uC774\uBE0C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."),
  207. new TextPatch(0x3F1278, 0x10, "\uC138\uC774\uBE0C\uD588\uC2B5\uB2C8\uB2E4."),
  208. new TextPatch(0x3F128C, 0x1C, "\uC138\uC774\uBE0C\uD574\uB3C4 \uC88B\uC2B5\uB2C8\uAE4C\uFF1F"),
  209. new TextPatch(0x3F12AC, 0x22, "\uB367\uC4F0\uAE30 \uC138\uC774\uBE0C\uD574\uB3C4 \uC88B\uC2B5\uB2C8\uAE4C\uFF1F"),
  210. new TextPatch(0x3F2E5C, 0x19, "\uC0AC\uC6A9 \uBD88\uAC00 \uC774\uB984"),
  211. new TextPatch(0x3F2E78, 0x15, "\uC0AC\uC6A9 \uBD88\uAC00 \uC774\uB984"),
  212. new TextPatch(0x3F2E90, 0x2D, "\uC774\uB984\uC5D0 \uC0AC\uC6A9 \uBD88\uAC00\uD55C \uAE30\uD638\n \uFF1C,\uFF1E,\uFF20,\uFF1D"),
  213. new TextPatch(0x3F2EC0, 0x15, "\uC0AC\uC6A9 \uBD88\uAC00 \uAE30\uD638"),
  214. new TextPatch(0x3F2ED8, 0x21, "\uC774\uB984\uC740 \uC804\uAC01\uBB38\uC790\uB85C \uC785\uB825\uD574 \uC8FC\uC138\uC694"),
  215. new TextPatch(0x3F2EFC, 0x19, "\uBC18\uAC01\uBB38\uC790\uAC00 \uD3EC\uD568\uB418\uC5C8\uC2B5\uB2C8\uB2E4"),
  216. new TextPatch(0x3F2F2C, 0x5, "\uBAA8\uB9AC"),
  217. new TextPatch(0x3F2F34, 0x5, "\uCE20\uB8E8"),
  218. new TextPatch(0x3F2F3C, 0x5, "\uD788\uCE74"),
  219. new TextPatch(0x3F2F44, 0x7, "\uBB18\uCF54\uC9C0"),
  220. new TextPatch(0x3F2F4C, 0x5, "\uC0AC\uCFE0"),
  221. new TextPatch(0x3F2F54, 0x7, "\uD558\uB8E8\uCE74"),
  222. new TextPatch(0x3F2F5C, 0x7, "\uCF00\uC774\uD1A0"),
  223. new TextPatch(0x3F2F64, 0x5, "\uC544\uC57C"),
  224. new TextPatch(0x3F2F6C, 0x7, "\uCE20\uBC14\uC0AC"),
  225. new TextPatch(0x3F2F74, 0x7, "\uB9C8\uB9B0"),
  226. new TextPatch(0x3F2F7C, 0x7, "\uC57C\uCE58\uC694"),
  227. new TextPatch(0x3F2F84, 0x7, "\uBBF8\uB098\uD1A0"),
  228. new TextPatch(0x3F2F8C, 0x7, "\uBBF8\uB77C\uC774"),
  229. new TextPatch(0x3F2F94, 0x21, "\uC774\uB984\uC740 %d\uC790 \uC774\uB0B4\uB85C \uC785\uB825\uD558\uC138\uC694"),
  230. new TextPatch(0x3F2FB8, 0x19, "\uBB38\uC790 \uC218 \uC81C\uD55C\uC744 \uB118\uC5C8\uC2B5\uB2C8\uB2E4"),
  231. new TextPatch(0x3F2FD4, 0x21, "\uC131\uC740 %d\uC790 \uC774\uB0B4\uB85C \uC785\uB825\uD558\uC138\uC694"),
  232. new TextPatch(0x3F2FF8, 0x17, "\uC774\uB984\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694"),
  233. new TextPatch(0x3F34E8, 0x41, "\uD604\uC7AC\uC758 \uAC8C\uC784\uC744 \uC911\uB2E8\uD574 \uC0C8\uB86D\uAC8C \uAC8C\uC784\uC744 \uAC1C\uC2DC\uD569\uB2C8\uB2E4.\n\uC88B\uC2B5\uB2C8\uAE4C\uFF1F"),
  234. new TextPatch(0x3F3530, 0x2E, "\uD604\uC7AC\uC758 \uAC8C\uC784\uC744 \uC911\uB2E8\uD574 \uD0C0\uC774\uD2C0 \uD654\uBA74\uC5D0 \uB3CC\uC544\uC635\uB2C8\uB2E4."),
  235. new TextPatch(0x3F355F, 0x10, "\uC88B\uC2B5\uB2C8\uAE4C?"),
  236. new TextPatch(0x3F3570, 0x12, "\uD68C\uC0C1\uC744 \uC885\uB8CC\uD569\uB2C8\uB2E4."),
  237. new TextPatch(0x3F3583, 0x10, "\uC88B\uC2B5\uB2C8\uAE4C?"),
  238. new TextPatch(0x3F3638, 0x6, "\uB2EB\uAE30"),
  239. new TextPatch(0x3F3640, 0xA, "\uAC8C\uC784 \uC885\uB8CC"),
  240. new TextPatch(0x3F364C, 0x6, "\uB85C\uB4DC"),
  241. new TextPatch(0x3F3654, 0x6, "\uC138\uC774\uBE0C"),
  242. new TextPatch(0x3F365C, 0xC, "\uC2A4\uD0B5 \uC2DC\uC791"),
  243. new TextPatch(0x3F366C, 0xE, "\uC2E0 \uC7AC\uC0DD \uC911\uB2E8"),
  244. new TextPatch(0x3F367C, 0xA, "\uAC8C\uC784 \uC911\uB2E8"),
  245. new TextPatch(0x3F376C, 0x4, "\uD45C\uC2DC"),
  246. new TextPatch(0x3F3775, 0x21, "\uD654\uBA74 \uBAA8\uB4DC(&M)\\\uC804\uCCB4 \uD654\uBA74(&F)"),
  247. new TextPatch(0x3F3798, 0x4, "\uD45C\uC2DC"),
  248. new TextPatch(0x3F37A1, 0x1D, "\uD654\uBA74 \uBAA8\uB4DC(&M)\\\uCC3D \uBAA8\uB4DC(&W)"),
  249. new TextPatch(0x3F37C0, 0x1F, "\uAC8C\uC784(&G)\\\uD14D\uC2A4\uD2B8 \uB85C\uADF8(&L)"),
  250. new TextPatch(0x3F37F8, 0x1F, "\uAC8C\uC784(&G)\\\uC2A4\uD0B5 \uC2DC\uC791(&S)\tTab"),
  251. new TextPatch(0x3F3818, 0x1D, "\uAC8C\uC784(&G)\\\uC790\uB3D9 \uC9C4\uD589(&A)\tP"),
  252. new TextPatch(0x3F3838, 0x6, "\uB3C4\uC6C0\uB9D0"),
  253. new TextPatch(0x3F3843, 0x10, "\uC81C\uD488 \uC0AC\uC774\uD2B8(&S)"),
  254. new TextPatch(0x3F3854, 0x6, "\uB3C4\uC6C0\uB9D0"),
  255. new TextPatch(0x3F385F, 0x15, "\uBC84\uC804 \uC815\uBCF4(&A)..."),
  256. new TextPatch(0x3F3878, 0x19, "\uC0AC\uC6B4\uB4DC(&S)\\\uC74C\uB7C9 \uC870\uC808(&C)."),
  257. new TextPatch(0x3F3898, 0x11, "\uC0AC\uC6B4\uB4DC(&S)\\\uC74C\uC18C\uAC70"),
  258. new TextPatch(0x3F38B0, 0x4, "\uD45C\uC2DC"),
  259. new TextPatch(0x3F38B9, 0x17, "\uD654\uBA74 \uBAA8\uB4DC(&M)\\1:1"),
  260. new TextPatch(0x3F38D4, 0x4, "\uD45C\uC2DC"),
  261. new TextPatch(0x3F38DD, 0x20, "\uD14D\uC2A4\uD2B8\uCC3D \uC228\uAE40(&H)\tSPACE"),
  262. new TextPatch(0x3F3900, 0x4, "\uD45C\uC2DC"),
  263. new TextPatch(0x3F3909, 0xE, "\uD654\uBA74 \uBAA8\uB4DC(&M)"),
  264. new TextPatch(0x3F3918, 0x29, "\uAC8C\uC784(&G)\\\uCD5C\uADFC \uC790\uB3D9\uC800\uC7A5(&B)"),
  265. new TextPatch(0x3F3944, 0xA, "\uAC8C\uC784(&G)"),
  266. new TextPatch(0x3F3950, 0x15, "\uD30C\uC77C(&F)\\\uC885\uB8CC(&X)"),
  267. new TextPatch(0x3F3968, 0x31, "\uD30C\uC77C(&F)\\\uC2A4\uD06C\uB9B0\uC0F7(&P)\tPrintScreen"),
  268. new TextPatch(0x3F399C, 0x18, "\uD30C\uC77C(&F)\\\uC124\uC815(&C)..."),
  269. new TextPatch(0x3F39B8, 0x1F, "\uD30C\uC77C(&F)\\\uD035 \uB85C\uB4DC(&O)"),
  270. new TextPatch(0x3F39D8, 0x1F, "\uD30C\uC77C(&F)\\\uD035 \uC138\uC774\uBE0C(&Q)"),
  271. new TextPatch(0x3F39F8, 0x1A, "\uD30C\uC77C(&F)\\\uB85C\uB4DC(&L)..."),
  272. new TextPatch(0x3F3A14, 0x1A, "\uD30C\uC77C(&F)\\\uC138\uC774\uBE0C(&S)..."),
  273. new TextPatch(0x3F3A30, 0x1B, "\uD30C\uC77C(&F)\\\uAC8C\uC784 \uC911\uB2E8(&B)"),
  274. new TextPatch(0x3F3A4C, 0x1B, "\uD30C\uC77C(&F)\\\uC0C8 \uAC8C\uC784(&N)"),
  275. new TextPatch(0x3F3C08, 0x4, "\uD45C\uC2DC"),
  276. new TextPatch(0x3F3C1C, 0x4, "\uD45C\uC2DC"),
  277. new TextPatch(0x3F3C25, 0xF, "\uD654\uBA74 \uBAA8\uB4DC(&M)\\-"),
  278. new TextPatch(0x3F3C40, 0xB, "\uAC8C\uC784(&G)\\--"),
  279. new TextPatch(0x3F3C54, 0xD, "\uD30C\uC77C(&F)\\----"),
  280. new TextPatch(0x3F3A68, 0x29, "<\uCD5C\uC2E0 \uC790\uB3D9\uC800\uC7A5(&B)> [\uB370\uC774\uD130 \uC5C6\uC74C]"),
  281. new TextPatch(0x3F3A94, 0x3C, "<\uCD5C\uC2E0 \uC790\uB3D9\uC800\uC7A5(&B)> [%04d/%02d/%02d %02d:%02d:%02d]"),
  282. new TextPatch(0x3F3AD0, 0x25, "<\uC911\uB2E8\uD55C \uAC8C\uC784(&A)> [\uB370\uC774\uD130 \uC5C6\uC74C]"),
  283. new TextPatch(0x3F3AF8, 0x38, "<\uC911\uB2E8\uD55C \uAC8C\uC784(&A)> [%04d/%02d/%02d %02d:%02d:%02d]"),
  284. new TextPatch(0x3F3B60, 0x15, "no.%2d [\uC0AC\uC6A9 \uBD88\uAC00]"),
  285. new TextPatch(0x3F3BD4, 0x17, "no.%2d [\uB370\uC774\uD130 \uC5C6\uC74C]"),
  286. new TextPatch(0x3F3BEC, 0x1A, "no.%2d [\uB370\uC774\uD130 \uC5C6\uC74C]\tF1"),
  287. new TextPatch(0x3F4020, 0x17, "\uC790\uB3D9\uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4."),
  288. new TextPatch(0x3F40A0, 0x9, "\uC785\uB2C8\uB2E4."),
  289. new TextPatch(0x3F40B0, 0x30, "\uC8FC\uC778\uACF5 \uC774\uB984\uC744 \uACB0\uC815\uD574 \uC8FC\uC138\uC694.\n\uD604\uC7AC \uC774\uB984\uC740 \u300C"),
  290. new TextPatch(0x3F4214, 0x1F, "\uB9AC\uC18C\uC2A4 \uCC98\uB9AC \uC624\uB958(\uC0DD\uC131\uC2DC)"),
  291. new TextPatch(0x3F4234, 0x17, "\uB9AC\uC18C\uC2A4 \uB204\uC218 \uC9C0\uC810"),
  292. new TextPatch(0x3F424C, 0x1F, "\uB9AC\uC18C\uC2A4 \uCC98\uB9AC \uC624\uB958(\uD574\uC81C\uC2DC)"),
  293. new TextPatch(0x3F426C, 0x5, "\uC624\uB958"),
  294. new TextPatch(0x3F4308, 0x1D, "Module %s \uB85C\uB4DC \uC2E4\uD328"),
  295. new TextPatch(0x3F4520, 0x1A, "\uC885\uB8CC\uD574\uB3C4 \uC88B\uC2B5\uB2C8\uAE4C\uFF1F"),
  296. new TextPatch(0x3F456C, 0xF, "\uB098\uB214\uACE0\uB515"),
  297. new TextPatch(0x3F457C, 0x2C, "\uD544\uC694 \uC778\uD130\uD398\uC774\uC2A4 \uD68D\uB4DD \uC2E4\uD328(2)"),
  298. new TextPatch(0x3F491C, 0x15, "\uC2DC\uC2A4\uD15C\uC744 \uC885\uB8CC\uD569\uB2C8\uB2E4"),
  299. new TextPatch(0x3F4934, 0xF, "\uC2DC\uC2A4\uD15C \uC624\uB958"),
  300. new TextPatch(0x3F4950, 0x7, "\uC624\uB958"),
  301. // 제품명 "姪少女"는 저장 폴더명과 About 문구에 쓰이므로 일본어 원문을 유지한다.
  302. new TextPatch(0x577B53, 0x8, "\u59EA\u5C11\u5973"),
  303. new TextPatch(0x577B5C, 0x9, "\u59EA\u5C11\u5973\n"),
  304. new TextPatch(0x577B66, 0xD, "\u59EA\u5C11\u5973 \uC815\uBCF4"),
  305. };
  306.  
  307. // 설정 다이얼로그 리소스는 UTF-16LE 문자열을 사용한다.
  308. // 여기의 유니코드 이스케이프는 WriteWideTextPatch에서 UTF-16LE 바이트로 바뀐다.
  309. // 구조체 길이를 바꾸면 컨트롤 배치가 깨지므로 기존 슬롯 크기 안에서만 바꾼다.
  310. private static readonly WideTextPatch[] WideTextPatches =
  311. {
  312. new WideTextPatch(0x53EAAE, 0x6, "\uD45C\uC2DC"),
  313. new WideTextPatch(0x53FAA6, 0x6, "\uD45C\uC2DC"),
  314. new WideTextPatch(0x53EE3E, 0x6, "\uC870\uC791"),
  315. new WideTextPatch(0x53FE36, 0x6, "\uC870\uC791"),
  316. new WideTextPatch(0x53F746, 0xA, "\uC2DC\uC2A4\uD15C"),
  317. new WideTextPatch(0x54073E, 0xA, "\uC2DC\uC2A4\uD15C"),
  318. new WideTextPatch(0x53F1FE, 0xA, "\uC0AC\uC6B4\uB4DC"),
  319. new WideTextPatch(0x5401F6, 0xA, "\uC0AC\uC6B4\uB4DC"),
  320. new WideTextPatch(0x53EAF0, 0xA, "\uD654\uBA74\uD45C\uC2DC"),
  321. new WideTextPatch(0x53FAE8, 0xA, "\uD654\uBA74\uD45C\uC2DC"),
  322. new WideTextPatch(0x53EB18, 0x14, "\uD654\uBA74\uBAA8\uB4DC:"),
  323. new WideTextPatch(0x53FB10, 0x14, "\uD654\uBA74\uBAA8\uB4DC:"),
  324. new WideTextPatch(0x53EB6C, 0xA, "\uD45C\uC2DC\uC18D\uB3C4"),
  325. new WideTextPatch(0x53FB64, 0xA, "\uD45C\uC2DC\uC18D\uB3C4"),
  326. new WideTextPatch(0x53EB94, 0xC, "\uD654\uBA74\uC5F0\uCD9C:"),
  327. new WideTextPatch(0x53FB8C, 0xC, "\uD654\uBA74\uC5F0\uCD9C:"),
  328. new WideTextPatch(0x53EBE0, 0xC, "\uBB38\uC790\uD45C\uC2DC:"),
  329. new WideTextPatch(0x53FBD8, 0xC, "\uBB38\uC790\uD45C\uC2DC:"),
  330. new WideTextPatch(0x53EC2C, 0x18, "\uD14D\uC2A4\uD2B8 / \uD3F0\uD2B8"),
  331. new WideTextPatch(0x53FC24, 0x18, "\uD14D\uC2A4\uD2B8 / \uD3F0\uD2B8"),
  332. new WideTextPatch(0x53EC64, 0x1A, "\uD14D\uC2A4\uD2B8\uCC3D\uB18D\uB3C4:"),
  333. new WideTextPatch(0x53FC5C, 0x1A, "\uD14D\uC2A4\uD2B8\uCC3D\uB18D\uB3C4:"),
  334. new WideTextPatch(0x53EC9C, 0x6, "\uC605\uAC8C"),
  335. new WideTextPatch(0x53FC94, 0x6, "\uC605\uAC8C"),
  336. new WideTextPatch(0x53ED00, 0x6, "\uC9C4\uD568"),
  337. new WideTextPatch(0x53FCF8, 0x6, "\uC9C4\uD568"),
  338. new WideTextPatch(0x53ED24, 0x1A, "\uBCF8\uBB38\uAE00\uAF34:"),
  339. new WideTextPatch(0x53FD1C, 0x1A, "\uBCF8\uBB38\uAE00\uAF34:"),
  340. new WideTextPatch(0x53ED7C, 0x14, "\uAE00\uC790\uD06C\uAE30\uC870\uC815:"),
  341. new WideTextPatch(0x53FD74, 0x14, "\uAE00\uC790\uD06C\uAE30\uC870\uC815:"),
  342. new WideTextPatch(0x53EE80, 0xE, "\uC2DC\uB098\uB9AC\uC624\uC870\uC791"),
  343. new WideTextPatch(0x53FE78, 0xE, "\uC2DC\uB098\uB9AC\uC624\uC870\uC791"),
  344. new WideTextPatch(0x53EEAC, 0x20, "\uC77D\uC9C0\uC54A\uC740\uAE00\uC2A4\uD0B5\uC548\uD568"),
  345. new WideTextPatch(0x53FEA4, 0x20, "\uC77D\uC9C0\uC54A\uC740\uAE00\uC2A4\uD0B5\uC548\uD568"),
  346. new WideTextPatch(0x53EEEC, 0x20, "\uC624\uD1A0\uAC1C\uD589\uB300\uAE30\uC2DC\uAC04:"),
  347. new WideTextPatch(0x53FEE4, 0x20, "\uC624\uD1A0\uAC1C\uD589\uB300\uAE30\uC2DC\uAC04:"),
  348. new WideTextPatch(0x53EF2C, 0x6, "\uC9E7\uAC8C"),
  349. new WideTextPatch(0x53FF24, 0x6, "\uC9E7\uAC8C"),
  350. new WideTextPatch(0x53EF90, 0x6, "\uAE38\uAC8C"),
  351. new WideTextPatch(0x53FF88, 0x6, "\uAE38\uAC8C"),
  352. new WideTextPatch(0x53EFB4, 0xE, "\uC2DC\uB098\uB9AC\uC624\uB85C\uADF8"),
  353. new WideTextPatch(0x53FFAC, 0xE, "\uC2DC\uB098\uB9AC\uC624\uB85C\uADF8"),
  354. new WideTextPatch(0x53EFE0, 0x10, "\uC774\uBBF8\uC9C0\uB3C4\uC7AC\uD604"),
  355. new WideTextPatch(0x53FFD8, 0x10, "\uC774\uBBF8\uC9C0\uB3C4\uC7AC\uD604"),
  356. new WideTextPatch(0x53F010, 0x2C, "\uC74C\uC131\uD398\uC774\uC9C0\uC790\uB3D9\uC7AC\uC0DD"),
  357. new WideTextPatch(0x540008, 0x2C, "\uC74C\uC131\uD398\uC774\uC9C0\uC790\uB3D9\uC7AC\uC0DD"),
  358. new WideTextPatch(0x53F05C, 0x8, "\uB9C8\uC6B0\uC2A4"),
  359. new WideTextPatch(0x540054, 0x8, "\uB9C8\uC6B0\uC2A4"),
  360. new WideTextPatch(0x53F084, 0x3C, "\uD3EC\uCEE4\uC2A4\uBC84\uD2BC\uC73C\uB85C\uCEE4\uC11C\uC790\uB3D9\uC774\uB3D9"),
  361. new WideTextPatch(0x54007C, 0x3C, "\uD3EC\uCEE4\uC2A4\uBC84\uD2BC\uC73C\uB85C\uCEE4\uC11C\uC790\uB3D9\uC774\uB3D9"),
  362. new WideTextPatch(0x53F0E0, 0x20, "\uCEE4\uC11C\uC790\uB3D9\uC228\uAE40\uC2DC\uAC04:"),
  363. new WideTextPatch(0x5400D8, 0x20, "\uCEE4\uC11C\uC790\uB3D9\uC228\uAE40\uC2DC\uAC04:"),
  364. new WideTextPatch(0x53F140, 0x1E, "\uC624\uB978\uCABD\uBC84\uD2BC\uC124\uC815:"),
  365. new WideTextPatch(0x540138, 0x1E, "\uC624\uB978\uCABD\uBC84\uD2BC\uC124\uC815:"),
  366. new WideTextPatch(0x53F19C, 0x1E, "\uAC00\uC6B4\uB370\uBC84\uD2BC\uC124\uC815:"),
  367. new WideTextPatch(0x540194, 0x1E, "\uAC00\uC6B4\uB370\uBC84\uD2BC\uC124\uC815:"),
  368. new WideTextPatch(0x53F244, 0xA, "\uC74C\uB7C9\uC870\uC808"),
  369. new WideTextPatch(0x54023C, 0xA, "\uC74C\uB7C9\uC870\uC808"),
  370. new WideTextPatch(0x53F26C, 0x14, "\uB9C8\uC2A4\uD130\uBCFC\uB968:"),
  371. new WideTextPatch(0x540264, 0x14, "\uB9C8\uC2A4\uD130\uBCFC\uB968:"),
  372. new WideTextPatch(0x53F328, 0x12, "\uC74C\uC545\uBCFC\uB968:"),
  373. new WideTextPatch(0x540320, 0x12, "\uC74C\uC545\uBCFC\uB968:"),
  374. new WideTextPatch(0x53F3E0, 0x14, "\uD6A8\uACFC\uC74C\uBCFC\uB968:"),
  375. new WideTextPatch(0x5403D8, 0x14, "\uD6A8\uACFC\uC74C\uBCFC\uB968:"),
  376. new WideTextPatch(0x53F49C, 0x12, "\uC74C\uC131\uBCFC\uB968:"),
  377. new WideTextPatch(0x540494, 0x12, "\uC74C\uC131\uBCFC\uB968:"),
  378. new WideTextPatch(0x53F554, 0x22, "\uC74C\uC131\uC7AC\uC0DD\uC2DC\uC74C\uC545\uB0AE\uCDA4:"),
  379. new WideTextPatch(0x54054C, 0x22, "\uC74C\uC131\uC7AC\uC0DD\uC2DC\uC74C\uC545\uB0AE\uCDA4:"),
  380. new WideTextPatch(0x53F5B4, 0x12, "\uCE90\uB9AD\uD130\uBCC4\uC74C\uC131\uC870\uC808"),
  381. new WideTextPatch(0x5405AC, 0x12, "\uCE90\uB9AD\uD130\uBCC4\uC74C\uC131\uC870\uC808"),
  382. new WideTextPatch(0x53F5E4, 0x8, "\uC544\uC57C:"),
  383. new WideTextPatch(0x5405DC, 0x8, "\uC544\uC57C:"),
  384. new WideTextPatch(0x53F694, 0x8, "\uC0AC\uC57C:"),
  385. new WideTextPatch(0x54068C, 0x8, "\uC0AC\uC57C:"),
  386. new WideTextPatch(0x53F78C, 0x10, "\uC131\uB2A5"),
  387. new WideTextPatch(0x540784, 0x10, "\uC131\uB2A5"),
  388. new WideTextPatch(0x53F7BC, 0x10, "CPU\uC0AC\uC6A9\uB960:"),
  389. new WideTextPatch(0x5407B4, 0x10, "CPU\uC0AC\uC6A9\uB960:"),
  390. new WideTextPatch(0x53F80C, 0x8, "\uC138\uC774\uBE0C"),
  391. new WideTextPatch(0x540804, 0x8, "\uC138\uC774\uBE0C"),
  392. new WideTextPatch(0x53F834, 0x16, "\uC138\uC774\uBE0C\uC800\uC7A5\uC704\uCE58:"),
  393. new WideTextPatch(0x54082C, 0x16, "\uC138\uC774\uBE0C\uC800\uC7A5\uC704\uCE58:"),
  394. new WideTextPatch(0x53F868, 0x12, "\uB0B4\uBB38\uC11C"),
  395. new WideTextPatch(0x540860, 0x12, "\uB0B4\uBB38\uC11C"),
  396. new WideTextPatch(0x53F898, 0x1C, "[\uC800\uC7A5\uAC8C\uC784]\uD3F4\uB354"),
  397. new WideTextPatch(0x540890, 0x1C, "[\uC800\uC7A5\uAC8C\uC784]\uD3F4\uB354"),
  398. new WideTextPatch(0x53F8D4, 0x14, "\uD3F4\uB354\uC9C0\uC815"),
  399. new WideTextPatch(0x5408CC, 0x14, "\uD3F4\uB354\uC9C0\uC815"),
  400. new WideTextPatch(0x53F908, 0xC, "\uCC3E\uAE30..."),
  401. new WideTextPatch(0x540900, 0xC, "\uCC3E\uAE30..."),
  402. new WideTextPatch(0x53F954, 0x1E, "\uC138\uC774\uBE0C\uD3F4\uB354\uC5F4\uAE30"),
  403. new WideTextPatch(0x54094C, 0x1E, "\uC138\uC774\uBE0C\uD3F4\uB354\uC5F4\uAE30"),
  404. new WideTextPatch(0x53F990, 0x16, "\uC2A4\uD06C\uB9B0\uC0F7"),
  405. new WideTextPatch(0x540988, 0x16, "\uC2A4\uD06C\uB9B0\uC0F7"),
  406. new WideTextPatch(0x53F9C4, 0xC, "\uC800\uC7A5\uD488\uC9C8:"),
  407. new WideTextPatch(0x5409BC, 0xC, "\uC800\uC7A5\uD488\uC9C8:"),
  408. new WideTextPatch(0x53FA10, 0x14, "\uC800\uC7A5\uD3F4\uB354\uC5F4\uAE30"),
  409. new WideTextPatch(0x540A08, 0x14, "\uC800\uC7A5\uD3F4\uB354\uC5F4\uAE30"),
  410. new WideTextPatch(0x53FA44, 0x40, "\u203B\uC800\uC7A5\uC704\uCE58\uB97C\uBC14\uAFC8\uB3C4\uC138\uC774\uBE0C\uB294\uC790\uB3D9\uC774\uB3D9\uB418\uC9C0\uC54A\uC2B5\uB2C8\uB2E4."),
  411. new WideTextPatch(0x540A3C, 0x40, "\u203B\uC800\uC7A5\uC704\uCE58\uB97C\uBC14\uAFC8\uB3C4\uC138\uC774\uBE0C\uB294\uC790\uB3D9\uC774\uB3D9\uB418\uC9C0\uC54A\uC2B5\uB2C8\uB2E4."),
  412. };
  413.  
  414. // 문자열 포인터/구분자처럼 텍스트 인코딩으로 표현하기 어려운 보조 바이트 패치.
  415. private static readonly HexPatch[] HexPatches =
  416. {
  417. new HexPatch(0x1641, "08"),
  418. new HexPatch(0x1643, "538797"),
  419. new HexPatch(0x1651, "08"),
  420. new HexPatch(0x1653, "538797"),
  421. new HexPatch(0x6A02, "538797"),
  422. new HexPatch(0x4CD26, "09"),
  423. new HexPatch(0x4CD28, "5c8797"),
  424. new HexPatch(0x4CD5B, "668797"),
  425. new HexPatch(0x3F3893, "00"),
  426. new HexPatch(0x3F3C3B, "00"),
  427. new HexPatch(0x3F3C51, "0000"),
  428. new HexPatch(0x3F3C65, "00000000"),
  429. };
  430.  
  431. // 리소스 패치만으로 처리되지 않는 Win32 컨트롤 텍스트는 생성 직후 SetWindowTextW로 번역한다.
  432. // 왼쪽 유니코드 문자열은 원본 일본어, 오른쪽 유니코드 문자열은 표시할 한국어다.
  433. private static readonly TextReplacement[] DialogTextReplacements =
  434. {
  435. new TextReplacement("\u8A2D\u5B9A", "\uC124\uC815"),
  436. new TextReplacement("\u8868\u793A", "\uD45C\uC2DC"),
  437. new TextReplacement("\u64CD\u4F5C", "\uC870\uC791"),
  438. new TextReplacement("\u30B7\u30B9\u30C6\u30E0", "\uC2DC\uC2A4\uD15C"),
  439. new TextReplacement("\u30B5\u30A6\u30F3\u30C9", "\uC0AC\uC6B4\uB4DC"),
  440. new TextReplacement("\u30B7\u30CA\u30EA\u30AA\u64CD\u4F5C", "\uC2DC\uB098\uB9AC\uC624 \uC870\uC791"),
  441. new TextReplacement("\u672A\u8AAD\u306E\u30C6\u30AD\u30B9\u30C8\u306F\u30B9\u30AD\u30C3\u30D7\u3057\u306A\u3044", "\uC77D\uC9C0 \uC54A\uC740 \uD14D\uC2A4\uD2B8\uB294 \uC2A4\uD0B5\uD558\uC9C0 \uC54A\uAE30"),
  442. new TextReplacement("\u30AA\u30FC\u30C8\u30D7\u30EC\u30A4\u6642\u306E\u6539\u884C\u5F85\u3061", "\uC624\uD1A0 \uD50C\uB808\uC774 \uC2DC \uAC1C\uD589 \uB300\uAE30"),
  443. new TextReplacement("\u77ED\u3044", "\uC9E7\uAC8C"),
  444. new TextReplacement("\u9577\u3044", "\uAE38\uAC8C"),
  445. new TextReplacement("\u30B7\u30CA\u30EA\u30AA\u30ED\u30B0", "\uC2DC\uB098\uB9AC\uC624 \uB85C\uADF8"),
  446. new TextReplacement("\u753B\u50CF\u3082\u518D\u73FE\u3059\u308B", "\uC774\uBBF8\uC9C0\uB3C4 \uC7AC\uD604"),
  447. new TextReplacement("\u97F3\u58F0\u306E\u3042\u308B\u30DA\u30FC\u30B8\u3067\u306F\u97F3\u58F0\u3092\u81EA\u52D5\u7684\u306B\u518D\u751F\u3059\u308B", "\uC74C\uC131\uC774 \uC788\uB294 \uD398\uC774\uC9C0\uC5D0\uC11C \uC74C\uC131 \uC790\uB3D9 \uC7AC\uC0DD"),
  448. new TextReplacement("\u30DE\u30A6\u30B9", "\uB9C8\uC6B0\uC2A4"),
  449. new TextReplacement("\u30D5\u30A9\u30FC\u30AB\u30B9\u306E\u5408\u3063\u305F\u30DC\u30BF\u30F3\u306B\u30DE\u30A6\u30B9\u30AB\u30FC\u30BD\u30EB\u3092\u81EA\u52D5\u7684\u306B\u5408\u308F\u305B\u308B", "\uD3EC\uCEE4\uC2A4\uB41C \uBC84\uD2BC\uC73C\uB85C \uB9C8\uC6B0\uC2A4 \uCEE4\uC11C \uC790\uB3D9 \uC774\uB3D9"),
  450. new TextReplacement("\u30D5\u30A9\u30FC\u30AB\u30B9\u306E\u5408\u3063\u305F\u30DC\u30BF\u30F3\u306B\u30DE\u30A6\u30B9\u30AB\u30FC\u30BD\u30EB\u3092\u81EA\u52D5\u7684\u306B", "\uD3EC\uCEE4\uC2A4\uB41C \uBC84\uD2BC\uC73C\uB85C \uB9C8\uC6B0\uC2A4 \uCEE4\uC11C \uC790\uB3D9 \uC774\uB3D9"),
  451. new TextReplacement("\u30DE\u30A6\u30B9\u30AB\u30FC\u30BD\u30EB\u306E\u81EA\u52D5\u6D88\u53BB", "\uB9C8\uC6B0\uC2A4 \uCEE4\uC11C \uC790\uB3D9 \uC228\uAE40"),
  452. new TextReplacement("\u30DE\u30A6\u30B9\u30AB\u30FC\u30BD\u30EB\u306E\u81EA\u52D5\u6D88\u53BB\u6642", "\uB9C8\uC6B0\uC2A4 \uCEE4\uC11C \uC790\uB3D9 \uC228\uAE40 \uC2DC\uAC04"),
  453. new TextReplacement("\u30DE\u30A6\u30B9\u53F3\u30DC\u30BF\u30F3\u30AB\u30B9\u30BF\u30DE\u30A4\u30BA", "\uB9C8\uC6B0\uC2A4 \uC624\uB978\uCABD \uBC84\uD2BC"),
  454. new TextReplacement("\u30DE\u30A6\u30B9\u53F3\u30DC\u30BF\u30F3\u30AB\u30B9\u30BF", "\uB9C8\uC6B0\uC2A4 \uC624\uB978\uCABD \uBC84\uD2BC"),
  455. new TextReplacement("\u30DE\u30A6\u30B9\u4E2D\u30DC\u30BF\u30F3\u30AB\u30B9\u30BF\u30DE\u30A4\u30BA", "\uB9C8\uC6B0\uC2A4 \uAC00\uC6B4\uB370 \uBC84\uD2BC"),
  456. new TextReplacement("\u30DE\u30A6\u30B9\u4E2D\u30DC\u30BF\u30F3\u30AB\u30B9\u30BF", "\uB9C8\uC6B0\uC2A4 \uAC00\uC6B4\uB370 \uBC84\uD2BC"),
  457. new TextReplacement("5\u79D2", "5\uCD08"),
  458. new TextReplacement("\u5C0F", "\uC791\uAC8C"),
  459. new TextReplacement("\u5927", "\uD06C\uAC8C"),
  460. new TextReplacement("\u78BA\u5B9A", "\uD655\uC778"),
  461. new TextReplacement("\u30AD\u30E3\u30F3\u30BB\u30EB", "\uCDE8\uC18C"),
  462. new TextReplacement("\u9069\u7528", "\uC801\uC6A9"),
  463. };
  464.  
  465. // 콤보박스 항목은 실행 중에 코드가 채우는 경우가 있어 리소스 패치 대상이 아니다.
  466. // 아래 목록은 항목 개수와 왼쪽 라벨을 함께 보고 필요한 콤보박스에만 적용한다.
  467. // 항목명도 Win32 Unicode 메시지로 다시 넣기 때문에 유니코드 이스케이프로 보관한다.
  468. private static readonly string[][] ComboBoxItemSets =
  469. {
  470. new[] { "\uC804\uCCB4\uD654\uBA74", "\uCC3D\uBAA8\uB4DC" },
  471. new[] { "\uC5C6\uC74C", "\uBCF4\uD1B5", "\uBE60\uB984" },
  472. new[] { "\uC228\uAE30\uC9C0\uC54A\uC74C", "10\uCD08", "5\uCD08", "2\uCD08" },
  473. new[] { "\uC74C\uC18C\uAC70", "\uC790\uB3D9\uC9C4\uD589", "\uC120\uD0DD\uC9C0\uC2A4\uD0B5", "\uC708\uB3C4\uC6B0 \uC18C\uAC70", "\uC2DC\uC2A4\uD15C \uBA54\uB274", "\uBA54\uC2DC\uC9C0 \uC2A4\uD0B5" },
  474. new[] { "\uB0AE\uC74C", "\uB0AE\uAC8C", "\uB192\uAC8C", "\uB192\uC74C" },
  475. };
  476.  
  477. private static readonly string[] AutoHideComboItems =
  478. { "\uC228\uAE30\uC9C0\uC54A\uC74C", "10\uCD08", "5\uCD08", "2\uCD08" };
  479.  
  480. // 문자표시 콤보는 원본 항목이 2개뿐이라 전용 유니코드 항목표를 사용한다.
  481. private static readonly string[] TextDisplayComboItems =
  482. { "\uC21C\uAC04", "\uBCF4\uD1B5" };
  483.  
  484. // 저장품질 콤보의 4개 한국어 항목이다.
  485. private static readonly string[] SaveQualityComboItems =
  486. { "\uCD5C\uC800", "\uB0AE\uAC8C", "\uB192\uC74C", "\uCD5C\uACE0" };
  487.  
  488. // CPU사용률 콤보는 원본 순서 低い/低め/通常/高め/高い/MAX에 맞춰 둔다.
  489. private static readonly string[] CpuUsageComboItems =
  490. { "\uB0AE\uC74C", "\uB0AE\uAC8C", "\uBCF4\uD1B5", "\uB192\uAC8C", "\uB192\uC74C", "MAX" };
  491.  
  492. // 음성 재생 시 음악 낮춤 콤보의 3개 한국어 항목이다.
  493. private static readonly string[] VoicePlaybackBgmComboItems =
  494. { "\uC548 \uB0AE\uCDA4", "\uC870\uAE08 \uB0AE\uCDA4", "\uB9CE\uC774 \uB0AE\uCDA4" };
  495.  
  496. private static readonly System.Collections.Generic.HashSet<IntPtr> TranslatedComboBoxes =
  497. new System.Collections.Generic.HashSet<IntPtr>();
  498.  
  499. [DllImport("kernel32.dll", SetLastError = true)]
  500. private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int dwProcessId);
  501.  
  502. [DllImport("kernel32.dll", SetLastError = true)]
  503. private static extern bool CloseHandle(IntPtr hObject);
  504.  
  505. [DllImport("kernel32.dll", SetLastError = true)]
  506. private static extern bool ReadProcessMemory(
  507. IntPtr hProcess,
  508. IntPtr lpBaseAddress,
  509. byte[] lpBuffer,
  510. IntPtr nSize,
  511. out IntPtr lpNumberOfBytesRead);
  512.  
  513. [DllImport("kernel32.dll", SetLastError = true)]
  514. private static extern bool WriteProcessMemory(
  515. IntPtr hProcess,
  516. IntPtr lpBaseAddress,
  517. byte[] lpBuffer,
  518. IntPtr nSize,
  519. out IntPtr lpNumberOfBytesWritten);
  520.  
  521. [DllImport("kernel32.dll", SetLastError = true)]
  522. private static extern bool VirtualProtectEx(
  523. IntPtr hProcess,
  524. IntPtr lpAddress,
  525. IntPtr dwSize,
  526. uint flNewProtect,
  527. out uint lpflOldProtect);
  528.  
  529. private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
  530.  
  531. [DllImport("user32.dll")]
  532. private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
  533.  
  534. [DllImport("user32.dll")]
  535. private static extern bool IsWindowVisible(IntPtr hWnd);
  536.  
  537. [DllImport("user32.dll", SetLastError = true)]
  538. private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
  539.  
  540. // 설정창 컨트롤은 Windows의 Unicode 버전 API로 읽고 쓴다.
  541. // ANSI API를 쓰면 일본어/한국어가 현재 시스템 코드페이지에 따라 깨질 수 있다.
  542. [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  543. private static extern bool SetWindowTextW(IntPtr hWnd, string lpString);
  544.  
  545. private delegate bool EnumChildProc(IntPtr hwnd, IntPtr lParam);
  546.  
  547. [DllImport("user32.dll")]
  548. private static extern bool EnumChildWindows(IntPtr hWndParent, EnumChildProc lpEnumFunc, IntPtr lParam);
  549.  
  550. [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  551. private static extern int GetWindowTextW(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
  552.  
  553. [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  554. private static extern int GetWindowTextLengthW(IntPtr hWnd);
  555.  
  556. [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  557. private static extern int GetClassNameW(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
  558.  
  559. [DllImport("user32.dll", SetLastError = true)]
  560. private static extern IntPtr GetParent(IntPtr hWnd);
  561.  
  562. [DllImport("user32.dll", SetLastError = true)]
  563. private static extern bool GetWindowRect(IntPtr hWnd, out Rect lpRect);
  564.  
  565. // 콤보박스 항목 추가도 SendMessageW를 통해 Unicode 문자열 그대로 전달한다.
  566. [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  567. private static extern IntPtr SendMessageW(IntPtr hWnd, int msg, IntPtr wParam, string lParam);
  568.  
  569. [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  570. private static extern IntPtr SendMessageW(IntPtr hWnd, int msg, IntPtr wParam, StringBuilder lParam);
  571.  
  572. [DllImport("user32.dll", SetLastError = true)]
  573. private static extern IntPtr SendMessageW(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
  574.  
  575. private delegate void WinEventProc(
  576. IntPtr hWinEventHook,
  577. uint eventType,
  578. IntPtr hwnd,
  579. int idObject,
  580. int idChild,
  581. uint dwEventThread,
  582. uint dwmsEventTime);
  583.  
  584. [DllImport("user32.dll", SetLastError = true)]
  585. private static extern IntPtr SetWinEventHook(
  586. uint eventMin,
  587. uint eventMax,
  588. IntPtr hmodWinEventProc,
  589. WinEventProc lpfnWinEventProc,
  590. uint idProcess,
  591. uint idThread,
  592. uint dwFlags);
  593.  
  594. [DllImport("user32.dll", SetLastError = true)]
  595. private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
  596.  
  597. [STAThread]
  598. private static int Main(string[] args)
  599. {
  600. // 이 파일은 단독 패치 exe가 아니라 런처다.
  601. // 같은 폴더의 원본 meishoujo.exe를 실행한 뒤, 언패킹된 메모리에 패치를 적용한다.
  602. string launcherPath = Application.ExecutablePath;
  603. string root = Path.GetDirectoryName(launcherPath) ?? Environment.CurrentDirectory;
  604. string targetExe = Path.Combine(root, "meishoujo.exe");
  605.  
  606. try
  607. {
  608. if (!File.Exists(targetExe))
  609. {
  610. throw new FileNotFoundException("meishoujo.exe was not found next to the launcher.", targetExe);
  611. }
  612.  
  613. ProcessStartInfo startInfo = new ProcessStartInfo
  614. {
  615. FileName = targetExe,
  616. WorkingDirectory = root,
  617. Arguments = JoinArguments(args),
  618. UseShellExecute = false
  619. };
  620.  
  621. using (Process process = Process.Start(startInfo))
  622. {
  623. if (process == null)
  624. {
  625. throw new InvalidOperationException("Failed to start meishoujo.exe.");
  626. }
  627.  
  628. PatchWhenUnpacked(process);
  629. // 설정창 같은 Win32 다이얼로그는 게임 실행 중 계속 생성되므로,
  630. // 원본 프로세스가 종료될 때까지 이벤트 후킹을 유지한다.
  631. TranslateDialogsUntilExit(process);
  632. }
  633.  
  634. return 0;
  635. }
  636. catch (Exception ex)
  637. {
  638. string message = ex.GetType().Name + ": " + ex.Message;
  639. MessageBox.Show(message, "meishoujo_kor patch failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
  640. return 1;
  641. }
  642. }
  643.  
  644. private static string PatchWhenUnpacked(Process process)
  645. {
  646. IntPtr handle = OpenProcess(
  647. ProcessQueryInformation | ProcessVmOperation | ProcessVmRead | ProcessVmWrite,
  648. false,
  649. process.Id);
  650. if (handle == IntPtr.Zero)
  651. {
  652. throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenProcess failed");
  653. }
  654.  
  655. try
  656. {
  657. Stopwatch stopwatch = Stopwatch.StartNew();
  658. Stopwatch productNameStopwatch = Stopwatch.StartNew();
  659. int productNamePatches = 0;
  660. bool productNamePatchComplete = false;
  661.  
  662. while (stopwatch.ElapsedMilliseconds < TimeoutMs)
  663. {
  664. if (process.HasExited)
  665. {
  666. throw new InvalidOperationException("Process exited before the unpacked patch pattern appeared.");
  667. }
  668.  
  669. if (!productNamePatchComplete)
  670. {
  671. // 저장 폴더명은 일반 문자열 패치보다 먼저 사용될 수 있다.
  672. // 그래서 언패킹 감지 패턴을 기다리기 전에도 제품명 슬롯만 반복해서 시도한다.
  673. productNamePatches += ApplyProductNamePatches(handle);
  674. productNamePatchComplete = productNamePatches >= ProductNameRawOffsets.Length;
  675. if (!productNamePatchComplete && productNameStopwatch.ElapsedMilliseconds < ProductNamePatchTimeoutMs)
  676. {
  677. Thread.Sleep(0);
  678. continue;
  679. }
  680. }
  681.  
  682. if (FindPatternAddresses(handle, OriginalPattern).Length > 0)
  683. {
  684. if (process.HasExited)
  685. {
  686. throw new InvalidOperationException("Process exited before the delayed patch was applied.");
  687. }
  688.  
  689. productNamePatches += ApplyProductNamePatches(handle);
  690. Thread.Sleep(PatchDelayMs);
  691. productNamePatches += ApplyProductNamePatches(handle);
  692.  
  693. // 언패킹 완료 후에는 고정 슬롯 문자열, 다이얼로그 리소스, charset 패턴을 한 번에 적용한다.
  694. int applied = ApplyTranslatedPatches(handle);
  695. int charsetPatches = ApplyCharsetPatches(handle);
  696. bool titleSet = TrySetWindowTitle(process, KoreanTitle, TitleWaitMs);
  697. return "applied " + productNamePatches + " early product-name patches; applied " + applied + " menu/title patch ranges; applied " + charsetPatches + " charset hook patches; unicode title set=" + titleSet;
  698. }
  699.  
  700. Thread.Sleep(PollIntervalMs);
  701. }
  702.  
  703. throw new TimeoutException("Timed out waiting for the unpacked patch pattern.");
  704. }
  705. finally
  706. {
  707. CloseHandle(handle);
  708. }
  709. }
  710.  
  711. private static int ApplyProductNamePatches(IntPtr handle)
  712. {
  713. int applied = 0;
  714. // CP949로 인코딩한 "姪少女"를 써서 한국어 로케일에서도 원래 일본어 폴더명이 유지되게 한다.
  715. // 원문 한자는 유니코드 이스케이프로 적어 소스 저장 형식과 무관하게 같은 문자열을 만든다.
  716. byte[] replacement = Encoding.GetEncoding(949).GetBytes("\u59EA\u5C11\u5973");
  717. foreach (int rawOffset in ProductNameRawOffsets)
  718. {
  719. IntPtr address = new IntPtr(rawOffset + RawToVaDelta);
  720. byte[] current = ReadBytes(handle, address, ProductNameCp932Bytes.Length);
  721. if (BytesEqual(current, ProductNameCp932Bytes))
  722. {
  723. WriteBytes(handle, address, replacement);
  724. applied++;
  725. }
  726. }
  727.  
  728. return applied;
  729. }
  730.  
  731. private static int ApplyTranslatedPatches(IntPtr handle)
  732. {
  733. int applied = 0;
  734. foreach (TextPatch patch in TextPatches)
  735. {
  736. byte[] bytes = EncodeCp949Padded(patch.Text, patch.Size);
  737. WriteBytes(handle, new IntPtr(patch.RawOffset + RawToVaDelta), bytes);
  738. applied++;
  739. }
  740.  
  741. foreach (WideTextPatch patch in WideTextPatches)
  742. {
  743. byte[] bytes = EncodeUtf16LePadded(patch.Text, patch.Size);
  744. WriteBytes(handle, new IntPtr(patch.RawOffset + RawToVaDelta), bytes);
  745. applied++;
  746. }
  747.  
  748. foreach (HexPatch patch in HexPatches)
  749. {
  750. byte[] bytes = HexToBytes(patch.Hex);
  751. WriteBytes(handle, new IntPtr(patch.RawOffset + RawToVaDelta), bytes);
  752. applied++;
  753. }
  754.  
  755. return applied;
  756. }
  757.  
  758. private static byte[] EncodeCp949Padded(string text, int size)
  759. {
  760. // TextPatch의 유니코드 문자열은 게임 본문/메뉴가 읽는 CP949 바이트 배열로 변환한다.
  761. byte[] encoded = Encoding.GetEncoding(949).GetBytes(text);
  762. if (encoded.Length > size)
  763. {
  764. throw new InvalidOperationException("Encoded text is too long for patch slot: " + text);
  765. }
  766.  
  767. byte[] padded = new byte[size];
  768. Buffer.BlockCopy(encoded, 0, padded, 0, encoded.Length);
  769. return padded;
  770. }
  771.  
  772. private static byte[] EncodeUtf16LePadded(string text, int size)
  773. {
  774. // WideTextPatch의 유니코드 문자열은 Windows 리소스 형식에 맞춰 UTF-16LE로 변환한다.
  775. byte[] encoded = Encoding.Unicode.GetBytes(text);
  776. if (encoded.Length + 2 > size)
  777. {
  778. throw new InvalidOperationException("Encoded UTF-16 text is too long for patch slot: " + text);
  779. }
  780.  
  781. byte[] padded = new byte[size];
  782. Buffer.BlockCopy(encoded, 0, padded, 0, encoded.Length);
  783. // 다이얼로그 템플릿 문자열은 중간 NUL이 나오면 거기서 끊긴다.
  784. // 남는 영역은 공백으로 채우고 마지막 2바이트만 NUL 종료로 남긴다.
  785. for (int i = encoded.Length; i < size - 2; i += 2)
  786. {
  787. padded[i] = 0x20;
  788. padded[i + 1] = 0x00;
  789. }
  790.  
  791. return padded;
  792. }
  793.  
  794. private static byte[] HexToBytes(string hex)
  795. {
  796. if (hex.Length % 2 != 0)
  797. {
  798. throw new InvalidOperationException("Invalid hex patch length.");
  799. }
  800.  
  801. byte[] bytes = new byte[hex.Length / 2];
  802. for (int i = 0; i < bytes.Length; i++)
  803. {
  804. bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
  805. }
  806.  
  807. return bytes;
  808. }
  809.  
  810. private static int ApplyCharsetPatches(IntPtr handle)
  811. {
  812. int applied = 0;
  813. foreach (BytePatchPattern patchPattern in CharsetPatterns)
  814. {
  815. IntPtr[] matches = FindPatternAddresses(handle, patchPattern.Pattern);
  816. foreach (IntPtr match in matches)
  817. {
  818. IntPtr patchAddress = new IntPtr(match.ToInt64() + patchPattern.PatchOffset);
  819. WriteBytes(handle, patchAddress, new[] { patchPattern.Value });
  820. applied++;
  821. }
  822. }
  823.  
  824. return applied;
  825. }
  826.  
  827. private static IntPtr[] FindPatternAddresses(IntPtr handle, byte[] pattern)
  828. {
  829. if (pattern == null || pattern.Length == 0)
  830. {
  831. return new IntPtr[0];
  832. }
  833.  
  834. IntPtr[] results = new IntPtr[16];
  835. int resultCount = 0;
  836. byte[] previousTail = new byte[0];
  837.  
  838. for (int offset = 0; offset < ImageSize; offset += ScanChunkSize)
  839. {
  840. int size = Math.Min(ScanChunkSize, ImageSize - offset);
  841. byte[] chunk = ReadBytes(handle, new IntPtr(ImageBase + offset), size);
  842. if (chunk.Length == 0)
  843. {
  844. previousTail = new byte[0];
  845. continue;
  846. }
  847.  
  848. byte[] scan = new byte[previousTail.Length + chunk.Length];
  849. Buffer.BlockCopy(previousTail, 0, scan, 0, previousTail.Length);
  850. Buffer.BlockCopy(chunk, 0, scan, previousTail.Length, chunk.Length);
  851.  
  852. // 청크 경계에 걸친 패턴도 놓치지 않도록 이전 청크의 꼬리를 붙여서 검사한다.
  853. int start = 0;
  854. while (true)
  855. {
  856. int index = IndexOf(scan, pattern, start);
  857. if (index < 0)
  858. {
  859. break;
  860. }
  861.  
  862. int absoluteOffset = offset - previousTail.Length + index;
  863. if (absoluteOffset >= 0 && absoluteOffset + pattern.Length <= ImageSize)
  864. {
  865. if (resultCount == results.Length)
  866. {
  867. IntPtr[] grown = new IntPtr[results.Length * 2];
  868. Array.Copy(results, grown, results.Length);
  869. results = grown;
  870. }
  871.  
  872. results[resultCount++] = new IntPtr(ImageBase + absoluteOffset);
  873. }
  874.  
  875. start = index + 1;
  876. }
  877.  
  878. int tailSize = Math.Min(pattern.Length - 1, scan.Length);
  879. previousTail = new byte[tailSize];
  880. Buffer.BlockCopy(scan, scan.Length - tailSize, previousTail, 0, tailSize);
  881. }
  882.  
  883. IntPtr[] trimmed = new IntPtr[resultCount];
  884. Array.Copy(results, trimmed, resultCount);
  885. return trimmed;
  886. }
  887.  
  888. private static int IndexOf(byte[] haystack, byte[] needle, int start)
  889. {
  890. int limit = haystack.Length - needle.Length;
  891. for (int i = start; i <= limit; i++)
  892. {
  893. int j = 0;
  894. while (j < needle.Length && haystack[i + j] == needle[j])
  895. {
  896. j++;
  897. }
  898.  
  899. if (j == needle.Length)
  900. {
  901. return i;
  902. }
  903. }
  904.  
  905. return -1;
  906. }
  907.  
  908. private static bool TrySetWindowTitle(Process process, string title, int timeoutMs)
  909. {
  910. Stopwatch stopwatch = Stopwatch.StartNew();
  911. while (stopwatch.ElapsedMilliseconds < timeoutMs)
  912. {
  913. if (process.HasExited)
  914. {
  915. return false;
  916. }
  917.  
  918. IntPtr hwnd = FindMainWindow(process.Id);
  919. if (hwnd != IntPtr.Zero)
  920. {
  921. return SetWindowTextW(hwnd, title);
  922. }
  923.  
  924. Thread.Sleep(50);
  925. }
  926.  
  927. return false;
  928. }
  929.  
  930. private static void TranslateDialogsUntilExit(Process process)
  931. {
  932. using (DialogTranslationContext context = new DialogTranslationContext(process))
  933. {
  934. Application.Run(context);
  935. }
  936. }
  937.  
  938. private sealed class DialogTranslationContext : ApplicationContext
  939. {
  940. private readonly Process process;
  941. private readonly WinEventProc callback;
  942. private readonly EventHandler exitedHandler;
  943. private IntPtr hook;
  944.  
  945. public DialogTranslationContext(Process process)
  946. {
  947. this.process = process;
  948. callback = OnWinEvent;
  949. exitedHandler = delegate { ExitThread(); };
  950.  
  951. process.EnableRaisingEvents = true;
  952. process.Exited += exitedHandler;
  953.  
  954. // CBT 후킹처럼 프로세스 안에 DLL을 넣지 않고, 외부 프로세스에서 창 생성/이름 변경 이벤트만 받는다.
  955. // 이 방식이면 원본 게임 코드와 충돌할 가능성이 낮고 런처 종료도 단순하다.
  956. hook = SetWinEventHook(
  957. EventObjectCreate,
  958. EventObjectNameChange,
  959. IntPtr.Zero,
  960. callback,
  961. (uint)process.Id,
  962. 0,
  963. WinEventOutOfContext);
  964.  
  965. TranslateProcessWindows(process.Id);
  966.  
  967. if (process.HasExited)
  968. {
  969. ExitThread();
  970. }
  971. }
  972.  
  973. protected override void ExitThreadCore()
  974. {
  975. if (hook != IntPtr.Zero)
  976. {
  977. UnhookWinEvent(hook);
  978. hook = IntPtr.Zero;
  979. }
  980.  
  981. base.ExitThreadCore();
  982. }
  983.  
  984. protected override void Dispose(bool disposing)
  985. {
  986. if (hook != IntPtr.Zero)
  987. {
  988. UnhookWinEvent(hook);
  989. hook = IntPtr.Zero;
  990. }
  991.  
  992. if (disposing)
  993. {
  994. process.Exited -= exitedHandler;
  995. }
  996.  
  997. base.Dispose(disposing);
  998. }
  999.  
  1000. private void OnWinEvent(
  1001. IntPtr hWinEventHook,
  1002. uint eventType,
  1003. IntPtr hwnd,
  1004. int idObject,
  1005. int idChild,
  1006. uint dwEventThread,
  1007. uint dwmsEventTime)
  1008. {
  1009. if (hwnd == IntPtr.Zero || process.HasExited)
  1010. {
  1011. return;
  1012. }
  1013.  
  1014. uint windowProcessId;
  1015. GetWindowThreadProcessId(hwnd, out windowProcessId);
  1016. if (windowProcessId == process.Id)
  1017. {
  1018. TranslateWindowAndChildren(hwnd);
  1019. }
  1020. }
  1021. }
  1022.  
  1023. private static void TranslateProcessWindows(int processId)
  1024. {
  1025. EnumWindows(delegate(IntPtr hwnd, IntPtr lParam)
  1026. {
  1027. uint windowProcessId;
  1028. GetWindowThreadProcessId(hwnd, out windowProcessId);
  1029. if (windowProcessId == processId)
  1030. {
  1031. TranslateWindowAndChildren(hwnd);
  1032. }
  1033.  
  1034. return true;
  1035. }, IntPtr.Zero);
  1036. }
  1037.  
  1038. private static void TranslateWindowAndChildren(IntPtr hwnd)
  1039. {
  1040. TranslateWindowText(hwnd);
  1041. TranslateComboBoxItems(hwnd);
  1042. EnumChildWindows(hwnd, delegate(IntPtr child, IntPtr lParam)
  1043. {
  1044. TranslateWindowText(child);
  1045. TranslateComboBoxItems(child);
  1046. return true;
  1047. }, IntPtr.Zero);
  1048. }
  1049.  
  1050. private static void TranslateWindowText(IntPtr hwnd)
  1051. {
  1052. int length = GetWindowTextLengthW(hwnd);
  1053. if (length <= 0)
  1054. {
  1055. return;
  1056. }
  1057.  
  1058. StringBuilder builder = new StringBuilder(length + 1);
  1059. GetWindowTextW(hwnd, builder, builder.Capacity);
  1060. string current = builder.ToString();
  1061. string translated = TranslateDialogText(current);
  1062. if (!String.Equals(current, translated, StringComparison.Ordinal))
  1063. {
  1064. SetWindowTextW(hwnd, translated);
  1065. }
  1066. }
  1067.  
  1068. private static string TranslateDialogText(string current)
  1069. {
  1070. string translated = current;
  1071. while (true)
  1072. {
  1073. TextReplacement best = default(TextReplacement);
  1074. int bestLength = 0;
  1075.  
  1076. foreach (TextReplacement replacement in DialogTextReplacements)
  1077. {
  1078. // 긴 원문을 먼저 치환해야 부분 문자열 치환이 먼저 일어나 문장이 깨지는 문제를 막을 수 있다.
  1079. if (translated.IndexOf(replacement.Source, StringComparison.Ordinal) >= 0 &&
  1080. replacement.Source.Length > bestLength)
  1081. {
  1082. best = replacement;
  1083. bestLength = replacement.Source.Length;
  1084. }
  1085. }
  1086.  
  1087. if (bestLength == 0)
  1088. {
  1089. return translated;
  1090. }
  1091.  
  1092. translated = translated.Replace(best.Source, best.Target);
  1093. }
  1094. }
  1095.  
  1096. private static void TranslateComboBoxItems(IntPtr hwnd)
  1097. {
  1098. if (!IsComboBox(hwnd))
  1099. {
  1100. return;
  1101. }
  1102.  
  1103. if (TranslatedComboBoxes.Contains(hwnd))
  1104. {
  1105. return;
  1106. }
  1107.  
  1108. int count = SendMessageW(hwnd, CbGetCount, IntPtr.Zero, IntPtr.Zero).ToInt32();
  1109. if (count <= 0)
  1110. {
  1111. return;
  1112. }
  1113.  
  1114. string[] items = FindComboBoxItemSet(hwnd, count);
  1115. if (items == null)
  1116. {
  1117. return;
  1118. }
  1119.  
  1120. // 항목 값 자체가 설정값으로 저장되는 구조가 아니므로, 같은 인덱스의 한국어 문자열로만 교체한다.
  1121. int current = SendMessageW(hwnd, CbGetCurSel, IntPtr.Zero, IntPtr.Zero).ToInt32();
  1122. SendMessageW(hwnd, CbResetContent, IntPtr.Zero, IntPtr.Zero);
  1123. foreach (string item in items)
  1124. {
  1125. SendMessageW(hwnd, CbAddString, IntPtr.Zero, item);
  1126. }
  1127.  
  1128. if (current >= 0 && current < items.Length)
  1129. {
  1130. SendMessageW(hwnd, CbSetCurSel, new IntPtr(current), IntPtr.Zero);
  1131. }
  1132.  
  1133. TranslatedComboBoxes.Add(hwnd);
  1134. }
  1135.  
  1136. private static bool IsComboBox(IntPtr hwnd)
  1137. {
  1138. StringBuilder className = new StringBuilder(64);
  1139. if (GetClassNameW(hwnd, className, className.Capacity) <= 0)
  1140. {
  1141. return false;
  1142. }
  1143.  
  1144. return String.Equals(className.ToString(), "ComboBox", StringComparison.OrdinalIgnoreCase);
  1145. }
  1146.  
  1147. private static string[] FindComboBoxItemSet(IntPtr hwnd, int count)
  1148. {
  1149. // 항목 개수만으로는 CPU/마우스/저장품질 콤보를 구분할 수 없다.
  1150. // 콤보박스 왼쪽의 라벨 텍스트를 같이 확인해 잘못된 번역 세트를 넣지 않게 한다.
  1151. // 라벨 비교 문자열은 앞에서 한국어로 바꾼 Unicode 컨트롤 텍스트와 같은 값을 사용한다.
  1152. string label = NormalizeLabel(FindNearestLeftLabelText(hwnd));
  1153. if (label.IndexOf("\uD654\uBA74\uBAA8\uB4DC", StringComparison.Ordinal) >= 0 && count == 2)
  1154. {
  1155. return ComboBoxItemSets[0];
  1156. }
  1157.  
  1158. if (label.IndexOf("\uD654\uBA74\uC5F0\uCD9C", StringComparison.Ordinal) >= 0 && count == 3)
  1159. {
  1160. return ComboBoxItemSets[1];
  1161. }
  1162.  
  1163. if (label.IndexOf("\uBB38\uC790\uD45C\uC2DC", StringComparison.Ordinal) >= 0 && count == 2)
  1164. {
  1165. return TextDisplayComboItems;
  1166. }
  1167.  
  1168. if (label.IndexOf("\uCEE4\uC11C\uC790\uB3D9\uC228\uAE40\uC2DC\uAC04", StringComparison.Ordinal) >= 0 && count == 4)
  1169. {
  1170. return AutoHideComboItems;
  1171. }
  1172.  
  1173. if ((label.IndexOf("\uC624\uB978\uCABD\uBC84\uD2BC", StringComparison.Ordinal) >= 0 ||
  1174. label.IndexOf("\uAC00\uC6B4\uB370\uBC84\uD2BC", StringComparison.Ordinal) >= 0) &&
  1175. count == 6)
  1176. {
  1177. return ComboBoxItemSets[3];
  1178. }
  1179.  
  1180. if (label.IndexOf("\uC800\uC7A5\uD488\uC9C8", StringComparison.Ordinal) >= 0 && count == 4)
  1181. {
  1182. return SaveQualityComboItems;
  1183. }
  1184.  
  1185. if (label.IndexOf("CPU\uC0AC\uC6A9\uB960", StringComparison.Ordinal) >= 0 && count == 6)
  1186. {
  1187. return CpuUsageComboItems;
  1188. }
  1189.  
  1190. if (label.IndexOf("\uC74C\uC131\uC7AC\uC0DD\uC2DC\uC74C\uC545\uB0AE\uCDA4", StringComparison.Ordinal) >= 0 && count == 3)
  1191. {
  1192. return VoicePlaybackBgmComboItems;
  1193. }
  1194.  
  1195. // 일부 2항목 콤보는 라벨 탐지가 실패할 수 있어, 깨져 보이는 원본 항목명으로 문자표시 콤보를 보조 판별한다.
  1196. if (count == 2 && ContainsAnyComboBoxItem(hwnd, "\uBE15\uBBC5", "\uB8B7\uB4E9"))
  1197. {
  1198. return TextDisplayComboItems;
  1199. }
  1200.  
  1201. return null;
  1202. }
  1203.  
  1204. private static bool ContainsAnyComboBoxItem(IntPtr hwnd, params string[] fragments)
  1205. {
  1206. int count = SendMessageW(hwnd, CbGetCount, IntPtr.Zero, IntPtr.Zero).ToInt32();
  1207. for (int i = 0; i < count; i++)
  1208. {
  1209. string text = ReadComboBoxItemText(hwnd, i);
  1210. foreach (string fragment in fragments)
  1211. {
  1212. if (text.IndexOf(fragment, StringComparison.Ordinal) >= 0)
  1213. {
  1214. return true;
  1215. }
  1216. }
  1217. }
  1218.  
  1219. return false;
  1220. }
  1221.  
  1222. private static string FindNearestLeftLabelText(IntPtr hwnd)
  1223. {
  1224. Rect comboRect;
  1225. if (!GetWindowRect(hwnd, out comboRect))
  1226. {
  1227. return string.Empty;
  1228. }
  1229.  
  1230. IntPtr parent = GetParent(hwnd);
  1231. if (parent == IntPtr.Zero)
  1232. {
  1233. return string.Empty;
  1234. }
  1235.  
  1236. string bestText = string.Empty;
  1237. int bestScore = int.MaxValue;
  1238. EnumChildWindows(parent, delegate(IntPtr child, IntPtr lParam)
  1239. {
  1240. if (child == hwnd || IsComboBox(child))
  1241. {
  1242. return true;
  1243. }
  1244.  
  1245. Rect childRect;
  1246. if (!GetWindowRect(child, out childRect))
  1247. {
  1248. return true;
  1249. }
  1250.  
  1251. int verticalDistance = Math.Abs(((childRect.Top + childRect.Bottom) / 2) - ((comboRect.Top + comboRect.Bottom) / 2));
  1252. if (verticalDistance > 40 || childRect.Left > comboRect.Left)
  1253. {
  1254. return true;
  1255. }
  1256.  
  1257. string text = GetWindowText(child);
  1258. if (String.IsNullOrEmpty(text))
  1259. {
  1260. return true;
  1261. }
  1262.  
  1263. int horizontalDistance = Math.Abs(comboRect.Left - childRect.Right);
  1264. int score = verticalDistance * 1000 + horizontalDistance;
  1265. // 같은 행에 있고 콤보박스 왼쪽에 가장 가까운 텍스트 컨트롤을 라벨로 본다.
  1266. if (score < bestScore)
  1267. {
  1268. bestScore = score;
  1269. bestText = text;
  1270. }
  1271.  
  1272. return true;
  1273. }, IntPtr.Zero);
  1274.  
  1275. return TranslateDialogText(bestText);
  1276. }
  1277.  
  1278. private static string NormalizeLabel(string value)
  1279. {
  1280. if (String.IsNullOrEmpty(value))
  1281. {
  1282. return string.Empty;
  1283. }
  1284.  
  1285. return value
  1286. .Replace(" ", string.Empty)
  1287. .Replace("\t", string.Empty)
  1288. .Replace(":", string.Empty)
  1289. .Replace("\uFF1A", string.Empty);
  1290. }
  1291.  
  1292. private static string GetWindowText(IntPtr hwnd)
  1293. {
  1294. int length = GetWindowTextLengthW(hwnd);
  1295. if (length <= 0)
  1296. {
  1297. return string.Empty;
  1298. }
  1299.  
  1300. StringBuilder builder = new StringBuilder(length + 1);
  1301. GetWindowTextW(hwnd, builder, builder.Capacity);
  1302. return builder.ToString();
  1303. }
  1304.  
  1305. private static string ReadComboBoxItemText(IntPtr hwnd, int index)
  1306. {
  1307. int length = SendMessageW(hwnd, CbGetLbTextLen, new IntPtr(index), IntPtr.Zero).ToInt32();
  1308. if (length <= 0)
  1309. {
  1310. return string.Empty;
  1311. }
  1312.  
  1313. StringBuilder builder = new StringBuilder(length + 1);
  1314. SendMessageW(hwnd, CbGetLbText, new IntPtr(index), builder);
  1315. return builder.ToString();
  1316. }
  1317.  
  1318. private static IntPtr FindMainWindow(int processId)
  1319. {
  1320. IntPtr result = IntPtr.Zero;
  1321. EnumWindows(delegate(IntPtr hwnd, IntPtr lParam)
  1322. {
  1323. uint windowProcessId;
  1324. GetWindowThreadProcessId(hwnd, out windowProcessId);
  1325. if (windowProcessId == processId && IsWindowVisible(hwnd))
  1326. {
  1327. result = hwnd;
  1328. return false;
  1329. }
  1330.  
  1331. return true;
  1332. }, IntPtr.Zero);
  1333. return result;
  1334. }
  1335.  
  1336. private static byte[] ReadBytes(IntPtr handle, IntPtr address, int size)
  1337. {
  1338. byte[] buffer = new byte[size];
  1339. IntPtr bytesRead;
  1340. bool ok = ReadProcessMemory(handle, address, buffer, new IntPtr(size), out bytesRead);
  1341. if (!ok || bytesRead.ToInt64() != size)
  1342. {
  1343. return Array.Empty<byte>();
  1344. }
  1345.  
  1346. return buffer;
  1347. }
  1348.  
  1349. private static void WriteBytes(IntPtr handle, IntPtr address, byte[] buffer)
  1350. {
  1351. uint oldProtect;
  1352. if (!VirtualProtectEx(handle, address, new IntPtr(buffer.Length), PageExecuteReadWrite, out oldProtect))
  1353. {
  1354. throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualProtectEx failed");
  1355. }
  1356.  
  1357. try
  1358. {
  1359. IntPtr written;
  1360. bool ok = WriteProcessMemory(handle, address, buffer, new IntPtr(buffer.Length), out written);
  1361. if (!ok || written.ToInt64() != buffer.Length)
  1362. {
  1363. throw new Win32Exception(Marshal.GetLastWin32Error(), "WriteProcessMemory failed");
  1364. }
  1365. }
  1366. finally
  1367. {
  1368. uint ignored;
  1369. VirtualProtectEx(handle, address, new IntPtr(buffer.Length), oldProtect, out ignored);
  1370. }
  1371. }
  1372.  
  1373. private static bool BytesEqual(byte[] left, byte[] right)
  1374. {
  1375. if (left.Length != right.Length)
  1376. {
  1377. return false;
  1378. }
  1379.  
  1380. for (int i = 0; i < left.Length; i++)
  1381. {
  1382. if (left[i] != right[i])
  1383. {
  1384. return false;
  1385. }
  1386. }
  1387.  
  1388. return true;
  1389. }
  1390.  
  1391. private static string JoinArguments(string[] args)
  1392. {
  1393. if (args == null || args.Length == 0)
  1394. {
  1395. return string.Empty;
  1396. }
  1397.  
  1398. string[] quoted = new string[args.Length];
  1399. for (int i = 0; i < args.Length; i++)
  1400. {
  1401. quoted[i] = QuoteArgument(args[i]);
  1402. }
  1403.  
  1404. return string.Join(" ", quoted);
  1405. }
  1406.  
  1407. private static string QuoteArgument(string value)
  1408. {
  1409. if (string.IsNullOrEmpty(value))
  1410. {
  1411. return "\"\"";
  1412. }
  1413.  
  1414. bool needsQuotes = value.IndexOfAny(new[] { ' ', '\t', '\n', '\v', '"' }) >= 0;
  1415. if (!needsQuotes)
  1416. {
  1417. return value;
  1418. }
  1419.  
  1420. StringBuilder builder = new StringBuilder();
  1421. builder.Append('"');
  1422. int backslashes = 0;
  1423. foreach (char c in value)
  1424. {
  1425. if (c == '\\')
  1426. {
  1427. backslashes++;
  1428. continue;
  1429. }
  1430.  
  1431. if (c == '"')
  1432. {
  1433. builder.Append('\\', backslashes * 2 + 1);
  1434. builder.Append('"');
  1435. backslashes = 0;
  1436. continue;
  1437. }
  1438.  
  1439. builder.Append('\\', backslashes);
  1440. backslashes = 0;
  1441. builder.Append(c);
  1442. }
  1443.  
  1444. builder.Append('\\', backslashes * 2);
  1445. builder.Append('"');
  1446. return builder.ToString();
  1447. }
  1448. }
  1449.  
  1450.  
Advertisement
Add Comment
Please, Sign In to add comment