多语言mod推进
This commit is contained in:
parent
da34fe20a8
commit
263204736f
@ -4,10 +4,11 @@
|
||||
|
||||
基于 TH1 现有的多语言系统架构,本创意工坊方案的设计核心为**“现有语种的局部文本替换”**。为了保证系统的稳定性和玩家创作的低门槛,明确以下设计边界:
|
||||
|
||||
1. **仅替换,不新增**:多语言 Mod 仅用于修改和润色游戏中已存在的语言(如 ZH, EN, JP 等),不支持通过 Mod 新增全新的语言枚举。
|
||||
2. **无需处理字体**:因为不新增语种,所有 Mod 的显示将直接复用游戏本体中该语言对应的原有字体(TMP_FontAsset)以及排版配置,创作者完全不需要考虑字体渲染和缺失问题。
|
||||
1. **仅替换,不新增**:多语言 Mod 仅用于修改和润色游戏中已存在的语言(如 ZH, EN, JP 等),不支持通过 Mod 新增全新的语言枚举。但系统提供了一个特殊的 **Custom**(自定义)语种,专供 Mod 应用时使用——它不参与游戏本体的多语言流程,只作为 Mod 目标语言的一个额外选项。
|
||||
2. **无需处理字体**:因为不新增语种,所有 Mod 的显示将直接复用游戏本体中该语言对应的原有字体(TMP_FontAsset)以及排版配置,创作者完全不需要考虑字体渲染和缺失问题。Custom 语种回退到 EN 字体。
|
||||
3. **支持增量/部分替换**:Mod 允许只修改一部分文本。玩家不需要翻译游戏内的所有内容,翻译了哪几条,游戏运行时就仅替换那几条,未修改的内容保持游戏默认文本。
|
||||
4. **多 Mod 冲突处理**:当玩家同时订阅了多个针对同一语种的翻译 Mod 时,系统将按照读取顺序依次进行替换,后读取的 Mod 翻译会覆盖先读取的 Mod 翻译(按序替换机制)。
|
||||
4. **多 Mod 优先级处理**:玩家可以为每个语种单独配置一组 Mod 列表及其优先级。低优先级 Mod 先应用,高优先级 Mod 后应用(覆盖低优先级)。Mod 本身不绑定语种——任意 Mod 可以被分配给任意语种。此配置存储在 `GameConfig.ModLanguageConfigs` 中,由 UI 或编辑器面板负责设置。
|
||||
5. **Mod 与语种解耦**:导出模板时,玩家可以分别指定"目标语种"(Mod 最终要修改的语种)和"参考语种"(CSV 中显示的参照文本列)。mod_info 记录创作者声明的目标语种,但在应用阶段可以被玩家的优先级配置覆盖。
|
||||
|
||||
---
|
||||
|
||||
@ -16,11 +17,24 @@
|
||||
对于想要参与多语言优化的创作者,整体流程分为三个简单的步骤,所有操作均设计为“零门槛”。
|
||||
|
||||
### 步骤一:一键获取 Mod 源文件
|
||||
玩家无需寻找游戏目录或解包游戏,直接在游戏运行时的客户端内(例如“主菜单”或“Mod 菜单”中)点击**“获取翻译模板”**按钮。
|
||||
系统会自动在玩家的电脑本地生成一个标准化的 Mod 源文件文件夹,里面包含了游戏中当前所有可翻译内容的对照表,以及必要的 Mod 配置文件。
|
||||
玩家无需寻找游戏目录或解包游戏,在游戏客户端内点击**"获取翻译模板"**按钮,并指定**目标语种**(要修改的语言)和**参考语种**(CSV 中显示的参照列)。
|
||||
系统会自动在本地生成标准化的 Mod 源文件文件夹,包含所有可翻译内容的对照表及 Mod 配置文件。
|
||||
|
||||
### 步骤二:本地编辑翻译内容
|
||||
玩家打开生成的本地文件夹,找到对应的翻译表格文件(例如 CSV 格式)。
|
||||
玩家打开生成的本地文件夹,找到对应的翻译表格文件(CSV 格式)。
|
||||
表格共 4 列:
|
||||
* **ID**:游戏内多语言系统分配的唯一标识符(不可修改)。
|
||||
* **EN**:英文文本,作为翻译标准参考。
|
||||
* **{参考语种}**:玩家指定的参考语言列,供对照(修改无效)。
|
||||
* **Translation**:创作者填写的翻译内容。**只有此列有内容的行才会在游戏中生效替换;留空则保持原文。**
|
||||
|
||||
玩家只需使用 Excel 或任意文本编辑器,将润色后的文本填入 Translation 列即可。
|
||||
|
||||
### 步骤三:上传至创意工坊
|
||||
玩家完成本地编辑,并在文件夹内准备好一张预览图(封面图)后,回到游戏客户端中。
|
||||
点击**"上传 Mod"**按钮,在游戏内 UI 中选择刚刚编辑好的文件夹,填写好 Mod 的名称和简介,即可一键打包并上传到 Steam 创意工坊,供其他玩家订阅。“主菜单”或“Mod 菜单”中)点击**“获取翻译模板”**按钮。
|
||||
|
||||
### 步骤三:上传至创意工坊
|
||||
表格中会清晰地列出每条文本的**“唯一 ID”**、**“原文参考”**以及留空的**“翻译内容列”**。
|
||||
玩家只需要使用 Excel 或任意文本编辑器,将自己润色或修改后的文本填入“翻译内容列”即可。不想修改的条目直接留空,系统会自动忽略。
|
||||
|
||||
@ -44,13 +58,24 @@
|
||||
一个标准的多语言 Mod 文件夹在本地包含以下三个核心文件:
|
||||
|
||||
1. **mod_info 配置**
|
||||
用于声明该 Mod 的基础元数据,主要包括:Mod 的标题、作者,以及**最核心的:该 Mod 目标替换的语种类型**(必须为游戏中已存在的语种枚举名称,如 EN)。
|
||||
|
||||
2. **translation 翻译数据表**
|
||||
这是 Mod 的核心数据载体。表结构包含三列:
|
||||
* **ID**:游戏内多语言系统分配的唯一标识符(不可修改)。
|
||||
* **原文参考**:游戏当前版本该 ID 对应的官方文本(供玩家参考,修改无效)。
|
||||
* **翻译内容**:创作者填写的全新文本。只有此列有内容的行,才会在游戏中生效替换。
|
||||
用于声明该 Mod 的基础元数据,主要包括:Mod 的标题、作者,以及该 Mod 创作者声明的目标替换语种(如 EN、ZH、Custom 等)。注意:在应用阶段,玩家可通过优先级配置将任意 Mod 用于任意语种,覆盖此声明。
|
||||
|
||||
2. **translation 翻译数据表(CSV 格式,4 列)**
|
||||
这是 Mod 的核心数据载体。表结构如下:
|
||||
|
||||
| 列名 | 说明 |
|
||||
|--------------|----------------------------------------------------|
|
||||
| ID | 游戏内唯一标识符(不可修改) |
|
||||
| EN | 英文原文,作为翻译标准参考(修改无效) |
|
||||
| {参考语种} | 玩家导出时指定的参考语言原文(修改无效) |
|
||||
| Translation | 创作者填写的翻译内容。**非空才生效**,空则跳过 |
|
||||
|
||||
示例(目标 ZH,参考 EN):
|
||||
```
|
||||
ID,EN,EN,Translation
|
||||
item_sword_name,Iron Sword,Iron Sword,铁剑
|
||||
item_shield_name,Wooden Shield,Wooden Shield,
|
||||
```
|
||||
|
||||
3. **preview 封面图**
|
||||
一张用于展示在 Steam 创意工坊列表中的预览图片。
|
||||
@ -59,16 +84,54 @@
|
||||
|
||||
## 5. 运行时读取与覆盖流程
|
||||
|
||||
为了让 Mod 生效,游戏在启动时的底层数据处理逻辑将遵循以下顺序:
|
||||
```
|
||||
游戏启动
|
||||
↓
|
||||
MultilingualManager.Init()
|
||||
→ 加载官方多语言数据(Resources/Export/Multilingual)
|
||||
↓
|
||||
ApplyWorkshopMods()
|
||||
→ 读取 GameConfig.ModLanguageConfigs(玩家的语种-Mod 优先级配置)
|
||||
↓
|
||||
WorkshopModLoader.ApplyModsWithConfig(data, configs)
|
||||
对每个语种配置:
|
||||
按列表顺序(低→高优先级)依次应用各 Mod:
|
||||
读取 mod_info.json → 确定目标语种(优先使用配置覆盖,其次用 mod_info 声明)
|
||||
读取 translation.csv → 仅处理 Translation 列非空的行
|
||||
在内存中覆盖对应 ID 的目标语种字段
|
||||
↓
|
||||
ChangedMultilingual(currentType) → 刷新所有 UI 文本
|
||||
```
|
||||
|
||||
1. **基础数据加载**:游戏启动,多语言管理器首先读取原生的多语言数据,完成官方默认多语言字典的初始化。
|
||||
2. **获取订阅列表**:通过 Steamworks API,获取当前玩家已订阅且下载完毕的所有多语言 Mod 文件夹的本地路径。
|
||||
3. **数据按序解析与覆盖**:
|
||||
* 遍历这些 Mod 文件夹,读取其中的 `mod_info` 配置文件,确认其目标替换的语种。
|
||||
* 读取翻译数据表,按行解析。
|
||||
* 如果该行的“翻译内容”不为空,则在内存中找到对应 ID 的多语言数据节点。
|
||||
* 强制将该节点下对应的目标语种字段(例如 `EN` 字段)的值替换为玩家提供的新文本。
|
||||
4. **UI 渲染与刷新**:数据覆盖完成后,触发系统的 UI 文本刷新机制。所有 UI 组件将读取内存中最新的字典数据进行展示。
|
||||
**关于嵌套引用的兼容性:**“翻译内容”不为空,则在内存中找到对应 ID 的多语言数据节点。
|
||||
**关于嵌套引用的兼容性:**
|
||||
原系统的 `**<id>**` 专有名词嵌套引用机制与 Mod 系统完全兼容。只要玩家在 Translation 列保留了 `**<id>**` 格式,运行时底层仍会正常触发解析,无需任何特殊处理。
|
||||
|
||||
**关于嵌套引用的特殊说明:**
|
||||
原系统中的 `**<id>**` 专有名词嵌套引用机制在 Mod 系统中完美兼容。只要创作者在填写的翻译内容中保留了这种富文本语法格式,运行时底层在获取该字符串时,仍会正常触发原有的解析逻辑,自动将其替换为带颜色和图标的动态内容,无需进行任何特殊处理。
|
||||
---
|
||||
|
||||
## 6. Workshop 浏览器(编辑器功能)
|
||||
|
||||
由于游戏未正式发售导致创意工坊页面无法直接在 Steam 客户端访问,编辑器内提供了 **Workshop 浏览器标签页**(⑤ 工坊浏览):
|
||||
|
||||
- **查询**:分页查询当前 AppId 对应创意工坊中的所有 Mod(通过 `SteamUGC.CreateQueryAllUGCRequest`)。
|
||||
- **订阅/取消订阅**:直接在编辑器内操作,Steam 客户端自动处理下载/删除。
|
||||
- **状态显示**:显示每个 Mod 的订阅状态、安装状态、本地路径、标签、投票数等信息。
|
||||
- **驱动机制**:编辑器通过 `EditorApplication.update` 定期调用 `SteamAPI.RunCallbacks()` 驱动 Steamworks 异步回调,无需运行游戏。
|
||||
|
||||
核心类:`Logic.Multilingual.WorkshopModBrowser`(单例)
|
||||
|
||||
---
|
||||
|
||||
## 7. 编辑器测试面板(WorkshopModEditorWindow)
|
||||
|
||||
菜单路径:`TH1工具 → 创意工坊多语言 Mod 测试面板`
|
||||
|
||||
| 标签页 | 功能说明 |
|
||||
|---------------|------------------------------------------------------------------------|
|
||||
| ① 导出模板 | 指定目标语言 + 参考语言,导出 4 列 CSV 模板(Translation 列留空) |
|
||||
| ② 上传Workshop| 选择 Mod 文件夹上传到 Steam 创意工坊 |
|
||||
| ③ Mod优先级 | 模拟为每个语种分配 Mod 并调整优先级(▲▼排序、添加/移除) |
|
||||
| ④ 应用Mod | 按优先级配置或批量应用本地/订阅 Mod,支持目标语言覆盖 |
|
||||
| ⑤ 工坊浏览 | 查询当前 AppId 所有 Workshop Mod,支持订阅/取消订阅 |
|
||||
| ⑥ CSV测试 | 4 列 CSV 读写回环测试(含特殊字符验证) |
|
||||
| ⑦ 数据检视 | 查看多语言数据概览、按 ID 查询、检视 Mod 文件夹内容(4 列格式) |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ExcelConfig;
|
||||
@ -16,6 +17,7 @@ using Logic.Multilingual;
|
||||
using TH1_Logic.Tools;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace TH1_Logic.Config
|
||||
{
|
||||
public class ConfigManager
|
||||
@ -181,6 +183,20 @@ namespace TH1_Logic.Config
|
||||
}
|
||||
|
||||
|
||||
[Serializable]
|
||||
public class ModLanguageConfig
|
||||
{
|
||||
public MultilingualType Language;
|
||||
/// <summary>
|
||||
/// 按优先级排序的 Mod 文件夹路径列表
|
||||
/// 索引 0 = 最低优先级(最先应用),最后一项 = 最高优先级(最后应用,可覆盖前面的)
|
||||
/// </summary>
|
||||
public List<string> ModPaths = new List<string>();
|
||||
|
||||
public ModLanguageConfig() { }
|
||||
public ModLanguageConfig(MultilingualType language) { Language = language; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class GameConfig
|
||||
{
|
||||
@ -196,6 +212,8 @@ namespace TH1_Logic.Config
|
||||
private bool _keyMomentEnabled;
|
||||
[SerializeField]
|
||||
private bool _bgmContinuousPlay;
|
||||
[SerializeField]
|
||||
private List<ModLanguageConfig> _modLanguageConfigs = new List<ModLanguageConfig>();
|
||||
private bool _isChanged;
|
||||
public bool IsChanged => _isChanged;
|
||||
|
||||
@ -274,6 +292,76 @@ namespace TH1_Logic.Config
|
||||
_showReminder = true;
|
||||
_keyMomentEnabled = true;
|
||||
_bgmContinuousPlay = false;
|
||||
_modLanguageConfigs = new List<ModLanguageConfig>();
|
||||
}
|
||||
|
||||
// ── Mod 优先级配置接口 ──
|
||||
|
||||
/// <summary>获取所有语种的 Mod 配置列表(只读)</summary>
|
||||
public List<ModLanguageConfig> ModLanguageConfigs => _modLanguageConfigs;
|
||||
|
||||
/// <summary>获取指定语种的 Mod 配置,不存在则创建</summary>
|
||||
public ModLanguageConfig GetOrCreateModConfig(MultilingualType language)
|
||||
{
|
||||
var config = _modLanguageConfigs.Find(c => c.Language == language);
|
||||
if (config != null) return config;
|
||||
config = new ModLanguageConfig(language);
|
||||
_modLanguageConfigs.Add(config);
|
||||
_isChanged = true;
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>设置指定语种的 Mod 优先级列表(覆盖现有)</summary>
|
||||
public void SetModsForLanguage(MultilingualType language, List<string> orderedPaths)
|
||||
{
|
||||
var config = GetOrCreateModConfig(language);
|
||||
config.ModPaths = new List<string>(orderedPaths);
|
||||
_isChanged = true;
|
||||
}
|
||||
|
||||
/// <summary>向指定语种添加一个 Mod(加到列表末尾,最高优先级)</summary>
|
||||
public void AddModToLanguage(MultilingualType language, string modPath)
|
||||
{
|
||||
var config = GetOrCreateModConfig(language);
|
||||
if (!config.ModPaths.Contains(modPath))
|
||||
{
|
||||
config.ModPaths.Add(modPath);
|
||||
_isChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从指定语种移除一个 Mod</summary>
|
||||
public void RemoveModFromLanguage(MultilingualType language, string modPath)
|
||||
{
|
||||
var config = _modLanguageConfigs.Find(c => c.Language == language);
|
||||
if (config == null) return;
|
||||
if (config.ModPaths.Remove(modPath)) _isChanged = true;
|
||||
}
|
||||
|
||||
/// <summary>移动指定语种 Mod 的优先级(向上提升=降低索引,向下降低=增加索引)</summary>
|
||||
public void MoveModPriority(MultilingualType language, int fromIndex, int toIndex)
|
||||
{
|
||||
var config = _modLanguageConfigs.Find(c => c.Language == language);
|
||||
if (config == null) return;
|
||||
var paths = config.ModPaths;
|
||||
if (fromIndex < 0 || fromIndex >= paths.Count) return;
|
||||
if (toIndex < 0 || toIndex >= paths.Count) return;
|
||||
var item = paths[fromIndex];
|
||||
paths.RemoveAt(fromIndex);
|
||||
paths.Insert(toIndex, item);
|
||||
_isChanged = true;
|
||||
}
|
||||
|
||||
/// <summary>清除指定语种的所有 Mod 配置</summary>
|
||||
public void ClearModsForLanguage(MultilingualType language)
|
||||
{
|
||||
var config = _modLanguageConfigs.Find(c => c.Language == language);
|
||||
if (config == null) return;
|
||||
if (config.ModPaths.Count > 0)
|
||||
{
|
||||
config.ModPaths.Clear();
|
||||
_isChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,7 @@ namespace Logic.Multilingual
|
||||
ES, // 西班牙语
|
||||
PT, // 葡萄牙语
|
||||
FR, // 法语
|
||||
Custom, // 自定义语种(仅供 Mod 应用使用,不参与常规多语言流程)
|
||||
Max,
|
||||
}
|
||||
|
||||
@ -73,6 +74,7 @@ namespace Logic.Multilingual
|
||||
MultilingualType.ES => group.ESFont,
|
||||
MultilingualType.PT => group.PTFont,
|
||||
MultilingualType.FR => group.FRFont,
|
||||
MultilingualType.Custom => group.ENFont, // Custom 语种回退到 EN 字体
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@ -405,6 +407,7 @@ namespace Logic.Multilingual
|
||||
public string ES;
|
||||
public string PT;
|
||||
public string FR;
|
||||
public string Custom; // 自定义语种(仅供 Mod 应用使用)
|
||||
public bool IsProperNoun;
|
||||
public bool IsDialogue;
|
||||
public string DialogueSpeaker;
|
||||
@ -435,6 +438,7 @@ namespace Logic.Multilingual
|
||||
ES = ES?.Replace("\r\n", "\n") ?? string.Empty;
|
||||
PT = PT?.Replace("\r\n", "\n") ?? string.Empty;
|
||||
FR = FR?.Replace("\r\n", "\n") ?? string.Empty;
|
||||
Custom = Custom?.Replace("\r\n", "\n") ?? string.Empty;
|
||||
DialogueSpeaker = DialogueSpeaker?.Replace("\r\n", "\n") ?? string.Empty;
|
||||
Desc = Desc?.Replace("\r\n", "\n") ?? string.Empty;
|
||||
}
|
||||
@ -452,6 +456,7 @@ namespace Logic.Multilingual
|
||||
MultilingualType.ES => ES,
|
||||
MultilingualType.PT => PT,
|
||||
MultilingualType.FR => FR,
|
||||
MultilingualType.Custom => Custom,
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
@ -477,6 +482,7 @@ namespace Logic.Multilingual
|
||||
MultilingualType.ES => !string.IsNullOrEmpty(ES),
|
||||
MultilingualType.PT => !string.IsNullOrEmpty(PT),
|
||||
MultilingualType.FR => !string.IsNullOrEmpty(FR),
|
||||
MultilingualType.Custom => !string.IsNullOrEmpty(Custom),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* @Author: 白哉
|
||||
* @Description: 创意工坊 Mod 浏览器(查询当前 AppId 的所有 Workshop 物品,支持订阅/取消订阅)
|
||||
* @Date: 2026年04月16日
|
||||
* @Modify:
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Logic.CrashSight;
|
||||
using Steamworks;
|
||||
|
||||
namespace Logic.Multilingual
|
||||
{
|
||||
/// <summary>
|
||||
/// Workshop 物品的简要信息(用于列表展示)
|
||||
/// </summary>
|
||||
public class WorkshopModItem
|
||||
{
|
||||
public PublishedFileId_t FileId;
|
||||
public string Title;
|
||||
public string Description;
|
||||
public string Tags;
|
||||
public bool IsSubscribed;
|
||||
public bool IsInstalled;
|
||||
public string InstallFolder;
|
||||
public ulong FileSize;
|
||||
public uint VotesUp;
|
||||
public uint VotesDown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创意工坊 Mod 浏览器,基于 Steamworks ISteamUGC 接口
|
||||
/// 支持分页查询、订阅、取消订阅操作
|
||||
/// </summary>
|
||||
public class WorkshopModBrowser
|
||||
{
|
||||
public static readonly WorkshopModBrowser Instance = new WorkshopModBrowser();
|
||||
|
||||
public const uint PageSize = 50;
|
||||
|
||||
// ── 查询状态 ──
|
||||
public bool IsQuerying { get; private set; }
|
||||
public uint CurrentPage { get; private set; } = 1;
|
||||
public uint TotalResults { get; private set; }
|
||||
public uint TotalPages => TotalResults == 0 ? 0 : (TotalResults + PageSize - 1) / PageSize;
|
||||
public List<WorkshopModItem> Results { get; } = new List<WorkshopModItem>();
|
||||
public string LastError { get; private set; } = "";
|
||||
|
||||
// ── 订阅/取消订阅状态 ──
|
||||
public bool IsSubscribeOperating { get; private set; }
|
||||
|
||||
private CallResult<SteamUGCQueryCompleted_t> _queryCallResult;
|
||||
private CallResult<RemoteStorageSubscribePublishedFileResult_t> _subscribeCallResult;
|
||||
private CallResult<RemoteStorageUnsubscribePublishedFileResult_t> _unsubscribeCallResult;
|
||||
|
||||
private UGCQueryHandle_t _pendingQueryHandle;
|
||||
|
||||
public event System.Action OnQueryCompleted;
|
||||
public event Action<PublishedFileId_t, bool> OnSubscribeCompleted;
|
||||
public event Action<PublishedFileId_t, bool> OnUnsubscribeCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 查询当前 AppId 的所有 Workshop Mod(分页)
|
||||
/// </summary>
|
||||
public void QueryPage(uint page = 1)
|
||||
{
|
||||
if (IsQuerying) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!SteamAPI.IsSteamRunning())
|
||||
{
|
||||
LastError = "Steam 未运行";
|
||||
return;
|
||||
}
|
||||
|
||||
IsQuerying = true;
|
||||
CurrentPage = page;
|
||||
LastError = "";
|
||||
|
||||
var appId = SteamUtils.GetAppID();
|
||||
var handle = SteamUGC.CreateQueryAllUGCRequest(
|
||||
EUGCQuery.k_EUGCQuery_RankedByPublicationDate,
|
||||
EUGCMatchingUGCType.k_EUGCMatchingUGCType_Items_ReadyToUse,
|
||||
new AppId_t(0), // creatorAppId: any
|
||||
appId,
|
||||
page);
|
||||
|
||||
SteamUGC.SetReturnLongDescription(handle, true);
|
||||
SteamUGC.SetReturnTotalOnly(handle, false);
|
||||
|
||||
_pendingQueryHandle = handle;
|
||||
var apiCall = SteamUGC.SendQueryUGCRequest(handle);
|
||||
|
||||
_queryCallResult = CallResult<SteamUGCQueryCompleted_t>.Create(OnSteamQueryCompleted);
|
||||
_queryCallResult.Set(apiCall);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
IsQuerying = false;
|
||||
LastError = $"查询异常: {e.Message}";
|
||||
LogSystem.LogError($"WorkshopModBrowser: 查询失败 - {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订阅指定 Workshop 物品
|
||||
/// </summary>
|
||||
public void Subscribe(PublishedFileId_t fileId)
|
||||
{
|
||||
if (IsSubscribeOperating) return;
|
||||
try
|
||||
{
|
||||
IsSubscribeOperating = true;
|
||||
var apiCall = SteamUGC.SubscribeItem(fileId);
|
||||
_subscribeCallResult = CallResult<RemoteStorageSubscribePublishedFileResult_t>.Create(
|
||||
(result, ioFail) =>
|
||||
{
|
||||
IsSubscribeOperating = false;
|
||||
bool success = !ioFail && result.m_eResult == EResult.k_EResultOK;
|
||||
if (success)
|
||||
{
|
||||
// 更新本地结果状态
|
||||
RefreshItemState(fileId);
|
||||
LogSystem.LogInfo($"WorkshopModBrowser: 已订阅 {fileId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogSystem.LogError($"WorkshopModBrowser: 订阅失败 {fileId}, Result={result.m_eResult}");
|
||||
}
|
||||
OnSubscribeCompleted?.Invoke(fileId, success);
|
||||
});
|
||||
_subscribeCallResult.Set(apiCall);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
IsSubscribeOperating = false;
|
||||
LogSystem.LogError($"WorkshopModBrowser: 订阅异常 - {e.Message}");
|
||||
OnSubscribeCompleted?.Invoke(fileId, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消订阅指定 Workshop 物品
|
||||
/// </summary>
|
||||
public void Unsubscribe(PublishedFileId_t fileId)
|
||||
{
|
||||
if (IsSubscribeOperating) return;
|
||||
try
|
||||
{
|
||||
IsSubscribeOperating = true;
|
||||
var apiCall = SteamUGC.UnsubscribeItem(fileId);
|
||||
_unsubscribeCallResult = CallResult<RemoteStorageUnsubscribePublishedFileResult_t>.Create(
|
||||
(result, ioFail) =>
|
||||
{
|
||||
IsSubscribeOperating = false;
|
||||
bool success = !ioFail && result.m_eResult == EResult.k_EResultOK;
|
||||
if (success)
|
||||
{
|
||||
RefreshItemState(fileId);
|
||||
LogSystem.LogInfo($"WorkshopModBrowser: 已取消订阅 {fileId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogSystem.LogError($"WorkshopModBrowser: 取消订阅失败 {fileId}, Result={result.m_eResult}");
|
||||
}
|
||||
OnUnsubscribeCompleted?.Invoke(fileId, success);
|
||||
});
|
||||
_unsubscribeCallResult.Set(apiCall);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
IsSubscribeOperating = false;
|
||||
LogSystem.LogError($"WorkshopModBrowser: 取消订阅异常 - {e.Message}");
|
||||
OnUnsubscribeCompleted?.Invoke(fileId, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新 Steam 回调(需外部在合适的时机定期调用,如 EditorApplication.update 或游戏的 Update)
|
||||
/// </summary>
|
||||
public void RunCallbacks()
|
||||
{
|
||||
try { SteamAPI.RunCallbacks(); } catch { }
|
||||
}
|
||||
|
||||
private void OnSteamQueryCompleted(SteamUGCQueryCompleted_t result, bool ioFailure)
|
||||
{
|
||||
IsQuerying = false;
|
||||
|
||||
if (ioFailure || result.m_eResult != EResult.k_EResultOK)
|
||||
{
|
||||
LastError = $"查询失败: Result={result.m_eResult}";
|
||||
LogSystem.LogError($"WorkshopModBrowser: {LastError}");
|
||||
SteamUGC.ReleaseQueryUGCRequest(result.m_handle);
|
||||
OnQueryCompleted?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
Results.Clear();
|
||||
TotalResults = result.m_unTotalMatchingResults;
|
||||
|
||||
for (uint i = 0; i < result.m_unNumResultsReturned; i++)
|
||||
{
|
||||
if (!SteamUGC.GetQueryUGCResult(result.m_handle, i, out var details)) continue;
|
||||
|
||||
var item = new WorkshopModItem
|
||||
{
|
||||
FileId = details.m_nPublishedFileId,
|
||||
Title = details.m_rgchTitle,
|
||||
Description = details.m_rgchDescription,
|
||||
Tags = details.m_rgchTags,
|
||||
VotesUp = details.m_unVotesUp,
|
||||
VotesDown = details.m_unVotesDown,
|
||||
FileSize = (ulong)details.m_nFileSize,
|
||||
};
|
||||
|
||||
RefreshItemStateForItem(item);
|
||||
Results.Add(item);
|
||||
}
|
||||
|
||||
SteamUGC.ReleaseQueryUGCRequest(result.m_handle);
|
||||
OnQueryCompleted?.Invoke();
|
||||
}
|
||||
|
||||
private void RefreshItemState(PublishedFileId_t fileId)
|
||||
{
|
||||
var item = Results.Find(r => r.FileId == fileId);
|
||||
if (item != null) RefreshItemStateForItem(item);
|
||||
}
|
||||
|
||||
private void RefreshItemStateForItem(WorkshopModItem item)
|
||||
{
|
||||
var state = (EItemState)SteamUGC.GetItemState(item.FileId);
|
||||
item.IsSubscribed = (state & EItemState.k_EItemStateSubscribed) != 0;
|
||||
item.IsInstalled = (state & EItemState.k_EItemStateInstalled) != 0;
|
||||
|
||||
if (item.IsInstalled &&
|
||||
SteamUGC.GetItemInstallInfo(item.FileId, out ulong size, out string folder, 1024, out _))
|
||||
{
|
||||
item.InstallFolder = folder;
|
||||
item.FileSize = size;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.InstallFolder = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46d2fb86290a5344c9569512a293e1f1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -30,42 +30,49 @@ namespace Logic.Multilingual
|
||||
public struct TranslationEntry
|
||||
{
|
||||
public uint Id;
|
||||
public string Reference; // ZH 中文原文参考
|
||||
public string Translation; // 目标语言翻译内容
|
||||
public string EnglishText; // EN 列(翻译标准参考)
|
||||
public string ReferenceText; // 参考语言列(玩家指定语言,供参考)
|
||||
public string Translation; // 翻译内容列(玩家填写,空则不替换)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CSV 读写工具(RFC 4180 兼容,支持引号内换行和转义引号)
|
||||
/// CSV 格式为 4 列:ID | EN | {referenceLanguage} | Translation
|
||||
/// </summary>
|
||||
public static class WorkshopModCsv
|
||||
{
|
||||
/// <summary>
|
||||
/// 将翻译条目列表写为 CSV 内容字符串
|
||||
/// 将翻译条目列表写为 CSV 内容字符串(4 列格式)
|
||||
/// </summary>
|
||||
public static string WriteCsv(MultilingualType targetType, List<TranslationEntry> entries)
|
||||
/// <param name="referenceLanguage">参考语言类型(第 3 列标题及内容)</param>
|
||||
/// <param name="entries">翻译条目列表</param>
|
||||
public static string WriteCsv(MultilingualType referenceLanguage, List<TranslationEntry> entries)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"ID,ZH,{targetType}");
|
||||
// 4 列英文标题:ID | EN | {referenceLanguage} | Translation
|
||||
sb.AppendLine($"ID,EN,{referenceLanguage},Translation");
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
sb.Append(entry.Id);
|
||||
sb.Append(',');
|
||||
sb.Append(EscapeField(entry.Reference));
|
||||
sb.Append(EscapeField(entry.EnglishText));
|
||||
sb.Append(',');
|
||||
sb.Append(EscapeField(entry.Translation));
|
||||
sb.Append(EscapeField(entry.ReferenceText));
|
||||
sb.Append(',');
|
||||
sb.Append(EscapeField(entry.Translation)); // 导出时留空,供玩家填写
|
||||
sb.AppendLine();
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 CSV 内容字符串,返回翻译条目列表
|
||||
/// 解析 CSV 内容字符串,返回有效翻译条目列表(仅返回 Translation 列非空的行)
|
||||
/// </summary>
|
||||
/// <param name="content">CSV 文本内容</param>
|
||||
/// <param name="headerLanguage">输出:表头第三列的语言标识</param>
|
||||
public static List<TranslationEntry> ReadCsv(string content, out string headerLanguage)
|
||||
/// <param name="referenceLanguage">输出:表头第 3 列的语言标识</param>
|
||||
public static List<TranslationEntry> ReadCsv(string content, out string referenceLanguage)
|
||||
{
|
||||
headerLanguage = "";
|
||||
referenceLanguage = "";
|
||||
var result = new List<TranslationEntry>();
|
||||
if (string.IsNullOrEmpty(content)) return result;
|
||||
|
||||
@ -76,26 +83,28 @@ namespace Logic.Multilingual
|
||||
var rows = ParseCsvRows(content);
|
||||
if (rows.Count < 2) return result;
|
||||
|
||||
// 解析表头:ID, ZH, {Language}
|
||||
// 解析表头:ID, EN, {referenceLanguage}, Translation
|
||||
var header = rows[0];
|
||||
if (header.Length < 3) return result;
|
||||
headerLanguage = header[2].Trim();
|
||||
if (header.Length < 4) return result;
|
||||
referenceLanguage = header[2].Trim();
|
||||
|
||||
// 解析数据行
|
||||
for (int i = 1; i < rows.Count; i++)
|
||||
{
|
||||
var row = rows[i];
|
||||
if (row.Length < 3) continue;
|
||||
if (row.Length < 4) continue;
|
||||
var idStr = row[0].Trim();
|
||||
if (!uint.TryParse(idStr, out uint id)) continue;
|
||||
|
||||
var translation = row[2];
|
||||
// 只有第 4 列(Translation)非空才生效
|
||||
var translation = row[3];
|
||||
if (string.IsNullOrEmpty(translation)) continue;
|
||||
|
||||
result.Add(new TranslationEntry
|
||||
{
|
||||
Id = id,
|
||||
Reference = row[1],
|
||||
EnglishText = row[1],
|
||||
ReferenceText = row[2],
|
||||
Translation = translation
|
||||
});
|
||||
}
|
||||
|
||||
@ -31,18 +31,18 @@ namespace Logic.Multilingual
|
||||
|
||||
/// <summary>
|
||||
/// 导出标准 Mod 翻译模板到本地磁盘
|
||||
/// 按当前玩家选择的语言导出:ID + 中文原文 + 玩家语言文本
|
||||
/// </summary>
|
||||
/// <param name="targetLanguage">Mod 目标语言(写入 mod_info,表示此 Mod 修改哪个语种字段)</param>
|
||||
/// <param name="referenceLanguage">参考语言(CSV 第 3 列,供玩家翻译时参考)</param>
|
||||
/// <param name="modName">Mod 文件夹名称,为空则自动生成</param>
|
||||
/// <returns>导出成功返回文件夹路径,失败返回 null</returns>
|
||||
public static string ExportModTemplate(string modName = null)
|
||||
public static string ExportModTemplate(MultilingualType targetLanguage, MultilingualType referenceLanguage, string modName = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentType = MultilingualManager.Instance.CurrentType;
|
||||
if (currentType == MultilingualType.None || currentType == MultilingualType.Max)
|
||||
if (targetLanguage == MultilingualType.None || targetLanguage == MultilingualType.Max)
|
||||
{
|
||||
LogSystem.LogError("WorkshopModExporter: 当前语言类型无效,无法导出模板");
|
||||
LogSystem.LogError("WorkshopModExporter: 目标语言类型无效,无法导出模板");
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -57,12 +57,12 @@ namespace Logic.Multilingual
|
||||
|
||||
// 生成默认 Mod 文件夹名
|
||||
if (string.IsNullOrEmpty(modName))
|
||||
modName = $"TranslationMod_{currentType}_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
modName = $"TranslationMod_{targetLanguage}_{DateTime.Now:yyyyMMdd_HHmmss}";
|
||||
|
||||
var modPath = Path.Combine(GetModRootPath(), modName);
|
||||
Directory.CreateDirectory(modPath);
|
||||
|
||||
// 构建翻译条目列表
|
||||
// 构建翻译条目列表(EN 为翻译标准列,referenceLanguage 为参考列,Translation 留空)
|
||||
var entries = new List<TranslationEntry>();
|
||||
foreach (var item in data.Items)
|
||||
{
|
||||
@ -71,17 +71,18 @@ namespace Logic.Multilingual
|
||||
entries.Add(new TranslationEntry
|
||||
{
|
||||
Id = item.ID,
|
||||
Reference = item.ZH ?? "",
|
||||
Translation = item.GetStrByType(currentType) ?? ""
|
||||
EnglishText = item.EN ?? "",
|
||||
ReferenceText = item.GetStrByType(referenceLanguage) ?? "",
|
||||
Translation = "" // 留空,由玩家填写
|
||||
});
|
||||
}
|
||||
|
||||
// 写入 mod_info.json
|
||||
var modInfo = new WorkshopModInfo
|
||||
{
|
||||
title = $"{currentType} Translation Mod",
|
||||
title = $"{targetLanguage} Translation Mod",
|
||||
author = GetSteamPlayerName(),
|
||||
targetLanguage = currentType.ToString(),
|
||||
targetLanguage = targetLanguage.ToString(),
|
||||
description = "",
|
||||
version = "1.0"
|
||||
};
|
||||
@ -93,7 +94,7 @@ namespace Logic.Multilingual
|
||||
);
|
||||
|
||||
// 写入 translation.csv(UTF-8 with BOM,Excel 友好)
|
||||
var csvContent = WorkshopModCsv.WriteCsv(currentType, entries);
|
||||
var csvContent = WorkshopModCsv.WriteCsv(referenceLanguage, entries);
|
||||
File.WriteAllText(
|
||||
Path.Combine(modPath, TranslationFileName),
|
||||
csvContent,
|
||||
@ -101,7 +102,7 @@ namespace Logic.Multilingual
|
||||
);
|
||||
|
||||
LogSystem.LogInfo(
|
||||
$"WorkshopModExporter: Mod 模板已导出到 {modPath},语言={currentType},共 {entries.Count} 条");
|
||||
$"WorkshopModExporter: Mod 模板已导出到 {modPath},目标语言={targetLanguage},参考语言={referenceLanguage},共 {entries.Count} 条");
|
||||
return modPath;
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -111,6 +112,22 @@ namespace Logic.Multilingual
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出标准 Mod 翻译模板(使用当前游戏语言作为参考语言的便捷重载)
|
||||
/// </summary>
|
||||
/// <param name="modName">Mod 文件夹名称,为空则自动生成</param>
|
||||
/// <returns>导出成功返回文件夹路径,失败返回 null</returns>
|
||||
public static string ExportModTemplate(string modName = null)
|
||||
{
|
||||
var currentType = MultilingualManager.Instance.CurrentType;
|
||||
if (currentType == MultilingualType.None || currentType == MultilingualType.Max)
|
||||
{
|
||||
LogSystem.LogError("WorkshopModExporter: 当前语言类型无效,无法导出模板");
|
||||
return null;
|
||||
}
|
||||
return ExportModTemplate(currentType, currentType, modName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Steam 玩家昵称,未初始化时返回默认值
|
||||
/// </summary>
|
||||
|
||||
@ -10,6 +10,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Logic.CrashSight;
|
||||
using Steamworks;
|
||||
using TH1_Logic.Config;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Logic.Multilingual
|
||||
@ -18,6 +19,7 @@ namespace Logic.Multilingual
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载并应用所有 Mod(先本地后订阅,后加载的覆盖先加载的)
|
||||
/// 目标语言由各 Mod 的 mod_info.targetLanguage 决定
|
||||
/// </summary>
|
||||
/// <returns>成功应用的翻译条目总数</returns>
|
||||
public static int ApplyAllMods(MultilingualData data)
|
||||
@ -28,39 +30,69 @@ namespace Logic.Multilingual
|
||||
|
||||
int count = 0;
|
||||
// 先应用本地 Mod(优先级较低)
|
||||
count += ApplyModsFromPaths(data, GetLocalModPaths());
|
||||
count += ApplyModsFromPaths(data, GetLocalModPaths(), MultilingualType.None);
|
||||
// 再应用订阅的 Workshop Mod(后加载覆盖前加载)
|
||||
count += ApplyModsFromPaths(data, GetSubscribedModPaths());
|
||||
count += ApplyModsFromPaths(data, GetSubscribedModPaths(), MultilingualType.None);
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅应用已订阅的 Workshop Mod
|
||||
/// 按玩家配置的语种-Mod 优先级设置应用所有 Mod
|
||||
/// 每个语种对应一个有序 Mod 列表,低索引=低优先级(先应用),高索引=高优先级(后应用可覆盖)
|
||||
/// </summary>
|
||||
/// <param name="data">多语言数据</param>
|
||||
/// <param name="configs">语种-Mod 配置列表</param>
|
||||
/// <returns>成功应用的翻译条目总数</returns>
|
||||
public static int ApplyModsWithConfig(MultilingualData data, List<ModLanguageConfig> configs)
|
||||
{
|
||||
if (data == null || configs == null) return 0;
|
||||
data.RefreshDict();
|
||||
if (data.ItemDict == null) return 0;
|
||||
|
||||
int total = 0;
|
||||
foreach (var config in configs)
|
||||
{
|
||||
if (config == null || config.ModPaths == null) continue;
|
||||
// 按索引顺序应用(低索引=低优先级先应用,高索引=高优先级后应用)
|
||||
total += ApplyModsFromPaths(data, config.ModPaths, config.Language);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅应用已订阅的 Workshop Mod(目标语言由 mod_info 决定)
|
||||
/// </summary>
|
||||
public static int ApplySubscribedMods(MultilingualData data)
|
||||
{
|
||||
if (data == null) return 0;
|
||||
data.RefreshDict();
|
||||
if (data.ItemDict == null) return 0;
|
||||
return ApplyModsFromPaths(data, GetSubscribedModPaths());
|
||||
return ApplyModsFromPaths(data, GetSubscribedModPaths(), MultilingualType.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅应用本地 Mod
|
||||
/// 仅应用本地 Mod(目标语言由 mod_info 决定)
|
||||
/// </summary>
|
||||
public static int ApplyLocalMods(MultilingualData data)
|
||||
{
|
||||
if (data == null) return 0;
|
||||
data.RefreshDict();
|
||||
if (data.ItemDict == null) return 0;
|
||||
return ApplyModsFromPaths(data, GetLocalModPaths());
|
||||
return ApplyModsFromPaths(data, GetLocalModPaths(), MultilingualType.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用指定文件夹中的单个 Mod
|
||||
/// </summary>
|
||||
/// <param name="data">多语言数据</param>
|
||||
/// <param name="folderPath">Mod 文件夹路径</param>
|
||||
/// <param name="targetLanguageOverride">
|
||||
/// 强制覆盖目标语言(None = 使用 mod_info.targetLanguage)
|
||||
/// 玩家可通过优先级配置将任意 Mod 应用到任意语种
|
||||
/// </param>
|
||||
/// <returns>成功覆盖的翻译条目数量</returns>
|
||||
public static int ApplyModFromFolder(MultilingualData data, string folderPath)
|
||||
public static int ApplyModFromFolder(MultilingualData data, string folderPath,
|
||||
MultilingualType targetLanguageOverride = MultilingualType.None)
|
||||
{
|
||||
if (data == null || string.IsNullOrEmpty(folderPath)) return 0;
|
||||
data.RefreshDict();
|
||||
@ -86,13 +118,21 @@ namespace Logic.Multilingual
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 2. 校验目标语言
|
||||
var targetType = ParseLanguageType(modInfo.targetLanguage);
|
||||
if (targetType == MultilingualType.None || targetType == MultilingualType.Max)
|
||||
// 2. 确定目标语言(优先使用外部覆盖,其次使用 mod_info 声明)
|
||||
MultilingualType targetType;
|
||||
if (targetLanguageOverride != MultilingualType.None && targetLanguageOverride != MultilingualType.Max)
|
||||
{
|
||||
LogSystem.LogError(
|
||||
$"WorkshopModLoader: 无效的目标语言 \"{modInfo.targetLanguage}\" - {folderPath}");
|
||||
return 0;
|
||||
targetType = targetLanguageOverride;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetType = ParseLanguageType(modInfo.targetLanguage);
|
||||
if (targetType == MultilingualType.None || targetType == MultilingualType.Max)
|
||||
{
|
||||
LogSystem.LogError(
|
||||
$"WorkshopModLoader: 无效的目标语言 \"{modInfo.targetLanguage}\" - {folderPath}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 读取 translation.csv
|
||||
@ -114,17 +154,9 @@ namespace Logic.Multilingual
|
||||
return 0;
|
||||
}
|
||||
|
||||
var entries = WorkshopModCsv.ReadCsv(csvContent, out string headerLanguage);
|
||||
var entries = WorkshopModCsv.ReadCsv(csvContent, out string referenceLanguage);
|
||||
|
||||
// 校验 CSV 表头语言与 mod_info 一致性
|
||||
if (!string.IsNullOrEmpty(headerLanguage) &&
|
||||
!string.Equals(headerLanguage, modInfo.targetLanguage, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
LogSystem.LogWarning(
|
||||
$"WorkshopModLoader: CSV 表头语言({headerLanguage})与 mod_info 目标语言({modInfo.targetLanguage})不一致,以 mod_info 为准");
|
||||
}
|
||||
|
||||
// 4. 逐条覆盖多语言数据
|
||||
// 4. 逐条覆盖多语言数据(仅 Translation 列非空的行生效)
|
||||
int appliedCount = 0;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
@ -136,7 +168,7 @@ namespace Logic.Multilingual
|
||||
if (appliedCount > 0)
|
||||
{
|
||||
LogSystem.LogInfo(
|
||||
$"WorkshopModLoader: 已应用 Mod \"{modInfo.title}\" (语言={targetType}),覆盖 {appliedCount}/{entries.Count} 条翻译");
|
||||
$"WorkshopModLoader: 已应用 Mod \"{modInfo.title}\" (目标语言={targetType},参考语言={referenceLanguage}),覆盖 {appliedCount}/{entries.Count} 条翻译");
|
||||
}
|
||||
|
||||
return appliedCount;
|
||||
@ -197,12 +229,22 @@ namespace Logic.Multilingual
|
||||
return paths;
|
||||
}
|
||||
|
||||
private static int ApplyModsFromPaths(MultilingualData data, List<string> paths)
|
||||
/// <summary>
|
||||
/// 获取所有可用的 Mod 路径(本地 + 订阅)
|
||||
/// </summary>
|
||||
public static List<string> GetAllAvailableModPaths()
|
||||
{
|
||||
var paths = new List<string>(GetLocalModPaths());
|
||||
paths.AddRange(GetSubscribedModPaths());
|
||||
return paths;
|
||||
}
|
||||
|
||||
private static int ApplyModsFromPaths(MultilingualData data, List<string> paths, MultilingualType targetOverride)
|
||||
{
|
||||
int totalCount = 0;
|
||||
foreach (var path in paths)
|
||||
{
|
||||
totalCount += ApplyModFromFolder(data, path);
|
||||
totalCount += ApplyModFromFolder(data, path, targetOverride);
|
||||
}
|
||||
return totalCount;
|
||||
}
|
||||
@ -222,15 +264,16 @@ namespace Logic.Multilingual
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MultilingualType.ZH: item.ZH = value; break;
|
||||
case MultilingualType.TDZH: item.TDZH = value; break;
|
||||
case MultilingualType.EN: item.EN = value; break;
|
||||
case MultilingualType.JP: item.JP = value; break;
|
||||
case MultilingualType.KR: item.KR = value; break;
|
||||
case MultilingualType.RU: item.RU = value; break;
|
||||
case MultilingualType.ES: item.ES = value; break;
|
||||
case MultilingualType.PT: item.PT = value; break;
|
||||
case MultilingualType.FR: item.FR = value; break;
|
||||
case MultilingualType.ZH: item.ZH = value; break;
|
||||
case MultilingualType.TDZH: item.TDZH = value; break;
|
||||
case MultilingualType.EN: item.EN = value; break;
|
||||
case MultilingualType.JP: item.JP = value; break;
|
||||
case MultilingualType.KR: item.KR = value; break;
|
||||
case MultilingualType.RU: item.RU = value; break;
|
||||
case MultilingualType.ES: item.ES = value; break;
|
||||
case MultilingualType.PT: item.PT = value; break;
|
||||
case MultilingualType.FR: item.FR = value; break;
|
||||
case MultilingualType.Custom: item.Custom = value; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user