創作內容

10 GP

FFXIV手動製作漢化筆記

作者:GPoint│2021-07-16 13:47:31│巴幣:1,116│人氣:3641
本文原寫於HackMD


因為小屋創作似乎不能放程式碼,所以建議移步HackMD觀看。

1 背景知識
1. FFXIV資料格式
2. Java
3. C#

2 FFXIVChnTextPatch
  1. 從Github下載FFXIVChnTextPatch
  2. 安裝Java SDK(我是用Java OpenJDK 16.0.1)
  3. 安裝Java的IDE(我是用Eclipse 2021-06版本)
  4. 安裝Dependencies,兩種選擇:
  5. 專案瀏覽器中右鍵點擊資料夾,內容>Java建置路徑,在類別路徑選擇新增外部jar。所需要的jar列表可以在gradle找到。
  6. 也可以考慮將專案轉換成maven project,可以在pom.xml中設定dependencies。

2.1 Java Decompiler
Java Decompiler is used to get the source codes of latest version.

我們將decompile的檔案拆包,覆蓋原本的檔案(`FFXIVChnTextPatch\src\main\java\`)。

清除專案重新build,發現無法執行。用和前面一樣的方法新增`commons-logging-1.2`這個jar,運行成功。


2.2 修改程式碼
在做任何修改之前,實際先測試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:
```java=85
// 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;`
```java=136
try {
    exdFileJA = extractFile(this.pathToIndexSE, exdIndexFileJA.getOffset());
} catch (Exception jaEXDFileException) {
    continue;
}
```
這樣就可以正常運行了。
類似的結構在EXDFUtil.java的210行、272行等地方也有出現。如果想防止意外,也可以都做修改。建議搜尋` catch (Exception jaEXDFileException) `。


2.3 修改:吃國際版漢化過的檔案
參考這篇巴哈小屋文章

為了方便能同時吃未漢化和漢化過的檔案,我們將抽取處也設定進config裡面。
要修改以下檔案
  • ConfigApplicationPanel.java
  • EXDFUtil.java
  • ReplaceEXDF.java

2.4 修改:指針
修正指針不會跑的問題,如下。

ReplaceEXDF.java
```java=105
// edited
// this.percentPanel.percentShow(++fileCount / this.fileList.size());
percentPanel.percentShow((double)(++fileCount) / (double)fileList.size());
```

2.5 移除提摩院更新
FFXIVPatchMain.java移除以下部分
```java=21
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有關的部分:
  • ConfigApplicationPanel.java
  • ReplaceEXDF.java
  • ReplaceThread.java
  • RollbackThread.java
  • TextPatchPanel.java

將與`TeemoUpdateVo`和`update`有關的行列刪掉或註解掉,然後依序檢查跳錯誤的地方,最後將import去除。

另外,ConfigApplicationPanel.java中的資源版本項也可以一併註解掉。
```java=58
/*
private JLabel transModeLable = new JLabel("资源版本");
private JComboBox<String> transModeVal;
*/
```

2.6 EXD/QUEST.EXH
任務名稱還是原文沒有翻譯。推測是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條件:
```java=269
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));
}
```
```java=341
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;
}
```

2.7 讀取CSV
讀取CSV的功能我使用了Univocity Parser,請記得在ReplaceEXDF.java加上import。

ConfigApplicationPanel.java
```java=152
// 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
```java=3
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用的內容
```java=162
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 - 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;
        }
    }
```
```java=269
// 更新文本內容
// 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`資料夾下放陸版檔案。

```java=153
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 - 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
```java=39
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!");
}
```

2.8 字型問題
在我們可以選擇用已漢化的包作為放進`resource/text/`的檔案後,發現這樣做的時候字體包會不太一樣,導致某些圖像類的英數字區塊會出現亂碼。



因為ReplaceFont和ReplaceEXDF是處理不同的檔案,目前先用手上的漢化覆蓋檔的`000000.win32....`來直接覆蓋遊戲檔案,可以解決。

如果手上有覆蓋漢化檔,可以用以下方式解決。

2.8.1 拆包替換
使用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


2.9 指令碼處理
可參考這篇。例如最基礎的換行指令碼(`0x02100103`)基本的結構是:
02 10 01 03
指令碼開頭 指令碼類型 指令碼長度 指令碼結尾

  • 指令碼類型可以參考SaintCoinach的`TagType.cs`
  • 指令碼長度$n$是指接下來的$n - 1$的byte屬於指令內,不含指令碼結尾,例如`0x01`代表$1-1=0$
  • 特殊指令碼長度可以參考SaintCoinach的`XIVStringDecoder.cs`的`protected static int GetInteger(BinaryReader input, List<byte> lenByte)`等函式
    • `IntegerType.cs`
  • 指令碼中的特殊指令碼(例如If判斷句)會以`0xFF`開頭,後面接一個長度,格式與上類似。
    • `XIVStringDecoder.cs`的`protected INode DecodeExpression(BinaryReader input)`
    • `DecodeExpressionType.cs`
  • 其他關於SaintCoinach的實作和修改列於後面章節。

另一個例子:
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` 指令類型:Switch
  • `57` 指令長度
  • `FF` 指令段落點,下一個byte`10`是段落長度
  • `E38199E381B9E381A6E8A1A8E7A4BA` 內文,UTF-8為`すべて表示`
  • `FF1F` 前byte為指令段落點,後byte為段落長度
  • `E382B3E383B3E38397E383AAE383BCE38388E381AEE381BFE8A1A8E7A4BA` 內文,UTF-8為`コンプリートのみ表示`
  • `FF22`
  • `E69CAAE382B3E383B3E38397E383AAE383BCE38388E381AEE381BFE8A1A8E7A4BA` 內文,UTF-8為`未コンプリートのみ表示`
  • `03` 指令結尾

3 SaintCoinach
  1. 安裝Visual Studio
  2. 匯入專案
  3. 工具 > Nuget > 套件管理器主控台
  4. 輸入 `Update-Package -reinstall`
參考資料來源:https://docs.microsoft.com/zh-tw/nuget/consume-packages/reinstalling-and-updating-packages

3.1 讓SaintCoinach可以輸出Offset
為了讓我們可以在csv直接讀到各個column的offset,我們需要讓其輸出offset資訊。我們修改`SaintCoinach.Cmd\ExdHelper.cs`:

```csharp=16
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);
    }
}
```

3.2 修改輸出格式
雖然我們也可以像FFXIVChnTextPatch一樣將包在指令碼裡面的一般文字全部以hex呈現,但既然SaintCoinach做了更精細的分類,那麼沒道理不用。

首先從`XivStringDecoder.cs`開始。

在SC做decode的過程中,會將代表指令長度的bytes捨去或轉換成int做處理。我們為所有Decoder增加第四個引數lengthByteStr,讓他能以hex的形式輸出這個長度。

```csharp=11
public class XivStringDecoder {
    public delegate INode TagDecoder(BinaryReader input, TagType tag, int length, String lengthByteStr);
```
```csharp=53
#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)。
```csharp=97
#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`。

```csharp=159
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的規則:
```csharp=192
#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 = 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
}
```
```csharp=+
#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的時候。

用舉例來看看下面的程式碼。當指令只有一個byte`0xC8`,這個byte會被放進lenByte裡面回傳(因為List是reference type,在函式裡加東西進去在函式外也有效)。因為唯一的byte被讀完了,這時候的`GetInteger(input, type, lenByte)`可以想成`GetInteger(空的input, 0xC8, lenByte{0xC8})`。

```csharp=+
#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()`函數。

```csharp=7
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);
        }
    }
}

```

4 結語
到這邊,基本上就解決了所有手動漢化會遇到的問題。

引用網址:https://home.gamer.com.tw/TrackBack.php?sn=5209820
All rights reserved. 版權所有,保留一切權利

相關創作

同標籤作品搜尋:FF14|FFXIV|漢化

留言共 3 篇留言

Q拔
大佬好厲害,SaintCoinach那邊的指令句轉換我是用暴力法將字串提出來,之前還煩惱怎麼把它弄好一點說XDD

08-20 14:19

GPoint
不敢,還是靠大大的文章帶我入門XD09-10 23:01
悠閒型秋嗄
大佬安安,想問一下5.55版本劇情去到扎杜诺尔高原左右的地方劇情開始沒有漢化是正常的嗎?

10-31 20:47

LOVe高橋李依
這code....
太强了....

12-29 14:28

我要留言提醒:您尚未登入,請先登入再留言

10喜歡★gpointdesu 可決定是否刪除您的留言,請勿發表違反站規文字。

追蹤私訊切換新版閱覽

作品資料夾

hyzgdivina喜歡虹咲的LLer
我的小屋裡有很多又香又甜的Hoenn繪師虹咲漫畫翻譯喔!歡迎LoveLiver來我的小屋裡坐坐~看更多我要大聲說1小時前


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】