Skip to content

Commit f19c440

Browse files
committed
init
0 parents  commit f19c440

File tree

9 files changed

+750
-0
lines changed

9 files changed

+750
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
bin/
2+
obj/
3+
dist/
4+
*.user
5+
*.suo
6+
.vs/

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# TIS-100 Korean Patch
2+
3+
TIS-100의 UI 텍스트를 한국어로 번역하는 모드.
4+
5+
DLL 인젝션에 [Unity Doorstop](https://github.com/NeighTools/UnityDoorstop)을 사용합니다.
6+
7+
## Install
8+
9+
[Releases](../../releases/latest)에서 zip을 다운로드한 뒤, 게임 폴더에 전부 복사합니다.
10+
11+
```
12+
TIS-100/
13+
tis100.exe (기존)
14+
winhttp.dll ← 추가
15+
doorstop_config.ini ← 추가
16+
TIS100Korean.dll ← 추가
17+
translations.json ← 추가
18+
```
19+
20+
## Build
21+
22+
직접 빌드하려면:
23+
24+
```
25+
dotnet build -c Release
26+
```
27+
28+
게임 DLL 참조 경로는 `TIS100KoreanPatch.csproj``GameDir` 속성에서 변경할 수 있습니다.
29+
30+
## Contributing
31+
32+
현재 번역은 완전하지 않을 수 있습니다. Issue 또는 Pull Request를 통한 기여를 환영합니다.
33+
34+
- `data/translations.json` — 번역 데이터 추가 및 수정
35+
- `src/` — 패치 로직 개선

TIS100KoreanPatch.csproj

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net35</TargetFramework>
5+
<AssemblyName>TIS100Korean</AssemblyName>
6+
<LangVersion>7.3</LangVersion>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
9+
<GameDir>C:\Program Files (x86)\Steam\steamapps\common\TIS-100</GameDir>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<Reference Include="UnityEngine">
14+
<HintPath>$(GameDir)\tis100_Data\Managed\UnityEngine.dll</HintPath>
15+
<Private>false</Private>
16+
</Reference>
17+
<Reference Include="UnityEngine.UI">
18+
<HintPath>$(GameDir)\tis100_Data\Managed\UnityEngine.UI.dll</HintPath>
19+
<Private>false</Private>
20+
</Reference>
21+
<Reference Include="Assembly-CSharp">
22+
<HintPath>$(GameDir)\tis100_Data\Managed\Assembly-CSharp.dll</HintPath>
23+
<Private>false</Private>
24+
</Reference>
25+
</ItemGroup>
26+
27+
</Project>

data/doorstop_config.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[General]
2+
enabled=true
3+
target_assembly=TIS100Korean.dll

data/translations.json

Lines changed: 269 additions & 0 deletions
Large diffs are not rendered by default.

data/winhttp.dll

21.5 KB
Binary file not shown.

src/Loader.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.IO;
2+
using System.Reflection;
3+
4+
namespace Doorstop
5+
{
6+
public static class Entrypoint
7+
{
8+
public static void Start()
9+
{
10+
var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
11+
TIS100Korean.Translations.Load(Path.Combine(dir, "translations.json"));
12+
TIS100Korean.Patch.Apply(dir);
13+
}
14+
}
15+
}

src/Patch.cs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Reflection;
5+
using System.Runtime.CompilerServices;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
using System.Threading;
9+
using UnityEngine;
10+
using UnityEngine.UI;
11+
12+
namespace TIS100Korean
13+
{
14+
static class Patch
15+
{
16+
[DllImport("kernel32.dll")]
17+
static extern bool VirtualProtect(IntPtr addr, uint size, uint prot, out uint old);
18+
19+
[DllImport("gdi32.dll")]
20+
static extern int AddFontResourceEx(string path, uint fl, IntPtr pdv);
21+
22+
static FieldInfo _fText;
23+
static FieldInfo _fDisableCallback;
24+
static FieldInfo _fFontData;
25+
static FieldInfo _fFont;
26+
static Font _koreanFont;
27+
static readonly Dictionary<string, string> _cache = new Dictionary<string, string>();
28+
29+
public static void Apply(string patchDir)
30+
{
31+
PatchTitleWidget();
32+
PatchQuantumChat();
33+
new Thread(() => InstallRuntimeHook(patchDir)) { IsBackground = true }.Start();
34+
}
35+
36+
static void PatchTitleWidget()
37+
{
38+
var flags = BindingFlags.Public | BindingFlags.Static;
39+
foreach (var name in new[] { "Text1", "Text2", "Text3", "Text4" })
40+
{
41+
try
42+
{
43+
var fi = typeof(TitleWidget).GetField(name, flags);
44+
if (fi == null) continue;
45+
var tr = Translations.Get((string)fi.GetValue(null));
46+
if (tr != null) fi.SetValue(null, tr);
47+
}
48+
catch { }
49+
}
50+
}
51+
52+
static void PatchQuantumChat()
53+
{
54+
try
55+
{
56+
var arrField = typeof(QuantumChat).GetField(
57+
"gclass41_0", BindingFlags.NonPublic | BindingFlags.Static);
58+
if (arrField == null) return;
59+
60+
var arr = (Array)arrField.GetValue(null);
61+
if (arr == null) return;
62+
63+
var strField = arr.GetType().GetElementType()
64+
.GetField("gparam_1", BindingFlags.Public | BindingFlags.Instance);
65+
if (strField == null) return;
66+
67+
for (int i = 0; i < arr.Length; i++)
68+
{
69+
var elem = arr.GetValue(i);
70+
var formatted = (string)strField.GetValue(elem);
71+
if (string.IsNullOrEmpty(formatted)) continue;
72+
73+
var tr = Translations.Get(UnformatChat(formatted));
74+
if (tr == null) continue;
75+
76+
strField.SetValue(elem, FormatChat(tr));
77+
}
78+
}
79+
catch { }
80+
}
81+
82+
static void InstallRuntimeHook(string patchDir)
83+
{
84+
Thread.Sleep(2000);
85+
86+
try
87+
{
88+
var bf = BindingFlags.NonPublic | BindingFlags.Instance;
89+
_fText = typeof(Text).GetField("m_Text", bf);
90+
_fDisableCallback = typeof(Text).GetField("m_DisableFontTextureRebuiltCallback", bf);
91+
_fFontData = typeof(Text).GetField("m_FontData", bf);
92+
_fFont = typeof(FontData).GetField("m_Font", bf);
93+
94+
JmpDetour(
95+
typeof(Text).GetMethod("OnFillVBO", bf),
96+
typeof(Patch).GetMethod("OnFillVBOHook", BindingFlags.Static | BindingFlags.NonPublic));
97+
}
98+
catch { }
99+
100+
try { _koreanFont = ResolveFont(patchDir); }
101+
catch { }
102+
}
103+
104+
static void OnFillVBOHook(Text self, List<UIVertex> vbo)
105+
{
106+
var original = (string)_fText.GetValue(self);
107+
var display = original;
108+
bool translated = false;
109+
110+
if (!string.IsNullOrEmpty(original))
111+
{
112+
string cached;
113+
if (!_cache.TryGetValue(original, out cached))
114+
{
115+
cached = Translations.Translate(original);
116+
_cache[original] = cached;
117+
}
118+
119+
if (cached != null)
120+
{
121+
display = cached;
122+
translated = true;
123+
}
124+
}
125+
126+
if (translated && _koreanFont != null)
127+
_fFont.SetValue(_fFontData.GetValue(self), _koreanFont);
128+
129+
Render(self, display, vbo);
130+
}
131+
132+
static void Render(Text self, string display, List<UIVertex> vbo)
133+
{
134+
if ((UnityEngine.Object)self.font == (UnityEngine.Object)null)
135+
return;
136+
137+
_fDisableCallback.SetValue(self, true);
138+
139+
self.cachedTextGenerator.Populate(
140+
display, self.GetGenerationSettings(self.rectTransform.rect.size));
141+
142+
var rect = self.rectTransform.rect;
143+
var pivot = Text.GetTextAnchorPivot(self.alignment);
144+
var anchor = new Vector2(
145+
(double)pivot.x != 1.0 ? rect.xMin : rect.xMax,
146+
(double)pivot.y != 0.0 ? rect.yMax : rect.yMin);
147+
var offset = self.PixelAdjustPoint(anchor) - anchor;
148+
var verts = self.cachedTextGenerator.verts;
149+
var scale = 1f / self.pixelsPerUnit;
150+
151+
for (int i = 0; i < verts.Count; i++)
152+
{
153+
var v = verts[i];
154+
v.position *= scale;
155+
v.position.x += offset.x;
156+
v.position.y += offset.y;
157+
vbo.Add(v);
158+
}
159+
160+
_fDisableCallback.SetValue(self, false);
161+
}
162+
163+
static Font ResolveFont(string dir)
164+
{
165+
foreach (var ext in new[] { "*.ttf", "*.otf" })
166+
{
167+
try
168+
{
169+
var files = Directory.GetFiles(dir, ext);
170+
if (files.Length > 0)
171+
{
172+
AddFontResourceEx(files[0], 0x10, IntPtr.Zero);
173+
var f = Font.CreateDynamicFontFromOSFont(
174+
Path.GetFileNameWithoutExtension(files[0]), 12);
175+
if (f != null) return f;
176+
}
177+
}
178+
catch { }
179+
}
180+
181+
foreach (var name in new[] { "Malgun Gothic", "Gulim", "Arial Unicode MS" })
182+
{
183+
try
184+
{
185+
var f = Font.CreateDynamicFontFromOSFont(name, 12);
186+
if (f != null) return f;
187+
}
188+
catch { }
189+
}
190+
return null;
191+
}
192+
193+
static unsafe void JmpDetour(MethodInfo from, MethodInfo to)
194+
{
195+
RuntimeHelpers.PrepareMethod(from.MethodHandle);
196+
RuntimeHelpers.PrepareMethod(to.MethodHandle);
197+
var src = from.MethodHandle.GetFunctionPointer();
198+
var dst = to.MethodHandle.GetFunctionPointer();
199+
uint old;
200+
VirtualProtect(src, 5, 0x40, out old);
201+
byte* p = (byte*)src;
202+
p[0] = 0xE9;
203+
*(int*)(p + 1) = (int)((long)dst - (long)src - 5);
204+
VirtualProtect(src, 5, old, out old);
205+
}
206+
207+
static string FormatChat(string message)
208+
{
209+
const int width = 17;
210+
var sb = new StringBuilder();
211+
int remaining = width;
212+
sb.Append('>');
213+
214+
foreach (var word in message.Split(' '))
215+
{
216+
var w = word.Length + 1 <= width ? word : word.Substring(0, width - 1);
217+
if (w.Length + 1 > remaining)
218+
{
219+
remaining = width;
220+
sb.Append("\n ");
221+
}
222+
sb.Append(' ');
223+
sb.Append(w);
224+
remaining -= w.Length + 1;
225+
}
226+
sb.Append('\n');
227+
return sb.ToString();
228+
}
229+
230+
static string UnformatChat(string formatted)
231+
{
232+
return formatted.TrimStart('>').Replace("\n ", " ").Replace("\n", "").Trim();
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)