TH1/Unity/Assets/Editor/WorkshopModEditorWindow.cs
2026-06-10 11:58:18 +08:00

972 lines
42 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* @Author: 白哉
* @Description: 创意工坊多语言 Mod 编辑器测试窗口(全功能测试面板)
* @Date: 2026年04月14日 星期二 15:43:02
* @Modify: 2026年04月16日 - 4列CSV/Custom语种/Mod优先级配置/Workshop浏览器
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Logic.Multilingual;
using Steamworks;
using TH1_Logic.Config;
using UnityEditor;
using UnityEngine;
public class WorkshopModEditorWindow : EditorWindow
{
// ── 选项卡 ──
private enum Tab { Export, Upload, ModConfig, Browse, CsvTest, Inspect }
private Tab _currentTab = Tab.Export;
private readonly string[] _tabNames =
{ "① 导出模板", "② 上传Workshop", "③ Mod配置", "④ 工坊浏览", "⑤ CSV测试", "⑥ 数据检视" };
// ── 导出面板状态 ──
private MultilingualType _exportTargetLanguage = MultilingualType.EN; // 目标语言mod_info 写入,决定替换哪个语种)
private MultilingualType _exportRefLanguage = MultilingualType.ZH; // 参考语言CSV 第3列供玩家参考
private string _exportModName = "";
private string _lastExportPath = "";
// ── 上传面板状态 ──
private string _uploadFolder = "";
private string _uploadTitle = "";
private string _uploadDescription = "";
private string _uploadPreviewPath = "";
private string _uploadStatus = "";
// ── Mod 配置面板状态 ──
private MultilingualType _configSelectedLang = MultilingualType.EN;
private List<string> _allAvailableMods = new List<string>(); // 所有可用 Mod 路径
private Vector2 _configModListScroll;
private Vector2 _configAvailScroll;
private string _configStatus = "";
private GameConfig _editorConfig; // 直接操作游戏真实序列化配置
private string _configFilePath; // game_cfg.json 文件路径
// ── Workshop 浏览器状态 ──
private Vector2 _browseScroll;
private string _browseStatus = "";
private bool _browseSteamOk;
// ── CSV 测试状态 ──
private MultilingualType _csvTestRefLang = MultilingualType.ZH;
private string _csvTestInput = "";
private string _csvTestOutput = "";
private Vector2 _csvInputScroll;
private Vector2 _csvOutputScroll;
// ── 数据检视状态 ──
private string _inspectFolder = "";
private string _inspectResult = "";
private Vector2 _inspectScroll;
private uint _inspectSearchId;
private string _inspectSearchResult = "";
// ── 通用 ──
private Vector2 _mainScroll;
[MenuItem("TH1工具/创意工坊多语言 Mod 测试面板")]
public static void ShowWindow()
{
var window = GetWindow<WorkshopModEditorWindow>("Workshop Mod 测试");
window.minSize = new Vector2(680, 520);
}
private void OnEnable()
{
LoadGameConfig();
// 注册 Workshop 浏览器事件
WorkshopModBrowser.Instance.OnQueryCompleted += OnBrowseQueryCompleted;
WorkshopModBrowser.Instance.OnSubscribeCompleted += OnBrowseSubscribeCompleted;
WorkshopModBrowser.Instance.OnUnsubscribeCompleted+= OnBrowseUnsubscribeCompleted;
// 注册 Editor 心跳,用于驱动 SteamAPI 回调
EditorApplication.update += EditorUpdate;
}
private void OnDisable()
{
WorkshopModBrowser.Instance.OnQueryCompleted -= OnBrowseQueryCompleted;
WorkshopModBrowser.Instance.OnSubscribeCompleted -= OnBrowseSubscribeCompleted;
WorkshopModBrowser.Instance.OnUnsubscribeCompleted-= OnBrowseUnsubscribeCompleted;
EditorApplication.update -= EditorUpdate;
}
private void EditorUpdate()
{
// 轮询上传器的异步 API 调用
if (WorkshopModUploader.Instance.IsBusy)
{
WorkshopModUploader.Instance.Poll();
Repaint();
}
// 浏览器相关回调(浏览器的 Subscribe/Unsubscribe 仍使用 CallResult需要 RunCallbacks 驱动)
bool browserBusy = WorkshopModBrowser.Instance.IsQuerying
|| WorkshopModBrowser.Instance.IsSubscribeOperating;
if (browserBusy || _currentTab == Tab.Browse)
{
try
{
SteamAPI.RunCallbacks();
}
catch (Exception e)
{
Debug.LogWarning($"SteamAPI.RunCallbacks 异常: {e.Message}");
}
if (browserBusy) Repaint();
}
}
private void OnGUI()
{
_currentTab = (Tab)GUILayout.Toolbar((int)_currentTab, _tabNames, GUILayout.Height(28));
EditorGUILayout.Space(4);
_mainScroll = EditorGUILayout.BeginScrollView(_mainScroll);
switch (_currentTab)
{
case Tab.Export: DrawExportTab(); break;
case Tab.Upload: DrawUploadTab(); break;
case Tab.ModConfig:DrawModConfigTab(); break;
case Tab.Browse: DrawBrowseTab(); break;
case Tab.CsvTest: DrawCsvTestTab(); break;
case Tab.Inspect: DrawInspectTab(); break;
}
EditorGUILayout.EndScrollView();
}
// ═══════════════════════════════════════════════════════════
// ① 导出模板
// ═══════════════════════════════════════════════════════════
private void DrawExportTab()
{
EditorGUILayout.LabelField("导出 Mod 翻译模板", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"导出标准 Mod 文件夹mod_info.json + translation.csv4列格式\n" +
"CSV 列ID | EN英文标准 | 参考语言文本 | Translation玩家填写留空不替换\n" +
"目标语言:决定此 Mod 修改哪个语种字段(写入 mod_info\n" +
"参考语言CSV 第3列内容辅助玩家翻译时参考",
MessageType.Info);
EditorGUILayout.Space(4);
_exportTargetLanguage = (MultilingualType)EditorGUILayout.EnumPopup("目标语言mod_info", _exportTargetLanguage);
_exportRefLanguage = (MultilingualType)EditorGUILayout.EnumPopup("参考语言CSV第3列", _exportRefLanguage);
_exportModName = EditorGUILayout.TextField("Mod 名称(留空自动生成)", _exportModName);
EditorGUILayout.Space(4);
var rootPath = WorkshopModExporter.GetModRootPath();
EditorGUILayout.LabelField("导出根目录", rootPath, EditorStyles.wordWrappedLabel);
EditorGUILayout.Space(8);
bool exportInvalid = _exportTargetLanguage == MultilingualType.None ||
_exportTargetLanguage == MultilingualType.Max ||
_exportRefLanguage == MultilingualType.None ||
_exportRefLanguage == MultilingualType.Max;
using (new EditorGUI.DisabledScope(exportInvalid))
{
if (GUILayout.Button("导出 Mod 模板", GUILayout.Height(32)))
DoExport();
}
if (!string.IsNullOrEmpty(_lastExportPath))
{
EditorGUILayout.Space(4);
EditorGUILayout.HelpBox($"✅ 导出成功:{_lastExportPath}", MessageType.None);
if (GUILayout.Button("在文件浏览器中打开"))
EditorUtility.RevealInFinder(_lastExportPath);
}
}
private void DoExport()
{
var modName = string.IsNullOrEmpty(_exportModName) ? null : _exportModName;
var result = WorkshopModExporter.ExportModTemplate(_exportTargetLanguage, _exportRefLanguage, modName);
if (string.IsNullOrEmpty(result))
{
EditorUtility.DisplayDialog("导出失败", "导出失败,请查看 Console 日志", "确定");
return;
}
_lastExportPath = result;
Debug.Log($"✅ Mod 模板导出完成:{result},目标={_exportTargetLanguage},参考={_exportRefLanguage}");
}
// ═══════════════════════════════════════════════════════════
// ② 上传到 Workshop
// ═══════════════════════════════════════════════════════════
private void DrawUploadTab()
{
EditorGUILayout.LabelField("上传 Mod 到 Steam 创意工坊", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"需要 Steam 客户端正在运行且已登录。\n" +
"选择已导出/编辑好的 Mod 文件夹,填写信息后上传。",
MessageType.Info);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
_uploadFolder = EditorGUILayout.TextField("Mod 文件夹", _uploadFolder);
if (GUILayout.Button("浏览...", GUILayout.Width(60)))
{
var selected = EditorUtility.OpenFolderPanel("选择 Mod 文件夹",
WorkshopModExporter.GetModRootPath(), "");
if (!string.IsNullOrEmpty(selected)) _uploadFolder = selected;
}
EditorGUILayout.EndHorizontal();
bool folderValid = !string.IsNullOrEmpty(_uploadFolder) && Directory.Exists(_uploadFolder);
bool hasModInfo = folderValid && File.Exists(Path.Combine(_uploadFolder, WorkshopModExporter.ModInfoFileName));
bool hasCsv = folderValid && File.Exists(Path.Combine(_uploadFolder, WorkshopModExporter.TranslationFileName));
if (folderValid)
{
EditorGUILayout.LabelField(" mod_info.json", hasModInfo ? "✅ 存在" : "❌ 缺失");
EditorGUILayout.LabelField(" translation.csv", hasCsv ? "✅ 存在" : "❌ 缺失");
}
EditorGUILayout.Space(4);
_uploadTitle = EditorGUILayout.TextField("标题", _uploadTitle);
_uploadDescription = EditorGUILayout.TextField("描述", _uploadDescription);
EditorGUILayout.BeginHorizontal();
_uploadPreviewPath = EditorGUILayout.TextField("预览图路径", _uploadPreviewPath);
if (GUILayout.Button("浏览...", GUILayout.Width(60)))
{
var selected = EditorUtility.OpenFilePanel("选择预览图", _uploadFolder ?? "", "png,jpg,jpeg");
if (!string.IsNullOrEmpty(selected)) _uploadPreviewPath = selected;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
bool isBusy = WorkshopModUploader.Instance.IsBusy;
using (new EditorGUI.DisabledScope(!hasModInfo || !hasCsv || isBusy))
{
if (GUILayout.Button(isBusy ? "上传中..." : "创建并上传到 Workshop", GUILayout.Height(32)))
DoUpload();
}
// 实时显示上传器状态
if (isBusy)
{
EditorGUILayout.Space(4);
EditorGUILayout.HelpBox(WorkshopModUploader.Instance.StatusMessage, MessageType.None);
}
if (!string.IsNullOrEmpty(_uploadStatus))
{
EditorGUILayout.Space(4);
EditorGUILayout.HelpBox(_uploadStatus, MessageType.None);
}
}
private void DoUpload()
{
_uploadStatus = "";
WorkshopModUploader.Instance.CreateAndUploadMod(
_uploadFolder, _uploadTitle, _uploadDescription, _uploadPreviewPath,
(success, fileId) =>
{
_uploadStatus = success
? $"✅ 上传成功PublishedFileId = {fileId}"
: $"❌ 上传失败: {WorkshopModUploader.Instance.StatusMessage}";
Repaint();
});
}
// ═══════════════════════════════════════════════════════════
// ③ Mod 配置(读写游戏真实 GameConfig
// ═══════════════════════════════════════════════════════════
private string GetGameConfigPath()
{
return Application.persistentDataPath + "/../Config/game_cfg.json";
}
private void LoadGameConfig()
{
_configFilePath = GetGameConfigPath();
if (File.Exists(_configFilePath))
{
try
{
string json = File.ReadAllText(_configFilePath);
_editorConfig = JsonUtility.FromJson<GameConfig>(json);
if (_editorConfig != null)
{
_editorConfig.MarkSaved();
_configStatus = $"✅ 已从 {_configFilePath} 加载配置";
return;
}
}
catch (Exception e)
{
Debug.LogError($"[WorkshopModEditor] 读取 GameConfig 失败: {e.Message}");
}
}
_editorConfig = new GameConfig();
_configStatus = File.Exists(_configFilePath)
? "⚠ 读取配置文件失败,已使用默认配置"
: " 配置文件不存在,已使用默认配置";
}
private void SaveGameConfig()
{
try
{
string dir = Path.GetDirectoryName(_configFilePath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
string json = JsonUtility.ToJson(_editorConfig, true);
File.WriteAllText(_configFilePath, json, System.Text.Encoding.UTF8);
_editorConfig.MarkSaved();
_configStatus = $"✅ 配置已保存到 {_configFilePath}";
Debug.Log($"[WorkshopModEditor] GameConfig 已保存: {_configFilePath}");
}
catch (Exception e)
{
_configStatus = $"❌ 保存失败: {e.Message}";
Debug.LogError($"[WorkshopModEditor] 保存 GameConfig 失败: {e.Message}");
}
}
private void DrawModConfigTab()
{
EditorGUILayout.LabelField("Mod 配置(游戏真实 GameConfig", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"直接读写游戏的 game_cfg.json 配置文件。\n" +
"为每个语种指定使用哪些 Mod 并设置优先级顺序,列表越靠下优先级越高。\n" +
"修改后点击「保存配置」写入文件,点击「应用 Mod」立即生效到多语言数据。",
MessageType.Info);
// 配置文件路径 & 操作按钮
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("配置文件", _configFilePath, EditorStyles.wordWrappedLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("重新加载配置", GUILayout.Width(100)))
LoadGameConfig();
var changedColor = _editorConfig.IsChanged ? Color.yellow : GUI.color;
var origColor = GUI.color;
GUI.color = changedColor;
if (GUILayout.Button(_editorConfig.IsChanged ? "● 保存配置 *" : "保存配置", GUILayout.Width(100)))
SaveGameConfig();
GUI.color = origColor;
if (GUILayout.Button("应用 Mod 到多语言数据", GUILayout.Width(160)))
DoApplyByConfig();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
// 语种选择
_configSelectedLang = (MultilingualType)EditorGUILayout.EnumPopup("当前配置语种", _configSelectedLang);
EditorGUILayout.Space(4);
// 扫描所有可用 Mod
if (GUILayout.Button("🔍 扫描所有可用 Mod本地 + 订阅)"))
{
_allAvailableMods = WorkshopModLoader.GetAllAvailableModPaths();
_configStatus = $"扫描完成,共 {_allAvailableMods.Count} 个 Mod";
}
EditorGUILayout.Space(4);
var config = _editorConfig.GetOrCreateModConfig(_configSelectedLang);
var modPaths = config.ModPaths;
// ─ 左:已配置的 Mod 优先级列表 ─
EditorGUILayout.LabelField($"「{_configSelectedLang}」 已启用的 Mod低→高优先级", EditorStyles.boldLabel);
_configModListScroll = EditorGUILayout.BeginScrollView(_configModListScroll, GUILayout.MaxHeight(200));
for (int i = 0; i < modPaths.Count; i++)
{
var path = modPaths[i];
string infoStr = GetModInfoStr(path);
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField($"[{i}] {Path.GetFileName(path)}{infoStr}", EditorStyles.wordWrappedLabel);
using (new EditorGUI.DisabledScope(i == 0))
{
if (GUILayout.Button("▲", GUILayout.Width(24)))
{
_editorConfig.MoveModPriority(_configSelectedLang, i, i - 1);
_configStatus = "已调整优先级";
}
}
using (new EditorGUI.DisabledScope(i == modPaths.Count - 1))
{
if (GUILayout.Button("▼", GUILayout.Width(24)))
{
_editorConfig.MoveModPriority(_configSelectedLang, i, i + 1);
_configStatus = "已调整优先级";
}
}
if (GUILayout.Button("移除", GUILayout.Width(48)))
{
_editorConfig.RemoveModFromLanguage(_configSelectedLang, path);
_configStatus = $"已从「{_configSelectedLang}」移除 {Path.GetFileName(path)}";
break;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
// ─ 右:可用 Mod 列表(点击添加) ─
if (_allAvailableMods.Count > 0)
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("可用 Mod点击添加到当前语种", EditorStyles.boldLabel);
_configAvailScroll = EditorGUILayout.BeginScrollView(_configAvailScroll, GUILayout.MaxHeight(160));
foreach (var path in _allAvailableMods)
{
bool alreadyAdded = modPaths.Contains(path);
string infoStr = GetModInfoStr(path);
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField(
$"{(alreadyAdded ? "" : " ")} {Path.GetFileName(path)}{infoStr}",
EditorStyles.wordWrappedLabel);
using (new EditorGUI.DisabledScope(alreadyAdded))
{
if (GUILayout.Button("添加", GUILayout.Width(48)))
{
_editorConfig.AddModToLanguage(_configSelectedLang, path);
_configStatus = $"已将 {Path.GetFileName(path)} 添加到「{_configSelectedLang}」";
}
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button($"清空「{_configSelectedLang}」的所有 Mod 配置"))
{
_editorConfig.ClearModsForLanguage(_configSelectedLang);
_configStatus = $"已清空「{_configSelectedLang}」配置";
}
EditorGUILayout.EndHorizontal();
if (!string.IsNullOrEmpty(_configStatus))
{
EditorGUILayout.Space(4);
EditorGUILayout.HelpBox(_configStatus, MessageType.None);
}
// 显示所有语种的配置摘要
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("所有语种配置摘要", EditorStyles.boldLabel);
foreach (var c in _editorConfig.ModLanguageConfigs)
{
if (c.ModPaths.Count == 0) continue;
EditorGUILayout.LabelField($" {c.Language}: {c.ModPaths.Count} 个 Mod", EditorStyles.wordWrappedLabel);
}
}
private string GetModInfoStr(string path)
{
var info = WorkshopModExporter.ReadModInfo(path);
if (info == null) return "";
return $" [{info.targetLanguage}] {info.title}";
}
private MultilingualData LoadMultilingualData(out string error)
{
error = "";
var data = TH1Resource.ResourceLoader.Load<MultilingualData>("Export/Multilingual");
if (data == null) error = "❌ 无法加载 Export/Multilingual";
return data;
}
private void DoApplyByConfig()
{
var data = LoadMultilingualData(out string err);
if (data == null) { _configStatus = err; return; }
int count = WorkshopModLoader.ApplyModsWithConfig(data, _editorConfig.ModLanguageConfigs);
_configStatus = $"✅ 按优先级配置应用完成,共覆盖 {count} 条翻译";
}
// ═══════════════════════════════════════════════════════════
// ④ Workshop 浏览器
// ═══════════════════════════════════════════════════════════
private void DrawBrowseTab()
{
EditorGUILayout.LabelField("Steam 创意工坊浏览器", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"查询当前 AppId 下的所有 Workshop Mod可在编辑器中直接订阅/取消订阅。\n" +
"需要 Steam 客户端正在运行且已登录。已订阅且已安装的 Mod 可在③中配置并应用。",
MessageType.Info);
EditorGUILayout.Space(4);
// 检测 Steam 状态
bool steamOk = false;
try { steamOk = SteamAPI.IsSteamRunning(); } catch { }
_browseSteamOk = steamOk;
EditorGUILayout.LabelField("Steam 状态", steamOk ? "✅ 运行中" : "❌ 未运行");
if (!steamOk)
{
EditorGUILayout.HelpBox("Steam 未运行,无法查询创意工坊。", MessageType.Warning);
return;
}
EditorGUILayout.Space(4);
// 翻页控件
var browser = WorkshopModBrowser.Instance;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(
browser.TotalResults > 0
? $"共 {browser.TotalResults} 个结果 | 第 {browser.CurrentPage}/{browser.TotalPages} 页"
: "尚未查询",
GUILayout.ExpandWidth(true));
using (new EditorGUI.DisabledScope(browser.IsQuerying))
{
if (GUILayout.Button("查询第1页", GUILayout.Width(80)))
{
_browseStatus = "查询中...";
browser.QueryPage(1);
}
}
using (new EditorGUI.DisabledScope(browser.IsQuerying || browser.CurrentPage <= 1))
{
if (GUILayout.Button("◀ 上一页", GUILayout.Width(70)))
{
_browseStatus = "查询中...";
browser.QueryPage(browser.CurrentPage - 1);
}
}
using (new EditorGUI.DisabledScope(browser.IsQuerying || browser.CurrentPage >= browser.TotalPages))
{
if (GUILayout.Button("下一页 ▶", GUILayout.Width(70)))
{
_browseStatus = "查询中...";
browser.QueryPage(browser.CurrentPage + 1);
}
}
EditorGUILayout.EndHorizontal();
if (!string.IsNullOrEmpty(_browseStatus))
{
EditorGUILayout.HelpBox(_browseStatus, MessageType.None);
}
EditorGUILayout.Space(4);
// Mod 列表
if (browser.Results.Count == 0 && !browser.IsQuerying)
{
EditorGUILayout.LabelField("暂无结果请点击「查询第1页」", EditorStyles.centeredGreyMiniLabel);
return;
}
_browseScroll = EditorGUILayout.BeginScrollView(_browseScroll);
foreach (var item in browser.Results)
{
EditorGUILayout.BeginVertical("box");
// 标题行
EditorGUILayout.BeginHorizontal();
string subscribedLabel = item.IsSubscribed
? (item.IsInstalled ? "✅ 已订阅+安装" : "⏳ 已订阅(下载中)")
: " 未订阅";
EditorGUILayout.LabelField(
$"{subscribedLabel} [{item.FileId}] {item.Title}",
EditorStyles.boldLabel, GUILayout.ExpandWidth(true));
bool opBusy = browser.IsSubscribeOperating;
if (item.IsSubscribed)
{
using (new EditorGUI.DisabledScope(opBusy))
{
if (GUILayout.Button("取消订阅", GUILayout.Width(72)))
{
_browseStatus = $"正在取消订阅 {item.Title}...";
browser.Unsubscribe(item.FileId);
}
}
}
else
{
using (new EditorGUI.DisabledScope(opBusy))
{
if (GUILayout.Button("订阅", GUILayout.Width(48)))
{
_browseStatus = $"正在订阅 {item.Title}...";
browser.Subscribe(item.FileId);
}
}
}
EditorGUILayout.EndHorizontal();
// 详情
if (!string.IsNullOrEmpty(item.Tags))
EditorGUILayout.LabelField($"标签: {item.Tags}", EditorStyles.miniLabel);
if (!string.IsNullOrEmpty(item.Description))
EditorGUILayout.LabelField(Truncate(item.Description, 120), EditorStyles.wordWrappedMiniLabel);
EditorGUILayout.LabelField($"👍 {item.VotesUp} 👎 {item.VotesDown} 大小: {item.FileSize / 1024:N0} KB",
EditorStyles.miniLabel);
if (item.IsInstalled && !string.IsNullOrEmpty(item.InstallFolder))
EditorGUILayout.LabelField($"安装路径: {item.InstallFolder}", EditorStyles.miniLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(2);
}
EditorGUILayout.EndScrollView();
}
private void OnBrowseQueryCompleted()
{
var browser = WorkshopModBrowser.Instance;
_browseStatus = string.IsNullOrEmpty(browser.LastError)
? $"✅ 查询完成,共 {browser.Results.Count} 条结果(总计 {browser.TotalResults} 个)"
: $"❌ {browser.LastError}";
Repaint();
}
private void OnBrowseSubscribeCompleted(PublishedFileId_t fileId, bool success)
{
_browseStatus = success
? $"✅ 订阅成功 [{fileId}]"
: $"❌ 订阅失败 [{fileId}],请查看 Console";
Repaint();
}
private void OnBrowseUnsubscribeCompleted(PublishedFileId_t fileId, bool success)
{
_browseStatus = success
? $"✅ 已取消订阅 [{fileId}]"
: $"❌ 取消订阅失败 [{fileId}],请查看 Console";
Repaint();
}
// ═══════════════════════════════════════════════════════════
// ⑤ CSV 读写测试
// ═══════════════════════════════════════════════════════════
private void DrawCsvTestTab()
{
EditorGUILayout.LabelField("CSV 读写回环测试4列格式", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"测试 WorkshopModCsv 的 WriteCsv / ReadCsv 方法4列ID | EN | 参考语言 | Translation。\n" +
"只有 Translation 列非空的行才会被读取为有效条目。",
MessageType.Info);
EditorGUILayout.Space(4);
_csvTestRefLang = (MultilingualType)EditorGUILayout.EnumPopup("参考语言第3列标题", _csvTestRefLang);
EditorGUILayout.Space(4);
if (GUILayout.Button("🧪 运行自动回环测试(含特殊字符)", GUILayout.Height(28)))
RunCsvRoundtripTest();
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("手动 CSV 解析测试", EditorStyles.boldLabel);
EditorGUILayout.LabelField("输入 CSV 内容:");
_csvInputScroll = EditorGUILayout.BeginScrollView(_csvInputScroll, GUILayout.Height(120));
_csvTestInput = EditorGUILayout.TextArea(_csvTestInput, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("解析 CSV")) DoParseCsv();
if (GUILayout.Button("生成示例 CSV")) _csvTestInput = GenerateSampleCsv();
EditorGUILayout.EndHorizontal();
if (!string.IsNullOrEmpty(_csvTestOutput))
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("解析结果:");
_csvOutputScroll = EditorGUILayout.BeginScrollView(_csvOutputScroll, GUILayout.Height(180));
EditorGUILayout.TextArea(_csvTestOutput, EditorStyles.wordWrappedLabel, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
}
private void RunCsvRoundtripTest()
{
var sb = new StringBuilder();
sb.AppendLine("══════ CSV 4列回环测试 ══════");
// 构造含特殊字符的测试数据Translation 非空才会被读出)
var entries = new List<TranslationEntry>
{
new TranslationEntry { Id=1, EnglishText="Normal text", ReferenceText="普通文本", Translation="Normal text translated" },
new TranslationEntry { Id=2, EnglishText="Text, with comma", ReferenceText="含逗号,的文本", Translation="翻译,含逗号" },
new TranslationEntry { Id=3, EnglishText="Text with \"quotes\"", ReferenceText="含\"引号\"的文本", Translation="翻译\"引号\"" },
new TranslationEntry { Id=4, EnglishText="Text\nwith\nnewlines", ReferenceText="含\n换行\n文本", Translation="换行\n翻译" },
new TranslationEntry { Id=5, EnglishText="**<123>** nested ref", ReferenceText="**<123>** 嵌套引用", Translation="**<123>** 嵌套翻译" },
new TranslationEntry { Id=6, EnglishText="Empty translation", ReferenceText="空翻译条目", Translation="" }, // 应被跳过
};
var csv = WorkshopModCsv.WriteCsv(_csvTestRefLang, entries);
sb.AppendLine("[生成的 CSV]:");
sb.AppendLine(csv);
var parsed = WorkshopModCsv.ReadCsv(csv, out string refLang);
sb.AppendLine($"[解析结果] 参考语言: {refLang}, 有效条目数: {parsed.Count}");
int passed = 0, failed = 0;
var validEntries = entries.FindAll(e => !string.IsNullOrEmpty(e.Translation));
for (int i = 0; i < validEntries.Count; i++)
{
var expected = validEntries[i];
if (i >= parsed.Count)
{
sb.AppendLine($" ❌ ID={expected.Id}: 缺失");
failed++;
continue;
}
var actual = parsed[i];
bool idOk = actual.Id == expected.Id;
bool enOk = actual.EnglishText == expected.EnglishText;
bool refOk = actual.ReferenceText == expected.ReferenceText;
bool transOk = actual.Translation == expected.Translation;
if (idOk && enOk && refOk && transOk)
{
sb.AppendLine($" ✅ ID={expected.Id}: 通过");
passed++;
}
else
{
sb.AppendLine($" ❌ ID={expected.Id}: 失败");
if (!idOk) sb.AppendLine($" ID: 期望={expected.Id} 实际={actual.Id}");
if (!enOk) sb.AppendLine($" EN: 期望=\"{Escape(expected.EnglishText)}\" 实际=\"{Escape(actual.EnglishText)}\"");
if (!refOk) sb.AppendLine($" Reference: 期望=\"{Escape(expected.ReferenceText)}\" 实际=\"{Escape(actual.ReferenceText)}\"");
if (!transOk) sb.AppendLine($" Translation: 期望=\"{Escape(expected.Translation)}\" 实际=\"{Escape(actual.Translation)}\"");
failed++;
}
}
// 验证空 Translation 被跳过
bool emptySkipped = parsed.Count == validEntries.Count;
if (emptySkipped) { sb.AppendLine(" ✅ 空 Translation 条目: 正确跳过"); passed++; }
else { sb.AppendLine($" ❌ 空 Translation 条目: 未正确跳过 (期望 {validEntries.Count} 条,实际 {parsed.Count} 条)"); failed++; }
sb.AppendLine($"\n══════ 测试结果: {passed} 通过, {failed} 失败 ══════");
_csvTestOutput = sb.ToString();
if (failed == 0) Debug.Log($"✅ CSV 4列回环测试全部通过 ({passed}/{passed})");
else Debug.LogError($"❌ CSV 回环测试 {failed} 项失败");
}
private static string Escape(string s) => s?.Replace("\n", "\\n").Replace("\r", "\\r") ?? "(null)";
private void DoParseCsv()
{
if (string.IsNullOrEmpty(_csvTestInput)) { _csvTestOutput = "输入为空"; return; }
var entries = WorkshopModCsv.ReadCsv(_csvTestInput, out string refLang);
var sb = new StringBuilder();
sb.AppendLine($"参考语言第3列: {refLang}");
sb.AppendLine($"有效条目数Translation非空: {entries.Count}");
sb.AppendLine("---");
foreach (var e in entries)
{
sb.AppendLine($"ID={e.Id} EN=\"{Escape(e.EnglishText)}\" Ref=\"{Escape(e.ReferenceText)}\" Translation=\"{Escape(e.Translation)}\"");
}
_csvTestOutput = sb.ToString();
}
private string GenerateSampleCsv()
{
// 4列示例Translation列前两行填写第三行留空不替换
return $"ID,EN,{_csvTestRefLang},Translation\n" +
"1,Hello World,你好世界,Hola Mundo\n" +
"2,\"Text, with comma\",\"含逗号,的文本\",\"Texto, con coma\"\n" +
"3,Empty translation,空翻译不替换,\n";
}
// ═══════════════════════════════════════════════════════════
// ⑥ 数据检视
// ═══════════════════════════════════════════════════════════
private void DrawInspectTab()
{
EditorGUILayout.LabelField("数据检视", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("查看多语言数据资源和 Mod 文件内容,用于验证导出/加载结果。", MessageType.Info);
// 多语言数据概览
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("多语言数据概览", EditorStyles.boldLabel);
if (GUILayout.Button("加载 Export/Multilingual 资源信息"))
InspectMultilingualData();
// ID 查询
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("按 ID 查询翻译", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_inspectSearchId = (uint)EditorGUILayout.IntField("多语言 ID", (int)_inspectSearchId);
if (GUILayout.Button("查询", GUILayout.Width(60)))
InspectSearchById();
EditorGUILayout.EndHorizontal();
if (!string.IsNullOrEmpty(_inspectSearchResult))
EditorGUILayout.TextArea(_inspectSearchResult, EditorStyles.wordWrappedLabel);
// Mod 文件夹内容
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("检视 Mod 文件夹内容", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_inspectFolder = EditorGUILayout.TextField("文件夹路径", _inspectFolder);
if (GUILayout.Button("浏览...", GUILayout.Width(60)))
{
var selected = EditorUtility.OpenFolderPanel("选择 Mod 文件夹",
WorkshopModExporter.GetModRootPath(), "");
if (!string.IsNullOrEmpty(selected)) { _inspectFolder = selected; InspectModFolder(); }
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("检视文件夹") && !string.IsNullOrEmpty(_inspectFolder))
InspectModFolder();
if (!string.IsNullOrEmpty(_inspectResult))
{
EditorGUILayout.Space(4);
_inspectScroll = EditorGUILayout.BeginScrollView(_inspectScroll, GUILayout.MaxHeight(320));
EditorGUILayout.TextArea(_inspectResult, EditorStyles.wordWrappedLabel, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
}
private void InspectMultilingualData()
{
var data = TH1Resource.ResourceLoader.Load<MultilingualData>("Export/Multilingual");
if (data == null) { _inspectResult = "❌ 无法加载 Export/Multilingual"; return; }
data.RefreshDict();
var sb = new StringBuilder();
sb.AppendLine("═══ MultilingualData 资源概览 ═══");
sb.AppendLine($"总条目数: {data.Items.Count}");
sb.AppendLine($"字典条目数: {data.ItemDict?.Count ?? 0}");
sb.AppendLine($"字体组数: {data.FontGroups.Count}");
int total = data.Items.Count, deprecated = 0;
var langCounts = new Dictionary<MultilingualType, int>();
// 包含 Custom 语种
for (var t = MultilingualType.ZH; t < MultilingualType.Max; t++)
langCounts[t] = 0;
foreach (var item in data.Items)
{
if (item.IsDeprecated) { deprecated++; continue; }
for (var t = MultilingualType.ZH; t < MultilingualType.Max; t++)
{
if (!string.IsNullOrEmpty(item.GetStrByType(t))) langCounts[t]++;
}
}
int active = total - deprecated;
sb.AppendLine($"有效条目: {active} (已弃用: {deprecated})");
sb.AppendLine("--- 各语言覆盖率 ---");
foreach (var kv in langCounts)
{
float pct = active > 0 ? (float)kv.Value / active * 100 : 0;
sb.AppendLine($" {kv.Key}: {kv.Value}/{active} ({pct:F1}%)");
}
sb.AppendLine("--- 前 5 条数据示例 ---");
int shown = 0;
foreach (var item in data.Items)
{
if (item.IsDeprecated) continue;
sb.AppendLine($" [{item.ID}] EN=\"{Truncate(item.EN, 30)}\" ZH=\"{Truncate(item.ZH, 30)}\"");
if (++shown >= 5) break;
}
_inspectResult = sb.ToString();
}
private void InspectSearchById()
{
var data = TH1Resource.ResourceLoader.Load<MultilingualData>("Export/Multilingual");
if (data == null) { _inspectSearchResult = "❌ 无法加载资源"; return; }
data.RefreshDict();
if (data.ItemDict == null || !data.ItemDict.TryGetValue(_inspectSearchId, out var item))
{
_inspectSearchResult = $"❌ 未找到 ID={_inspectSearchId}";
return;
}
var sb = new StringBuilder();
sb.AppendLine($"═══ ID={item.ID} ═══");
sb.AppendLine($"IsDeprecated={item.IsDeprecated} IsProperNoun={item.IsProperNoun} IsDialogue={item.IsDialogue}");
if (!string.IsNullOrEmpty(item.Color)) sb.AppendLine($"Color={item.Color}");
if (!string.IsNullOrEmpty(item.Icon)) sb.AppendLine($"Icon={item.Icon}");
sb.AppendLine("--- 各语言文本 ---");
// 包含 Custom
for (var t = MultilingualType.ZH; t < MultilingualType.Max; t++)
sb.AppendLine($" {t}: \"{Truncate(item.GetStrByType(t), 80)}\"");
_inspectSearchResult = sb.ToString();
}
private void InspectModFolder()
{
if (!Directory.Exists(_inspectFolder))
{
_inspectResult = $"❌ 文件夹不存在: {_inspectFolder}";
return;
}
var sb = new StringBuilder();
sb.AppendLine($"═══ Mod 文件夹: {Path.GetFileName(_inspectFolder)} ═══");
var files = Directory.GetFiles(_inspectFolder);
sb.AppendLine($"文件数: {files.Length}");
foreach (var f in files)
sb.AppendLine($" {Path.GetFileName(f)} ({new FileInfo(f).Length:N0} bytes)");
var info = WorkshopModExporter.ReadModInfo(_inspectFolder);
if (info != null)
{
sb.AppendLine("\n--- mod_info.json ---");
sb.AppendLine($" title: {info.title}");
sb.AppendLine($" author: {info.author}");
sb.AppendLine($" targetLanguage: {info.targetLanguage}");
sb.AppendLine($" description: {info.description}");
sb.AppendLine($" version: {info.version}");
}
else sb.AppendLine("\n❌ mod_info.json 不存在或解析失败");
var csvPath = Path.Combine(_inspectFolder, WorkshopModExporter.TranslationFileName);
if (File.Exists(csvPath))
{
sb.AppendLine("\n--- translation.csv (4列格式) ---");
try
{
var csvContent = File.ReadAllText(csvPath);
var entries = WorkshopModCsv.ReadCsv(csvContent, out string refLang);
sb.AppendLine($" 参考语言第3列: {refLang}");
sb.AppendLine($" 有效翻译条目Translation非空: {entries.Count}");
sb.AppendLine(" --- 前 10 条 ---");
for (int i = 0; i < Math.Min(10, entries.Count); i++)
{
var e = entries[i];
sb.AppendLine($" [{e.Id}] EN=\"{Truncate(e.EnglishText,20)}\" Ref=\"{Truncate(e.ReferenceText,20)}\" → \"{Truncate(e.Translation,20)}\"");
}
if (entries.Count > 10) sb.AppendLine($" ... 还有 {entries.Count - 10} 条");
}
catch (Exception e) { sb.AppendLine($" ❌ 解析失败: {e.Message}"); }
}
else sb.AppendLine("\n❌ translation.csv 不存在");
_inspectResult = sb.ToString();
}
private static string Truncate(string s, int maxLen)
{
if (string.IsNullOrEmpty(s)) return "(空)";
s = s.Replace("\n", "\\n").Replace("\r", "");
return s.Length <= maxLen ? s : s.Substring(0, maxLen) + "...";
}
}