此程式現於Github開源。
Java Decompiler is used to get the source codes of latest version.
我們將decompile的檔案拆包,覆蓋原本的檔案(FFXIVChnTextPatch\src\main\java\
)。
清除專案重新build,發現無法執行。用和前面一樣的方法新增commons-logging-1.2
這個jar,運行成功。
在做任何修改之前,實際先測試build一遍,發現會跳以下錯誤
java.nio.BufferUnderflowException
at java.base/java.nio.HeapByteBuffer.get(HeapByteBuffer.java:183)
at java.base/java.nio.ByteBuffer.get(ByteBuffer.java:822)
at name.yumao.ffxiv.chn.model.EXDFEntry.getString(EXDFEntry.java:83)
at name.yumao.ffxiv.chn.replace.ReplaceEXDF.replace(ReplaceEXDF.java:178)
at name.yumao.ffxiv.chn.thread.ReplaceThread.run(ReplaceThread.java:42)
at java.base/java.lang.Thread.run(Thread.java:831)
這是因為我們反編譯的檔案少了一串括號。修改EXDFEntry.java:
// nullTermPos = buffer.position() - datasetChunkSize + stringOffset;
nullTermPos = buffer.position() - (datasetChunkSize + stringOffset);
接著又遇到這個問題:
Now File : EXD/AchievementHideCondition.EXH
Replace File : AchievementHideCondition
java.lang.NullPointerException: Cannot read the array length because "array" is null
at java.base/java.nio.ByteBuffer.wrap(ByteBuffer.java:435)
at name.yumao.ffxiv.chn.model.EXDFFile.loadEXDF(EXDFFile.java:32)
at name.yumao.ffxiv.chn.model.EXDFFile.<init>(EXDFFile.java:18)
at name.yumao.ffxiv.chn.replace.ReplaceEXDF.replace(ReplaceEXDF.java:139)
at name.yumao.ffxiv.chn.thread.ReplaceThread.run(ReplaceThread.java:38)
at java.base/java.lang.Thread.run(Thread.java:831)
修改ReplaceEXDF.java,新增continue;
try {
exdFileJA = extractFile(this.pathToIndexSE, exdIndexFileJA.getOffset());
} catch (Exception jaEXDFileException) {
continue;
}
這樣就可以正常運行了。
類似的結構在EXDFUtil.java的210行、272行等地方也有出現。如果想防止意外,也可以都做修改。建議搜尋catch (Exception jaEXDFileException)
。
參考這篇巴哈小屋文章。
為了方便能同時吃未漢化和漢化過的檔案,我們將抽取處也設定進config裡面。
要修改以下檔案
修正指針不會跑的問題,如下。
ReplaceEXDF.java
// edited
// this.percentPanel.percentShow(++fileCount / this.fileList.size());
percentPanel.percentShow((double)(++fileCount) / (double)fileList.size());
FFXIVPatchMain.java移除以下部分
PercentPanel percentPanel = new PercentPanel("提莫苑|获取资源");
try {
String index = Request.Get("http://ffxiv.chn.teemo.name/index").connectTimeout(1000).socketTimeout(1000).execute().returnContent().asString();
// System.out.println("index: " + index);
Type listType = (new TypeToken<ArrayList<TeemoUpdateVo>>() {
}).getType();
updates = (List)(new Gson()).fromJson(index, listType);
} catch (Exception exception) {
System.out.println("Exception caught in Request get!");
} finally {
percentPanel.dispose();
}
修改以下檔案中和update有關的部分:
將與TeemoUpdateVo
和update
有關的行列刪掉或註解掉,然後依序檢查跳錯誤的地方,最後將import去除。
另外,ConfigApplicationPanel.java中的資源版本項也可以一併註解掉。
/*
private JLabel transModeLable = new JLabel("资源版本");
private JComboBox<String> transModeVal;
*/
任務名稱還是原文沒有翻譯。推測是EXD/QUEST.EXH沒有被正確處理。
進入EXDFUtil.java檢查,最後鎖定問題在於public HashMap<String, byte[]> exQuestCN(HashMap<String, byte[]> exMap)
沒有將任何東西塞進questMap
。而會發生這樣的事情是因為沒有讀到任何的「KEY」,例如SubFst010_00001
這樣一串符號。
透過Godbert,我們發現key的Offset從0x968變成了0x96C。因此,我們修改第二個if處的offset條件:
if (exdfDatasetSE.type == 0x0 && exdfDatasetSE.offset == 0x96C /* it was 0x968 */) {
key = new String(exdfEntryJA.getString(exdfDatasetSE.offset), "UTF-8");
// System.out.println("\t\t\tGet Key! " + String.valueOf(key));
}
if (exdfDatasetSE.type == 0x0 && exdfDatasetSE.offset == 0x96C /* it was 0x968 */) {
String key = new String(exdfEntryJA.getString(exdfDatasetSE.offset), "UTF-8");
if (sourceMap.get(key) != null)
exMap.put(("EXD/Quest_".toLowerCase() + String.valueOf(listEntryIndex) + "_1").toLowerCase(), sourceMap.get(key));
break;
}
讀取CSV的功能我使用了Univocity Parser,請記得在ReplaceEXDF.java加上import。
ConfigApplicationPanel.java
// added
// 因為新增了這一段,以下各物件的setBounds的第二項都要增加30(往下移)。
// 為了因應CSV,新增CSV選項。
this.fLangLable.setBounds(30, 70, 100, 25);
this.fLangLable.setFont(new Font("Microsoft Yahei", 1, 13));
this.fLangLable.setForeground(new Color(110, 110, 110));
add(this.fLangLable, 0);
this.fLangLableVal = new JComboBox<>();
this.fLangLableVal.addItem("CSV");
this.fLangLableVal.addItem("日文");
this.fLangLableVal.addItem("英文");
this.fLangLableVal.addItem("德文");
this.fLangLableVal.addItem("法文");
this.fLangLableVal.addItem("簡體中文");
this.fLangLableVal.setBounds(100, 70, 160, 23);
this.fLangLableVal.setFont(new Font("Microsoft Yahei", 1, 13));
this.fLangLableVal.setForeground(new Color(110, 110, 110));
this.fLangLableVal.setOpaque(false);
this.fLangLableVal.setFocusable(false);
add(this.fLangLableVal, 0);
Language.java
public enum Language {
CHS("簡體中文", "CHS", "chs", "5"),
CHT("正體中文", "CHT", "cht", "5"),
CSV("CSV", "CSV", "csv", "6"),
JA("日文", "JA", "ja", "0"),
EN("英文", "EN", "en-gb", "1"),
DE("德文", "DE", "de", "2"),
FR("法文", "FR", "fr", "3");
ReplaceEXDF.java 在以下位置新增CSV用的內容。
7/21修改:確定FileReader編碼以避免後續輸出exe時出現編碼問題。
if ((exhSE.getLangs()).length > 0) {
// added for CSV
HashMap<Integer, Integer> offsetMap = new HashMap<>();
// ArrayList<List<String>> dataList = new ArrayList<List<String>>();
HashMap<Integer, String[]> csvDataMap = new HashMap<>();
if (this.csv) {
try {
CsvParserSettings csvSettings = new CsvParserSettings();
csvSettings.setMaxCharsPerColumn(-1);
csvSettings.setMaxColumns(4096);
CsvParser csvParser = new CsvParser(csvSettings);
String csvPath = "resource" + File.separator + "rawexd" + File.separator + replaceFile.substring(4, replaceFile.indexOf(".")) + ".csv";
if (new File(csvPath).exists()) {
List<String[]> allRows = csvParser.parseAll(new FileReader(csvPath, StandardCharsets.UTF_8));
for (int i = 1; i < allRows.get(1).length; i++) {
offsetMap.put(Integer.valueOf((allRows.get(1))[i]), i - 1);
}
int rowNumber = allRows.size();
for (int i = 3; i < rowNumber; i++) {
csvDataMap.put(Integer.valueOf((allRows.get(i))[0]), Arrays.copyOfRange(allRows.get(i), 1, allRows.get(i).length));
}
} else {
System.out.println("\t\tCSV file not exists! " + csvPath);
continue;
}
} catch (Exception csvFileIndexValueException) {
System.out.println("\t\tCSV Exception. " + csvFileIndexValueException.getMessage());
continue;
}
}
// 更新文本內容
// EXD/warp/WarpInnUldah.EXH -> exd/warp/warpinnuldah_xxxxx_xxxxx
String transKey = replaceFile.substring(0, replaceFile.lastIndexOf(".")).toLowerCase() + "_" + String.valueOf(listEntryIndex) + "_" + String.valueOf(stringCount);
if (this.csv) {
// added CSV mode
// need a name like quest or quest/000/ClsHrv001_00003 (replaceFile)
// need an offset (exdfDatasetSE.offset)
Integer offsetInteger = offsetMap.get(Integer.valueOf(exdfDatasetSE.offset));
String[] rowStrings = csvDataMap.get(listEntryIndex);
if (rowStrings != null) {
String readString = rowStrings[offsetInteger];
String newString = new String();
boolean isHexString = false;
if (readString != null) {
for (int i = 0; i < readString.length(); i++) {
char currentChar = readString.charAt(i);
switch (currentChar) {
case '<': {
if ((readString.charAt(i+1) == 'h') && (readString.charAt(i+2) == 'e') && (readString.charAt(i+3) == 'x')) {
if (isHexString) {
throw new Exception("TagInTagException!" + readString);
} else {
isHexString = true;
}
}
if (newString.length() > 0) {
newFFXIVString = ArrayUtil.append(newFFXIVString, newString.getBytes("UTF-8"));
newString = "";
}
newString += currentChar;
break;
}
case '>': {
newString += currentChar;
if (isHexString) {
newFFXIVString = ArrayUtil.append(newFFXIVString, HexUtils.hexStringToBytes(newString.substring(5, newString.length() - 1)));
newString = "";
isHexString = false;
}
break;
}
default: {
newString += currentChar;
}
}
}
if (newString.length() > 0) {
newFFXIVString = ArrayUtil.append(newFFXIVString, newString.getBytes("UTF-8"));
newString = "";
}
} else {
System.out.println("\t\tCannot find listEntryIndex " + String.valueOf(listEntryIndex));
newFFXIVString = ArrayUtil.append(newFFXIVString, jaBytes);
}
}
// added end ^
} else if (Config.getConfigResource("transtable") != null && Config.getProperty("transtable", transKey) != null && Config.getProperty("transtable", transKey).length() > 0) {
如果想要,前面也可以作相對應修改,讓我們可以不用在common/text
資料夾下放陸版檔案。
boolean cnEXHFileAvailable = true;
if (!this.csv) {
try {
SqPackIndexFile exhIndexFileCN = (SqPackIndexFile)((SqPackIndexFolder)indexCN.get(filePatchCRC)).getFiles().get(exhFileCRC);
byte[] exhFileCN = extractFile(this.pathToIndexCN, exhIndexFileCN.getOffset());
exhCN = new EXHFFile(exhFileCN);
// 添加對照的StringDataset
int cnDatasetPossition = 0;
if (datasetStringCount(exhSE.getDatasets()) > 0 && datasetStringCount(exhSE.getDatasets()) == datasetStringCount(exhCN.getDatasets()))
for (EXDFDataset datasetSE : exhSE.getDatasets()) {
if (datasetSE.type == 0) {
while ((exhCN.getDatasets()[cnDatasetPossition]).type != 0)
cnDatasetPossition++;
datasetMap.put(datasetSE, exhCN.getDatasets()[cnDatasetPossition++]);
}
}
} catch (Exception cnEXHFileException) {
cnEXHFileAvailable = false;
}
} else {
cnEXHFileAvailable = false;
}
if ((exhSE.getLangs()).length > 0) {
// added for CSV
HashMap<Integer, Integer> offsetMap = new HashMap<>();
// ArrayList<List<String>> dataList = new ArrayList<List<String>>();
HashMap<Integer, String[]> csvDataMap = new HashMap<>();
if (this.csv) {
try {
CsvParserSettings csvSettings = new CsvParserSettings();
csvSettings.setMaxCharsPerColumn(-1);
csvSettings.setMaxColumns(4096);
CsvParser csvParser = new CsvParser(csvSettings);
String csvPath = "resource" + File.separator + "rawexd" + File.separator + replaceFile.substring(4, replaceFile.indexOf(".")) + ".csv";
if (new File(csvPath).exists()) {
List<String[]> allRows = csvParser.parseAll(new FileReader(csvPath));
for (int i = 1; i < allRows.get(1).length; i++) {
offsetMap.put(Integer.valueOf((allRows.get(1))[i]), i - 1);
}
int rowNumber = allRows.size();
for (int i = 3; i < rowNumber; i++) {
csvDataMap.put(Integer.valueOf((allRows.get(i))[0]), Arrays.copyOfRange(allRows.get(i), 1, allRows.get(i).length));
}
} else {
System.out.println("\t\tCSV file not exists! " + csvPath);
continue;
}
} catch (Exception csvFileIndexValueException) {
System.out.println("\t\tCSV Exception. " + csvFileIndexValueException.getMessage());
continue;
}
}
然後是ReplaceThread.java
if ((new File("resource" + File.separator + "text" + File.separator + "0a0000.win32.index")).exists()) {
(new ReplaceEXDF(this.resourceFolder + File.separator + "0a0000.win32.index", "resource" + File.separator + "text" + File.separator + "0a0000.win32.index", percentPanel)).replace();
} else if ((new File("resource" + File.separator + "rawexd" + File.separator + "Achievement.csv")).exists()) {
(new ReplaceEXDF(this.resourceFolder + File.separator + "0a0000.win32.index", "resource" + File.separator + "rawexd" + File.separator + "Achievement.csv", percentPanel)).replace();
} else {
System.out.println("No resource files detected!");
}
在我們可以選擇用已漢化的包作為放進resource/text/
的檔案後,發現這樣做的時候字體包會不太一樣,導致某些圖像類的英數字區塊會出現亂碼。
因為ReplaceFont和ReplaceEXDF是處理不同的檔案,目前先用手上的漢化覆蓋檔的000000.win32....
來直接覆蓋遊戲檔案,可以解決。
如果手上有覆蓋漢化檔,可以用以下方式解決。
使用FFXIV Explorer查看漢化覆蓋檔的000000.win32.dat0裡面的檔案,然後和提摩院的漢化resource/font
資料夾內的檔案做比較。
除了完全一樣的檔案外,部分檔案在FFXIV Explorer無法正確顯示。透過比較binary內容,確定以下檔案對應:
漢化包檔案 | 覆蓋檔FFXIV Explorer拆包檔案 |
---|---|
axis_12.fdt | ~a9b7b1a2 |
axis_14.fdt | ~26f74402 |
axis_18.fdt | ~e307a903 |
axis_36.fdt | ~11ffb669 |
axis_96.fdt | ~b064950f |
miedingermid_10.fdt | ~7b3aa512 |
miedingermid_12.fdt | ~1faf672 |
miedingermid_14.fdt | ~8eba03d2 |
miedingermid_18.fdt | ~4b4aeed3 |
trumpgothic_184.fdt | ~ffa087be |
除此之外,以下檔案與原版檔案不同,且漢化包中沒有。我們猜測這些檔案和其他檔案一樣,都是font + number + .fdt的格式,所以手猜猜出了四個,最後一個是用程式窮舉直到找出結果為止。將這些檔案改名後放到resource/font
裡面,就可以解決字體問題。
覆蓋檔FFXIV Explorer拆包檔案 | 嘗試結果 |
---|---|
73CDF66F | meidinger_40.fdt |
B9B3F2B9 | miedingermid_36.fdt |
D2024114 | jupiter_90.fdt |
DC44E770 | trumpgothic_68.fdt |
E1DCA76A | jupiter_46.fdt |
可參考這篇。例如最基礎的換行指令碼(0x02100103
)基本的結構是:
02 | 10 | 01 | 03 |
---|---|---|---|
指令碼開頭 | 指令碼類型 | 指令碼長度 | 指令碼結尾 |
TagType.cs
0x01
代表XIVStringDecoder.cs
的protected static int GetInteger(BinaryReader input, List<byte> lenByte)
等函式IntegerType.cs
0xFF
開頭,後面接一個長度,格式與上類似。
XIVStringDecoder.cs
的protected INode DecodeExpression(BinaryReader input)
DecodeExpressionType.cs
另一個例子:
02 | 48 | 04 | F20215 | 03 |
---|---|---|---|---|
指令碼開頭 | 指令碼類型 (UI前景) |
長度 3 bytes |
指令內容 UI前景類型 |
指令碼結尾 |
更複雜的例子:
020851E4E80201FF25024804F2021E03024904F2021F03e78db5e99abce999a3e7879f02490201030248020103FF25024804F2022003024904F2022103e6b8a1e9b489e999a3e7879f0249020103024802010303
<hex:020851E4E80201FF25><hex:024804F2021E03><hex:024904F2021F03>獵隼陣營<hex:0249020103><hex:0248020103><hex:FF25><hex:024804F2022003><hex:024904F2022103>渡鴉陣營<hex:0249020103><hex:0248020103><hex:03>
switch case:
<hex:020957E802FF10E38199E381B9E381A6E8A1A8E7A4BAFF1FE382B3E383B3E38397E383AAE383BCE38388E381AEE381BFE8A1A8E7A4BAFF22E69CAAE382B3E383B3E38397E383AAE383BCE38388E381AEE381BFE8A1A8E7A4BA03>
<Switch(IntegerParameter(1))><Case(1)>顯示全部</Case><Case(2)>只顯示已完成</Case><Case(3)>只顯示未完成</Case></Switch>
02
指令開頭09
指令類型:Switch57
指令長度FF
指令段落點,下一個byte10
是段落長度E38199E381B9E381A6E8A1A8E7A4BA
內文,UTF-8為すべて表示
FF1F
前byte為指令段落點,後byte為段落長度E382B3E383B3E38397E383AAE383BCE38388E381AEE381BFE8A1A8E7A4BA
內文,UTF-8為コンプリートのみ表示
FF22
E69CAAE382B3E383B3E38397E383AAE383BCE38388E381AEE381BFE8A1A8E7A4BA
內文,UTF-8為未コンプリートのみ表示
03
指令結尾Update-Package -reinstall
為了讓我們可以在csv直接讀到各個column的offset,我們需要讓其輸出offset資訊。我們修改SaintCoinach.Cmd\ExdHelper.cs
:
public static void SaveAsCsv(Ex.Relational.IRelationalSheet sheet, Language language, string path, bool writeRaw) {
using (var s = new StreamWriter(path, false, Encoding.UTF8)) {
var indexLine = new StringBuilder("key");
var nameLine = new StringBuilder("#");
var offsetLine = new StringBuilder("offset"); // added
var typeLine = new StringBuilder("int32");
var colIndices = new List<int>();
foreach (var col in sheet.Header.Columns) {
indexLine.AppendFormat(",{0}", col.Index);
nameLine.AppendFormat(",{0}", col.Name);
offsetLine.AppendFormat(",{0}", col.Offset); // added
typeLine.AppendFormat(",{0}", col.ValueType);
colIndices.Add(col.Index);
}
s.WriteLine(indexLine);
s.WriteLine(nameLine);
s.WriteLine(offsetLine); // added
s.WriteLine(typeLine);
ExdHelper.WriteRows(s, sheet, language, colIndices, writeRaw);
}
}
雖然我們也可以像FFXIVChnTextPatch一樣將包在指令碼裡面的一般文字全部以hex呈現,但既然SaintCoinach做了更精細的分類,那麼沒道理不用。
首先從XivStringDecoder.cs
開始。
在SC做decode的過程中,會將代表指令長度的bytes捨去或轉換成int做處理。我們為所有Decoder增加第四個引數lengthByteStr,讓他能以hex的形式輸出這個長度。
public class XivStringDecoder {
public delegate INode TagDecoder(BinaryReader input, TagType tag, int length, String lengthByteStr);
#region Constructor
public XivStringDecoder() {
this.DefaultTagDecoder = DecodeTagDefault;
SetDecoder(TagType.Clickable, (i, t, l, h) => DecodeGenericElementWithVariableArguments(i, t, l, h, 1, int.MaxValue)); // I have no idea.
SetDecoder(TagType.Color, DecodeColor);
SetDecoder(TagType.CommandIcon, (i, t, l, h) => DecodeGenericElement(i, t, l, h, 1, false));
SetDecoder(TagType.Dash, (i, t, l, h) => new Nodes.StaticString(this.Dash));
SetDecoder(TagType.Emphasis, DecodeGenericSurroundingTag);
SetDecoder(TagType.Emphasis2, DecodeGenericSurroundingTag);
// TODO: Fixed
SetDecoder(TagType.Format, DecodeFormat);
SetDecoder(TagType.Gui, (i, t, l, h) => DecodeGenericElement(i, t, l, h, 1, false));
SetDecoder(TagType.Highlight, (i, t, l, h) => DecodeGenericElement(i, t, l, h, 0, true));
SetDecoder(TagType.If, DecodeIf);
SetDecoder(TagType.IfEquals, DecodeIfEquals);
// Indent
SetDecoder(TagType.InstanceContent, (i, t, l, h) => DecodeGenericElement(i, t, l, h, 0, true));
SetDecoder(TagType.LineBreak, (i, t, l, h) => new Nodes.StaticString("<hex:02100103>"));
SetDecoder(TagType.Sheet, (i, t, l, h) => DecodeGenericElementWithVariableArguments(i, t, l, h, 2, int.MaxValue)); // Sheet name, Row[, Column[, Parameters]+]
SetDecoder(TagType.SheetDe, (i, t, l, h) => DecodeGenericElementWithVariableArguments(i, t, l, h, 3, int.MaxValue)); // Sheet name, Attributive row, Sheet row[, Sheet column[, Attributive index[, Parameters]+]
SetDecoder(TagType.SheetEn, (i, t, l, h) => DecodeGenericElementWithVariableArguments(i, t, l, h, 3, int.MaxValue)); // Sheet name, Attributive row, Sheet row[, Sheet column[, Attributive index[, Parameters]+]
SetDecoder(TagType.SheetFr, (i, t, l, h) => DecodeGenericElementWithVariableArguments(i, t, l, h, 3, int.MaxValue)); // Sheet name, Attributive row, Sheet row[, Sheet column[, Attributive index[, Parameters]+]
SetDecoder(TagType.SheetJa, (i, t, l, h) => DecodeGenericElementWithVariableArguments(i, t, l, h, 3, int.MaxValue)); // Sheet name, Attributive row, Sheet row[, Sheet column[, Attributive index[, Parameters]+]
SetDecoder(TagType.Split, (i, t, l, h) => DecodeGenericElement(i, t, l, h, 3, false)); // Input expression, Seperator, Index to use
SetDecoder(TagType.Switch, DecodeSwitch);
SetDecoder(TagType.Time, (i, t, l, h) => DecodeGenericElement(i, t, l, h, 1, false));
SetDecoder(TagType.TwoDigitValue, (i, t, l, h) => DecodeGenericElement(i, t, l, h, 0, true));
// Unknowns
SetDecoder(TagType.Value, (i, t, l, h) => DecodeGenericElement(i, t, l, h, 0, true));
SetDecoder(TagType.ZeroPaddedValue, DecodeZeroPaddedValue);
}
修改主要Decode函式的規則如下。注意第二層Decode()也會被指令碼出現0xFF
時呼叫,所以同樣新增lenByte
──而且在這種情況下,lenByte
會含有0xFF
以及代表長度的byte(s)。
#region Decode
public XivString Decode(byte[] buffer) {
using (var ms = new MemoryStream(buffer)) {
using (var r = new BinaryReader(ms, this.Encoding))
return Decode(r, buffer.Length, new List<byte> { } );
}
}
public XivString Decode(BinaryReader input, int length, List<byte> lenByte) {
// check input size
if (length < 0)
throw new ArgumentOutOfRangeException("length");
// set the end of the input
var end = input.BaseStream.Position + length;
if (end > input.BaseStream.Length)
throw new ArgumentOutOfRangeException("length");
var parts = new List<INode>();
var pendingStatic = new List<byte>();
// Add the bytes representing decode tag (if exist) and length to pendingStatic
if (lenByte.Count > 0) {
String forText = BitConverter.ToString(lenByte.ToArray()).Replace("-", String.Empty) + ">";
parts.Add(new Nodes.StaticString(forText));
}
// while loop until reaching the end
while (input.BaseStream.Position < end) {
var v = input.ReadByte();
if (v == TagStartMarker) {
// What this function does:
// If no item in "pending", just return
// A list of interface can take any instance of that interface
// TargetParts adds an element, which is the string version of "pending"
// Finally, remove everything in "pending"
// P.S. it modifies the references directly. (list = reference type)
AddStatic(pendingStatic, parts);
// DecodeTag: byte -> string; added into "parts"
parts.Add(DecodeTag(input));
if (input.BaseStream.Position > end)
throw new InvalidOperationException();
} else
pendingStatic.Add(v);
}
AddStatic(pendingStatic, parts);
// Add <hex: if needed
if (lenByte.Count > 0) {
parts.Add(new Nodes.StaticString("<hex:"));
}
return new XivString(parts);
}
如果讀入的bytes出現代表指令碼開頭的0x02
,就會呼叫以下函式。這個函式會將輸入的binary code轉換成自定義的class INode
。
private INode DecodeTag(BinaryReader input) {
// the first byte means the tag type
var tag = (TagType)input.ReadByte();
// edited
// the second byte(s) means the length of commnad
List<byte> lengthByte = new List<byte> { };
var length = GetInteger(input, lengthByte);
String lengthByteStr = BitConverter.ToString(lengthByte.ToArray()).Replace("-", String.Empty);
var end = input.BaseStream.Position + length;
// System.Diagnostics.Trace.WriteLine(string.Format("{0} @ {1:X}h+{2:X}h", tag, input.BaseStream.Position, length));
TagDecoder decoder = null;
// ref and out:
// both means to modify the reference;
// "out" means it may not be initialized yet, so has to be done in the function.
// 這個方法傳回時,如果找到索引鍵,則包含與指定索引鍵相關聯的值,否則為 value 參數類型的預設值。這個參數會以未初始化的狀態傳遞。
_TagDecoders.TryGetValue(tag, out decoder);
// "??" operator: return the left side if it is not null; otherwise, return the right side.
// If tag in _TagDecoders, decoder will be that decoder; otherwise, it will be DefaultTagDecoder
var result = (decoder ?? DefaultTagDecoder)(input, tag, length, lengthByteStr);
if (input.BaseStream.Position != end)
{
// Triggered by two entries in LogMessage as of 3.15.
// Looks like a tag has some extra bits, as the end length is a proper TagEndMarker.
System.Diagnostics.Debug.WriteLine(string.Format("Position mismatch in XivStringDecoder.DecodeTag. Position {0} != predicted {1}.", input.BaseStream.Position, end));
input.BaseStream.Position = end;
}
if (input.ReadByte() != TagEndMarker)
throw new InvalidDataException();
return result;
}
修改各個Tag Decoder的規則:
#region Generic
protected INode DecodeTagDefault(BinaryReader input, TagType tag, int length, String lenByte) {
return new Nodes.DefaultElement(tag, input.ReadBytes(length), lenByte);
}
protected INode DecodeExpression(BinaryReader input) {
var t = input.ReadByte();
// expressionTypeByte = t;
return DecodeExpression(input, (DecodeExpressionType)t);
}
protected INode DecodeExpression(BinaryReader input, DecodeExpressionType exprType) {
var t = (byte)exprType;
if (t < 0xD0) {
return new Nodes.StaticInteger(t - 1, ((byte)t).ToString("X2"));
}
if (t < 0xE0) {
return new Nodes.TopLevelParameter(t - 1, ((byte)t).ToString("X2"));
}
List<byte> addByte = new List<byte> { };
addByte.Add((Byte)exprType);
switch (exprType) {
case DecodeExpressionType.Decode: {
var len = GetInteger(input, addByte);
// XIVString is also an INode
return Decode(input, len, addByte);
}
case DecodeExpressionType.Byte: {
var expr = GetInteger(input, IntegerType.Byte, addByte);
var lenByte = BitConverter.ToString(addByte.ToArray()).Replace("-", String.Empty);
return new Nodes.StaticInteger(expr, lenByte);
}
case DecodeExpressionType.Int16_MinusOne: {
var expr = GetInteger(input, IntegerType.Int16, addByte) - 1;
var lenByte = BitConverter.ToString(addByte.ToArray()).Replace("-", String.Empty);
return new Nodes.StaticInteger(expr, lenByte);
}
case DecodeExpressionType.Int16_1:
case DecodeExpressionType.Int16_2: {
var expr = GetInteger(input, IntegerType.Int16, addByte);
var lenByte = BitConverter.ToString(addByte.ToArray()).Replace("-", String.Empty);
return new Nodes.StaticInteger(expr, lenByte);
}
case DecodeExpressionType.Int24_MinusOne: {
var expr = GetInteger(input, IntegerType.Int24, addByte) - 1;
var lenByte = BitConverter.ToString(addByte.ToArray()).Replace("-", String.Empty);
return new Nodes.StaticInteger(expr, lenByte);
}
case DecodeExpressionType.Int24: {
var expr = GetInteger(input, IntegerType.Int24, addByte);
var lenByte = BitConverter.ToString(addByte.ToArray()).Replace("-", String.Empty);
return new Nodes.StaticInteger(expr, lenByte);
}
case DecodeExpressionType.Int24_Lsh8: {
var expr = GetInteger(input, IntegerType.Int24, addByte) << 8;
var lenByte = BitConverter.ToString(addByte.ToArray()).Replace("-", String.Empty);
return new Nodes.StaticInteger(expr, lenByte);
}
case DecodeExpressionType.Int24_SafeZero: {
var v16 = input.ReadByte();
var v8 = input.ReadByte();
var v0 = input.ReadByte();
addByte.Add(v16);
addByte.Add(v8);
addByte.Add(v0);
var lenByte = BitConverter.ToString(addByte.ToArray()).Replace("-", String.Empty);
int v = 0;
if (v16 != byte.MaxValue)
v |= v16 << 16;
if (v8 != byte.MaxValue)
v |= v8 << 8;
if (v0 != byte.MaxValue)
v |= v0;
return new Nodes.StaticInteger(v, lenByte);
}
case DecodeExpressionType.Int32: {
var expr = GetInteger(input, IntegerType.Int32, addByte);
var lenByte = BitConverter.ToString(addByte.ToArray()).Replace("-", String.Empty);
return new Nodes.StaticInteger(expr, lenByte);
}
case DecodeExpressionType.GreaterThanOrEqualTo:
case DecodeExpressionType.GreaterThan:
case DecodeExpressionType.LessThanOrEqualTo:
case DecodeExpressionType.LessThan:
case DecodeExpressionType.NotEqual:
case DecodeExpressionType.Equal: {
var left = DecodeExpression(input);
var right = DecodeExpression(input);
return new Nodes.Comparison(exprType, left, right);
}
case DecodeExpressionType.IntegerParameter:
case DecodeExpressionType.PlayerParameter:
case DecodeExpressionType.StringParameter:
case DecodeExpressionType.ObjectParameter: {
var parameter = DecodeExpression(input);
return new Nodes.Parameter(exprType, parameter);
}
default:
throw new NotSupportedException();
}
}
protected INode DecodeGenericElement(BinaryReader input, TagType tag, int length, String lenByte, int argCount, bool hasContent) {
if (length == 0) {
return new Nodes.EmptyElement(tag, lenByte);
}
var arguments = new INode[argCount];
for (var i = 0; i < argCount; ++i) {
arguments[i] = DecodeExpression(input);
}
INode content = null;
if (hasContent) {
content = DecodeExpression(input);
}
return new Nodes.GenericElement(tag, content, lenByte, arguments);
}
protected INode DecodeGenericElementWithVariableArguments(BinaryReader input, TagType tag, int length, String lenByte, int minCount, int maxCount) {
var end = input.BaseStream.Position + length;
var args = new List<INode>();
for (var i = 0; i < maxCount && input.BaseStream.Position < end; ++i) {
args.Add(DecodeExpression(input));
}
return new Nodes.GenericElement(tag, null, lenByte, args);
}
protected INode DecodeGenericSurroundingTag(BinaryReader input, TagType tag, int length, String lenByte) {
if (length != 1)
throw new ArgumentOutOfRangeException("length");
List<byte> insideLenByte = new List<byte> { };
var status = GetInteger(input, insideLenByte);
lenByte += BitConverter.ToString(insideLenByte.ToArray()).Replace("-", String.Empty);
if (status == 0)
return new Nodes.CloseTag(tag, lenByte);
if (status == 1)
return new Nodes.OpenTag(tag, lenByte, null); /* should be lenByte or insideLenByte? */
throw new InvalidDataException();
}
#endregion
}
#region Specific
protected INode DecodeZeroPaddedValue(BinaryReader input, TagType tag, int length, String lenByte) {
var val = DecodeExpression(input);
var arg = DecodeExpression(input);
return new GenericElement(tag, val, lenByte, arg);
}
protected INode DecodeColor(BinaryReader input, TagType tag, int length, String lenByte) {
var t = input.ReadByte();
// I think the byte should be added
if (length == 1 && t == 0xEC)
return new Nodes.CloseTag(tag, lenByte + t.ToString("X2"));
var color = DecodeExpression(input, (DecodeExpressionType)t);
return new Nodes.OpenTag(tag, lenByte, color);
}
protected INode DecodeFormat(BinaryReader input, TagType tag, int length, String lenByte) {
var end = input.BaseStream.Position + length;
var arg1 = DecodeExpression(input);
var arg2 = new Nodes.StaticByteArray(input.ReadBytes((int)(end - input.BaseStream.Position)));
return new Nodes.GenericElement(tag, null, lenByte, arg1, arg2);
}
protected INode DecodeIf(BinaryReader input, TagType tag, int length, String lenByte) {
var end = input.BaseStream.Position + length;
var condition = DecodeExpression(input);
INode trueValue, falseValue;
DecodeConditionalOutputs(input, (int)end, out trueValue, out falseValue);
return new Nodes.IfElement(tag, condition, trueValue, falseValue, lenByte);
}
protected INode DecodeIfEquals(BinaryReader input, TagType tag, int length, String lenByte) {
var end = input.BaseStream.Position + length;
var left = DecodeExpression(input);
var right = DecodeExpression(input);
/*
var trueValue = DecodeExpression(input);
INode falseValue = null;
if (input.BaseStream.Position != end)
falseValue = DecodeExpression(input);*/
INode trueValue, falseValue;
DecodeConditionalOutputs(input, (int)end, out trueValue, out falseValue);
return new Nodes.IfEqualsElement(tag, left, right, trueValue, falseValue, lenByte);
}
protected void DecodeConditionalOutputs(BinaryReader input, int end, out INode trueValue, out INode falseValue) {
var exprs = new List<INode>();
while (input.BaseStream.Position != end) {
var expr = DecodeExpression(input);
exprs.Add(expr);
}
// Only one instance with more than two expressions (LogMessage.en[1115][4])
// TODO: Not sure how it should be handled, discarding all but first and second for now.
if (exprs.Count > 0)
trueValue = exprs[0];
else
trueValue = null;
if (exprs.Count > 1)
falseValue = exprs[1];
else
falseValue = null;
}
protected INode DecodeSwitch(BinaryReader input, TagType tag, int length, String lenByte) {
var end = input.BaseStream.Position + length;
var caseSwitch = DecodeExpression(input);
var cases = new Dictionary<int, INode>();
var i = 1;
while (input.BaseStream.Position < end)
cases.Add(i++, DecodeExpression(input));
return new Nodes.SwitchElement(tag, caseSwitch, cases, lenByte);
}
#endregion
下面是處理指令長度的函式。在一般情況下,指令長度只會有一個byte,但若數字較大也會需要有兩個bytes的時候。
用舉例來看看下面的程式碼。當指令只有一個byte0xC8
,這個byte會被放進lenByte裡面回傳(因為List是reference type,在函式裡加東西進去在函式外也有效)。因為唯一的byte被讀完了,這時候的GetInteger(input, type, lenByte)
可以想成GetInteger(空的input, 0xC8, lenByte{0xC8})
。
#region Shared
protected static int GetInteger(BinaryReader input, List<byte> lenByte) {
// added new function
var t = input.ReadByte();
var type = (IntegerType)t;
lenByte.Add(t);
return GetInteger(input, type, lenByte);
}
protected static int GetInteger(BinaryReader input, IntegerType type, List<byte> lenByte) {
const byte ByteLengthCutoff = 0xF0;
var t = (byte)type;
if (t < ByteLengthCutoff)
return t - 1;
switch (type) {
case IntegerType.Byte: {
byte res = input.ReadByte();
lenByte.Add(res);
return (res);
}
case IntegerType.ByteTimes256: {
byte res = input.ReadByte();
lenByte.Add(res);
return (res * 256);
}
case IntegerType.Int16: {
int v = 0;
byte res = input.ReadByte();
lenByte.Add(res);
v |= res << 8;
res = input.ReadByte();
lenByte.Add(res);
v |= res;
return (v);
}
case IntegerType.Int24: {
int v = 0;
byte res = input.ReadByte();
lenByte.Add(res);
v |= res << 16;
res = input.ReadByte();
lenByte.Add(res);
v |= res << 8;
res = input.ReadByte();
lenByte.Add(res);
v |= res;
return (v);
}
case IntegerType.Int32: {
int v = 0;
byte res = input.ReadByte();
lenByte.Add(res);
v |= res << 24;
res = input.ReadByte();
lenByte.Add(res);
v |= res << 16;
res = input.ReadByte();
lenByte.Add(res);
v |= res << 8;
res = input.ReadByte();
lenByte.Add(res);
v |= res;
return (v);
}
default:
throw new NotSupportedException();
}
}
#endregion
接著修改以下相應檔案(附例子):
DefaultElement.cs
EmptyElement.cs
GenericElement.cs
StaticInteger.cs
199
→ C8
CloseTag.cs
、OpenTag.cs
<Emphasis></Emphasis>
和<Color(數字)></color>
兩種狀況呼叫。<Emphasis><Value>IntegerParameter(1)</Value></Emphasis>
→ <hex:021A020203>
<Value>IntegerParameter(1)</Value>
<hex:021A020103>
<Color(-15523537)><Unknown14>FEFFB1BACD</Unknown14>選擇大國防聯軍<Unknown14>EC</Unknown14></Color>
→ <hex:021306FEFF13212F03>
<Unknown14>FEFFB1BACD</Unknown14>選擇大國防聯軍<Unknown14>EC</Unknown14>
<hex:021302EC03>
IfElement.cs
、IfEqualsElement.cs
<If(GreaterThan(IntegerParameter(1),9999))>9,999+<Else/><Format(IntegerParameter(1),FF022C)/></If>
<hex:02081A
GreaterThan(IntegerParameter(1),9999)
FF07>9,999+<hex:FF0A>
<Format(IntegerParameter(1),FF022C)/>
<hex:03>
SwitchElement.cs
<Switch(IntegerParameter(2))><Case(1)>格里達尼亞新街</Case><Case(2)>彎枝牧場</Case><Case(3)>霍桑山寨</Case><Case(4)>石場水車</Case><Case(5)>恬靜路營地</Case><Case(6)>秋瓜浮村</Case></Switch>
→ <hex:020963E803FF16>格里達尼亞新街<hex:FF0D>彎枝牧場<hex:FF0D>霍桑山寨<hex:FF0D>石場水車<hex:FF10>恬靜路營地<hex:FF0D>秋瓜浮村<hex:03>
TopLevelParameter.cs
DecodeExpression
裡面,當代表表示式類型的byte在0xD0
到0xDF
之間(含)時呼叫。TopLevelParameter(222)
→ DF
Comparison.cs
Equal(IntegerParameter(1),1)
→ E4
IntegerParameter(1),1
ArgumentCollection.cs
(IntegerParameter(1),1)
→ IntegerParameter(1)
02
Parameter.cs
IntegerParameter(1)
→ E802
通常是將各個INode新增代表長度的byte(原本程式會將這些bytes轉成人類可讀的數字,但現在我們要繼續保留HEX的形式),方便我們output時使用。
需要修改的地方包括properties、get functions、ToString()
。以DefaultElement.cs
為例。我們增加了作為String傳入的lenByte
,接著修改ToString()
函數。
namespace SaintCoinach.Text.Nodes {
public class DefaultElement : INode {
private readonly TagType _Tag;
private readonly StaticByteArray _Data;
private readonly String _LenByte;
public TagType Tag { get { return _Tag; } }
public INode Data { get { return _Data; } }
public String LenByte { get { return _LenByte; } }
NodeFlags INode.Flags { get { return NodeFlags.IsStatic; } }
public DefaultElement(TagType tag, byte[] innerBuffer, String lenByte) {
_Tag = tag;
_Data = new StaticByteArray(innerBuffer);
_LenByte = lenByte;
}
public override string ToString() {
var sb = new StringBuilder();
ToString(sb);
return sb.ToString();
}
public void ToString(StringBuilder builder) {
// edit here!!!!
builder.Append(StringTokens.TagOpen);
builder.Append("hex:02");
builder.Append(((byte)Tag).ToString("X2")); /* X means hex, 2 means 2-digit */
builder.Append(LenByte);
if (_Data.Value.Length == 0) {
builder.Append("03");
builder.Append(StringTokens.TagClose);
}
else {
_Data.ToString(builder);
builder.Append("03");
builder.Append(StringTokens.TagClose);
}
/*
builder.Append("DefaultElement");
builder.Append(StringTokens.TagOpen);
builder.Append(Tag);
if (_Data.Value.Length == 0) {
builder.Append(StringTokens.ElementClose);
builder.Append(StringTokens.TagClose);
} else {
builder.Append(StringTokens.TagClose);
_Data.ToString(builder);
builder.Append(StringTokens.TagOpen);
builder.Append(StringTokens.ElementClose);
builder.Append(Tag);
builder.Append(StringTokens.TagClose);
}
*/
}
public T Accept<T>(SaintCoinach.Text.Nodes.INodeVisitor<T> visitor) {
return visitor.Visit(this);
}
}
}
在Eclipse輸出jar:
JAR轉exe:
C:\Users\User\.p2\pool\plugins\org.eclipse.justj.openjdk.hotspot.jre.full.win32.x86_64_16.0.1.v20210528-1205\jre
的bin和lib資料夾複製進和exe同層的jre
資料夾jre
Check dependencies:
jdeps -s <輸出的jar>
Example Output:
FFXIVChnTextPatch-0211.jar -> java.base
FFXIVChnTextPatch-0211.jar -> java.desktop
FFXIVChnTextPatch-0211.jar -> java.logging
FFXIVChnTextPatch-0211.jar -> java.naming
FFXIVChnTextPatch-0211.jar -> java.security.jgss
FFXIVChnTextPatch-0211.jar -> java.sql
FFXIVChnTextPatch-0211.jar -> not found
Produce JRE:
jlink --module-path <JDK module path> --add-modules <listed modules> --output <output path>
Example:
jlink --module-path "D:\Java\jdk-16.0.1\jmods" --add-modules java.base,java.desktop,java.logging,java.naming,java.security.jgss,java.sql --output ".\jre"
到這邊,基本上就解決了所有手動漢化會遇到的問題。
除了直接進入遊戲以外,也可以用「迴圈」測試自己有沒有漢化正確。通常會出問題的地方在於SaintCoinach對於指令碼的處理。
如果以上步驟會出問題,就表示漢化處理上有失敗的地方,即使進入遊戲也無法正常讀取。
這是一開始用GitHub版本的漢化器的bug,後來改用反編譯版本後已不需要,留做參考。
嘗試一、把下面這行註解掉(後來發現下面的解決方法,這個方法僅留供參考)
percentPanel.percentShow((double)(++fileCount) / (double)fileList.size());
嘗試二、在ReplaceEXDF.java新增this.percentPanel = percentPanel;
public ReplaceEXDF(String pathToIndexSE, String pathToIndexCN, PercentPanel percentPanel) {
this.pathToIndexSE = pathToIndexSE;
this.pathToIndexCN = pathToIndexCN;
this.fileList = new ArrayList<String>();
this.exQuestMap = new HashMap();
this.transMap = new HashMap<>();
this.lang = Config.getProperty("Language");
// added
this.percentPanel = percentPanel;
}
(4948216 - 4297736 = 650480)
行(一行16 bytes)。用一個簡單的程式證明覆蓋檔擁有漢化器所注入的字型,只是散落四處:
a = {}
b = []
with open('Han.txt', 'r') as f:
number = 0
for line in f:
a[line] = number
number += 1
with open('AfterProcess Trim.txt', 'r') as f:
for line in f:
if line in a:
# print(line, a[line])
pass
else:
print("line not found!", line)
結果沒有print出任何東西。