972 lines
42 KiB
C#
972 lines
42 KiB
C#
/*
|
||
* @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.csv(4列格式)\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) + "...";
|
||
}
|
||
}
|