/** * Grasscutter Tools * Copyright (C) 2023 jie65535 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * **/ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Text.RegularExpressions; using System.Windows.Forms; using GrasscutterTools.Game; using GrasscutterTools.Game.Activity; using GrasscutterTools.Game.Data; using GrasscutterTools.Properties; using GrasscutterTools.Utils; using Newtonsoft.Json; namespace GrasscutterTools.Pages { internal partial class PageTools : BasePage { private const string LogTag = nameof(PageTools); private sealed class UpdateProgressInfo { public UpdateProgressInfo(int value, string message) { Value = value; Message = message; } public int Value { get; } public string Message { get; } } private bool _updateInProgress; public override string Text => "Tools"; public PageTools() { InitializeComponent(); } public override void OnLoad() { TxtGcResRoot.Text = Settings.Default.ResourcesDirPath; TxtProjectResRoot.Text = Settings.Default.ProjectResourcePath; TxtRemoteRepository.Text = string.IsNullOrWhiteSpace(Settings.Default.RemoteResourceRepository) ? RemoteResourceRepositoryDefault : Settings.Default.RemoteResourceRepository; TxtRemoteBranch.Text = string.IsNullOrWhiteSpace(Settings.Default.RemoteResourceBranch) ? RemoteResourceBranchDefault : Settings.Default.RemoteResourceBranch; TxtRemoteToken.Text = Settings.Default.RemoteResourceToken; CmbResourceSource.SelectedIndex = Settings.Default.UseRemoteResourceRepository ? 1 : 0; UpdateResourceSourceControls(); } private const string RemoteResourceRepositoryDefault = "https://gitlab.com/Dimbreath/animegamedata2"; private const string RemoteResourceBranchDefault = "main"; private void CmbResourceSource_SelectedIndexChanged(object sender, EventArgs e) { UpdateResourceSourceControls(); } private void UpdateResourceSourceControls() { var useRemote = CmbResourceSource.SelectedIndex == 1; TxtGcResRoot.Enabled = !useRemote; TxtRemoteRepository.Enabled = useRemote; TxtRemoteBranch.Enabled = useRemote; TxtRemoteToken.Enabled = useRemote; BtnGenerateToken.Enabled = useRemote; } private void BtnGenerateToken_Click(object sender, EventArgs e) { UIUtil.OpenURL("https://gitlab.com/-/user_settings/personal_access_tokens?name=GrasscutterTools&scopes=read_api,read_repository"); } public override void OnClosed() { SaveResourceSettings(); } private void SaveResourceSettings() { Settings.Default.ResourcesDirPath = TxtGcResRoot.Text; Settings.Default.ProjectResourcePath = TxtProjectResRoot.Text; Settings.Default.RemoteResourceRepository = TxtRemoteRepository.Text.Trim(); Settings.Default.RemoteResourceBranch = TxtRemoteBranch.Text.Trim(); Settings.Default.RemoteResourceToken = TxtRemoteToken.Text.Trim(); Settings.Default.UseRemoteResourceRepository = CmbResourceSource.SelectedIndex == 1; } private bool CheckInputPaths() { if (string.IsNullOrEmpty(TxtProjectResRoot.Text)) { MessageBox.Show("请先填写项目 Resources 路径!", Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } var useRemote = CmbResourceSource.SelectedIndex == 1; if (Directory.Exists(TxtProjectResRoot.Text) && (useRemote || Directory.Exists(TxtGcResRoot.Text))) { SaveResourceSettings(); return true; } else { MessageBox.Show(useRemote ? "请填写正确的项目 Resources 路径!" : "请填写正确的 Res 路径!", Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } } private async void BtnUpdateAllResources_Click(object sender, EventArgs e) { if (_updateInProgress || !CheckInputPaths()) return; var useRemote = CmbResourceSource.SelectedIndex == 1; var projectResourcesPath = TxtProjectResRoot.Text.Trim(); var localResourcePath = useRemote ? null : TxtGcResRoot.Text.Trim(); var repositoryUrl = TxtRemoteRepository.Text.Trim(); var branch = TxtRemoteBranch.Text.Trim(); var token = TxtRemoteToken.Text.Trim(); var progress = new Progress(UpdateProgressOnUi); SetUpdateBusy(true); var completed = false; try { await Task.Run(() => RunUpdateAllResourcesAsync( useRemote, localResourcePath, projectResourcesPath, repositoryUrl, branch, token, progress)); completed = true; } catch (Exception ex) { Logger.E(LogTag, "更新所有资源失败", ex); MessageBox.Show(ex.ToString(), Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { SetUpdateBusy(false); } if (completed) MessageBox.Show("OK", Resources.Tips, MessageBoxButtons.OK); } private async Task RunUpdateAllResourcesAsync(bool useRemote, string localResourcePath, string projectResourcesPath, string repositoryUrl, string branch, string token, IProgress progress) { RemoteResourceLoader remoteSource = null; try { ReportProgress(progress, 0, "开始更新所有资源"); string resourcePath; if (useRemote) { ReportProgress(progress, 2, "准备连接远程 GitLab 仓库"); remoteSource = await RemoteResourceLoader.DownloadAsync( repositoryUrl, branch, token, includeAllExcelFiles: true, includeActivityFile: true, includeManualTextMap: true, reportProgress: (value, message) => ReportProgress(progress, 5 + value * 40 / 100, message), cancellationToken: CancellationToken.None); resourcePath = remoteSource.DirectoryPath; ReportProgress(progress, 46, "远程资源下载完成"); } else { resourcePath = localResourcePath; ReportProgress(progress, 5, "使用本地 GC_Resources 目录"); } ReportProgress(progress, 48, "加载 TextMap 数据"); var textMapData = new TextMapData(resourcePath); ReportProgress(progress, 52, "加载 ExcelBinOutput 数据"); var gameResources = new GameResources(resourcePath, textMapData); ReportProgress(progress, 55, "生成基础资源文件"); gameResources.ConvertResources(projectResourcesPath); ReportProgress(progress, 72, "基础资源文件生成完成"); var activityJson = File.ReadAllText( Path.Combine(resourcePath, "ExcelBinOutput", "NewActivityExcelConfigData.json"), Encoding.UTF8); var activityItems = JsonConvert.DeserializeObject>(activityJson); if (activityItems == null) throw new InvalidDataException("NewActivityExcelConfigData.json 内容无效。"); ReportProgress(progress, 74, "加载活动名称资源"); var savedCulture = System.Threading.Thread.CurrentThread.CurrentUICulture; try { System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-cn"); GameData.LoadResources(); } finally { System.Threading.Thread.CurrentThread.CurrentUICulture = savedCulture; } var activityLanguages = new[] { "zh-cn", "zh-tw", "en-us", "ru-ru" }; for (var i = 0; i < activityLanguages.Length; i++) { var language = activityLanguages[i]; ReportProgress(progress, 76 + i * 3, $"生成活动资源:{language}"); UpdateActivityForLanguage(activityItems, language, projectResourcesPath, textMapData); } var bannerLanguages = new[] { "zh-cn", "zh-tw", "en-us", "ru-ru" }; for (var i = 0; i < bannerLanguages.Length; i++) { var language = bannerLanguages[i]; ReportProgress(progress, 89 + i * 2, $"生成卡池标题:{language}"); UpdateGachaResourceForLanguage(language, projectResourcesPath, textMapData); } ReportProgress(progress, 100, "所有资源更新完成"); } finally { remoteSource?.Dispose(); } } private void SetUpdateBusy(bool busy) { if (IsDisposed || Disposing) return; _updateInProgress = busy; BtnUpdateAllResources.Enabled = !busy; CmbResourceSource.Enabled = !busy; TxtProjectResRoot.Enabled = !busy; TxtGcResRoot.Enabled = !busy; TxtRemoteRepository.Enabled = !busy; TxtRemoteBranch.Enabled = !busy; TxtRemoteToken.Enabled = !busy; BtnGenerateToken.Enabled = !busy; PrgUpdateResources.Visible = busy; LblUpdateProgress.Visible = busy; if (busy) { PrgUpdateResources.Value = 0; LblUpdateProgress.Text = "准备中..."; } else { UpdateResourceSourceControls(); LblUpdateProgress.Text = string.Empty; } UseWaitCursor = busy; var form = FindForm(); if (form != null) form.UseWaitCursor = busy; } private void UpdateProgressOnUi(UpdateProgressInfo progress) { if (IsDisposed || Disposing) return; PrgUpdateResources.Value = Math.Max(0, Math.Min(100, progress.Value)); LblUpdateProgress.Text = progress.Message; Logger.I(LogTag, progress.Message); } private static void ReportProgress(IProgress progress, int value, string message) { progress?.Report(new UpdateProgressInfo(value, message)); } private void UpdateActivityForLanguage(IReadOnlyCollection activityItems, string languageCode, string projectResourcesPath, TextMapData textMapData) { textMapData.LoadTextMapByLanguage(languageCode); // 从 JSON 构建 activityId -> 翻译文本的映射 var activityMap = new Dictionary(); foreach (var item in activityItems) activityMap[item.ActivityId] = textMapData.GetText(item.NameTextMapHash); var buffer = new StringBuilder(); var processedIds = new HashSet(); // 保持原有的分组结构(从 GameData.Activity 读取),保留人工整理的内容 // 旧的 // New 分组跳过,统一在末尾合并输出 foreach (var group in GameData.Activity) { if (group.Key == "New") { foreach (var id in group.Value.Ids) processedIds.Add(id); continue; } buffer.Append("// ").AppendLine(group.Key); foreach (var id in group.Value.Ids) { processedIds.Add(id); buffer.Append(id).Append(':'); // 对于中文,保留人工整理的内容;对于其他语言,使用 JSON 中的官方翻译 if (languageCode == "zh-cn") buffer.AppendLine(group.Value[id]); // 使用旧的人工整理的内容 else buffer.AppendLine(activityMap.TryGetValue(id, out var title) ? title : group.Value[id]); } } // 合并旧的 // New 和 JSON 中新增的活动 var newIds = activityItems .Where(item => !processedIds.Contains(item.ActivityId)) .Select(item => item.ActivityId) .Concat(GameData.Activity.TryGetValue("New", out var oldNewGroup) ? oldNewGroup.Ids : Enumerable.Empty()) .Distinct() .OrderBy(id => id) .ToList(); if (newIds.Count > 0) { buffer.AppendLine("// New"); foreach (var id in newIds) { var name = activityMap.TryGetValue(id, out var title) ? title : GameData.Activity.TryGetValue("New", out var ng) && ng[id] != ItemMap.EmptyName ? ng[id] : title; buffer.Append(id).Append(':').AppendLine(name); } } var activityFilePath = Path.Combine(projectResourcesPath, languageCode, "Activity.txt"); File.WriteAllText(activityFilePath, buffer.ToString(), Encoding.UTF8); } private void UpdateGachaResourceForLanguage(string languageCode, string projectResourcesPath, TextMapData textMapData) { textMapData.LoadTextMapByLanguage(languageCode); var titleBuffer = new StringBuilder(); const string titlePattern = "UI_GACHA_SHOW_PANEL_([^_]+?)_TITLE"; const string markPattern = "<[^>]+>"; foreach (var kv in textMapData.ManualTextMap) { var titleId = Regex.Match(kv.Value, titlePattern); if (!titleId.Success) continue; var text = textMapData.TextMap[kv.Key]; titleBuffer.Append(titleId.Groups[1].Captures[0].Value) .Append(":") .AppendLine(Regex.Replace(text, markPattern, "")); } var titleFilePath = Path.Combine(projectResourcesPath, languageCode, "GachaBannerTitle.txt"); File.WriteAllText(titleFilePath, titleBuffer.ToString(), Encoding.UTF8); } } }