diff --git a/Source/GrasscutterTools/Game/TextMapData.cs b/Source/GrasscutterTools/Game/TextMapData.cs index e019f46..4e70232 100644 --- a/Source/GrasscutterTools/Game/TextMapData.cs +++ b/Source/GrasscutterTools/Game/TextMapData.cs @@ -42,6 +42,7 @@ namespace GrasscutterTools.Game public TextMapData(string resourcesDirPath) { + ManualTextMap = new Dictionary(); LoadManualTextMap(Path.Combine(resourcesDirPath, "ExcelBinOutput", "ManualTextMapConfigData.json")); _textMapDirPath = Path.Combine(resourcesDirPath, "TextMap"); LoadTextMaps(_textMapDirPath); @@ -205,4 +206,4 @@ namespace GrasscutterTools.Game return false; } } -} \ No newline at end of file +} diff --git a/Source/GrasscutterTools/GrasscutterTools.csproj b/Source/GrasscutterTools/GrasscutterTools.csproj index 7597737..60ff939 100644 --- a/Source/GrasscutterTools/GrasscutterTools.csproj +++ b/Source/GrasscutterTools/GrasscutterTools.csproj @@ -361,6 +361,7 @@ + @@ -891,4 +892,4 @@ - \ No newline at end of file + diff --git a/Source/GrasscutterTools/Pages/PageTools.Designer.cs b/Source/GrasscutterTools/Pages/PageTools.Designer.cs index 58ff59c..72b8a39 100644 --- a/Source/GrasscutterTools/Pages/PageTools.Designer.cs +++ b/Source/GrasscutterTools/Pages/PageTools.Designer.cs @@ -1,4 +1,4 @@ -namespace GrasscutterTools.Pages +namespace GrasscutterTools.Pages { partial class PageTools { @@ -28,100 +28,187 @@ /// private void InitializeComponent() { - this.BtnUpdateResources = new System.Windows.Forms.Button(); this.LblProjectResRoot = new System.Windows.Forms.Label(); this.TxtProjectResRoot = new System.Windows.Forms.TextBox(); this.LblGcResRoot = new System.Windows.Forms.Label(); this.TxtGcResRoot = new System.Windows.Forms.TextBox(); + this.LblResourceSource = new System.Windows.Forms.Label(); + this.CmbResourceSource = new System.Windows.Forms.ComboBox(); + this.LblRemoteRepository = new System.Windows.Forms.Label(); + this.TxtRemoteRepository = new System.Windows.Forms.TextBox(); + this.LblRemoteBranch = new System.Windows.Forms.Label(); + this.TxtRemoteBranch = new System.Windows.Forms.TextBox(); + this.LblRemoteToken = new System.Windows.Forms.Label(); + this.TxtRemoteToken = new System.Windows.Forms.TextBox(); + this.BtnGenerateToken = new System.Windows.Forms.Button(); this.BtnUpdateAllResources = new System.Windows.Forms.Button(); - this.BtnUpdateActivity = new System.Windows.Forms.Button(); - this.BtnUpdateBannerTitles = new System.Windows.Forms.Button(); + this.PrgUpdateResources = new System.Windows.Forms.ProgressBar(); + this.LblUpdateProgress = new System.Windows.Forms.Label(); this.SuspendLayout(); - // - // BtnUpdateResources - // - this.BtnUpdateResources.Location = new System.Drawing.Point(41, 195); - this.BtnUpdateResources.Name = "BtnUpdateResources"; - this.BtnUpdateResources.Size = new System.Drawing.Size(100, 30); - this.BtnUpdateResources.TabIndex = 0; - this.BtnUpdateResources.Text = "Update res"; - this.BtnUpdateResources.UseVisualStyleBackColor = true; - this.BtnUpdateResources.Click += new System.EventHandler(this.BtnUpdateResources_Click); - // + // // LblProjectResRoot - // + // this.LblProjectResRoot.AutoSize = true; - this.LblProjectResRoot.Location = new System.Drawing.Point(38, 34); + this.LblProjectResRoot.Location = new System.Drawing.Point(38, 12); this.LblProjectResRoot.Name = "LblProjectResRoot"; this.LblProjectResRoot.Size = new System.Drawing.Size(141, 17); this.LblProjectResRoot.TabIndex = 1; this.LblProjectResRoot.Text = "项目 Resources/ 路径:"; - // + // // TxtProjectResRoot - // - this.TxtProjectResRoot.Location = new System.Drawing.Point(185, 31); + // + this.TxtProjectResRoot.Location = new System.Drawing.Point(185, 9); this.TxtProjectResRoot.Name = "TxtProjectResRoot"; this.TxtProjectResRoot.Size = new System.Drawing.Size(413, 23); this.TxtProjectResRoot.TabIndex = 2; - // + // // LblGcResRoot - // + // this.LblGcResRoot.AutoSize = true; - this.LblGcResRoot.Location = new System.Drawing.Point(44, 63); + this.LblGcResRoot.Location = new System.Drawing.Point(38, 68); this.LblGcResRoot.Name = "LblGcResRoot"; this.LblGcResRoot.Size = new System.Drawing.Size(135, 17); this.LblGcResRoot.TabIndex = 3; - this.LblGcResRoot.Text = "GC_Resources/ 路径:"; - // + this.LblGcResRoot.Text = "本地 GC_Resources/:"; + // // TxtGcResRoot - // - this.TxtGcResRoot.Location = new System.Drawing.Point(185, 60); + // + this.TxtGcResRoot.Location = new System.Drawing.Point(185, 65); this.TxtGcResRoot.Name = "TxtGcResRoot"; this.TxtGcResRoot.Size = new System.Drawing.Size(413, 23); this.TxtGcResRoot.TabIndex = 4; - // + // + // LblResourceSource + // + this.LblResourceSource.AutoSize = true; + this.LblResourceSource.Location = new System.Drawing.Point(38, 40); + this.LblResourceSource.Name = "LblResourceSource"; + this.LblResourceSource.Size = new System.Drawing.Size(141, 17); + this.LblResourceSource.TabIndex = 5; + this.LblResourceSource.Text = "GC_Resources 来源:"; + // + // CmbResourceSource + // + this.CmbResourceSource.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.CmbResourceSource.FormattingEnabled = true; + this.CmbResourceSource.Items.AddRange(new object[] { + "本地目录", + "远程 GitLab 仓库"}); + this.CmbResourceSource.Location = new System.Drawing.Point(185, 37); + this.CmbResourceSource.Name = "CmbResourceSource"; + this.CmbResourceSource.Size = new System.Drawing.Size(200, 25); + this.CmbResourceSource.TabIndex = 6; + this.CmbResourceSource.SelectedIndexChanged += new System.EventHandler(this.CmbResourceSource_SelectedIndexChanged); + // + // LblRemoteRepository + // + this.LblRemoteRepository.AutoSize = true; + this.LblRemoteRepository.Location = new System.Drawing.Point(38, 96); + this.LblRemoteRepository.Name = "LblRemoteRepository"; + this.LblRemoteRepository.Size = new System.Drawing.Size(141, 17); + this.LblRemoteRepository.TabIndex = 7; + this.LblRemoteRepository.Text = "远程仓库地址:"; + // + // TxtRemoteRepository + // + this.TxtRemoteRepository.Location = new System.Drawing.Point(185, 93); + this.TxtRemoteRepository.Name = "TxtRemoteRepository"; + this.TxtRemoteRepository.Size = new System.Drawing.Size(413, 23); + this.TxtRemoteRepository.TabIndex = 8; + // + // LblRemoteBranch + // + this.LblRemoteBranch.AutoSize = true; + this.LblRemoteBranch.Location = new System.Drawing.Point(38, 125); + this.LblRemoteBranch.Name = "LblRemoteBranch"; + this.LblRemoteBranch.Size = new System.Drawing.Size(141, 17); + this.LblRemoteBranch.TabIndex = 9; + this.LblRemoteBranch.Text = "远程分支:"; + // + // TxtRemoteBranch + // + this.TxtRemoteBranch.Location = new System.Drawing.Point(185, 122); + this.TxtRemoteBranch.Name = "TxtRemoteBranch"; + this.TxtRemoteBranch.Size = new System.Drawing.Size(200, 23); + this.TxtRemoteBranch.TabIndex = 10; + // + // LblRemoteToken + // + this.LblRemoteToken.AutoSize = true; + this.LblRemoteToken.Location = new System.Drawing.Point(38, 154); + this.LblRemoteToken.Name = "LblRemoteToken"; + this.LblRemoteToken.Size = new System.Drawing.Size(141, 17); + this.LblRemoteToken.TabIndex = 11; + this.LblRemoteToken.Text = "GitLab Token:"; + // + // TxtRemoteToken + // + this.TxtRemoteToken.Location = new System.Drawing.Point(185, 151); + this.TxtRemoteToken.Name = "TxtRemoteToken"; + this.TxtRemoteToken.PasswordChar = '*'; + this.TxtRemoteToken.Size = new System.Drawing.Size(310, 23); + this.TxtRemoteToken.TabIndex = 12; + // + // BtnGenerateToken + // + this.BtnGenerateToken.Location = new System.Drawing.Point(501, 149); + this.BtnGenerateToken.Name = "BtnGenerateToken"; + this.BtnGenerateToken.Size = new System.Drawing.Size(97, 27); + this.BtnGenerateToken.TabIndex = 13; + this.BtnGenerateToken.Text = "生成 Token"; + this.BtnGenerateToken.UseVisualStyleBackColor = true; + this.BtnGenerateToken.Click += new System.EventHandler(this.BtnGenerateToken_Click); + // // BtnUpdateAllResources - // - this.BtnUpdateAllResources.Location = new System.Drawing.Point(41, 100); + // + this.BtnUpdateAllResources.Location = new System.Drawing.Point(41, 190); this.BtnUpdateAllResources.Name = "BtnUpdateAllResources"; - this.BtnUpdateAllResources.Size = new System.Drawing.Size(150, 30); + this.BtnUpdateAllResources.Size = new System.Drawing.Size(180, 30); this.BtnUpdateAllResources.TabIndex = 0; this.BtnUpdateAllResources.Text = "Update All Resources"; this.BtnUpdateAllResources.UseVisualStyleBackColor = true; this.BtnUpdateAllResources.Click += new System.EventHandler(this.BtnUpdateAllResources_Click); - // - // BtnUpdateActivity - // - this.BtnUpdateActivity.Location = new System.Drawing.Point(197, 100); - this.BtnUpdateActivity.Name = "BtnUpdateActivity"; - this.BtnUpdateActivity.Size = new System.Drawing.Size(150, 30); - this.BtnUpdateActivity.TabIndex = 0; - this.BtnUpdateActivity.Text = "Update Activity"; - this.BtnUpdateActivity.UseVisualStyleBackColor = true; - this.BtnUpdateActivity.Click += new System.EventHandler(this.BtnUpdateActivity_Click); - // - // BtnUpdateBannerTitles - // - this.BtnUpdateBannerTitles.Location = new System.Drawing.Point(353, 100); - this.BtnUpdateBannerTitles.Name = "BtnUpdateBannerTitles"; - this.BtnUpdateBannerTitles.Size = new System.Drawing.Size(150, 30); - this.BtnUpdateBannerTitles.TabIndex = 0; - this.BtnUpdateBannerTitles.Text = "Update Banner Titles"; - this.BtnUpdateBannerTitles.UseVisualStyleBackColor = true; - this.BtnUpdateBannerTitles.Click += new System.EventHandler(this.BtnUpdateBannerTitles_Click); - // + // + // PrgUpdateResources + // + this.PrgUpdateResources.Location = new System.Drawing.Point(235, 195); + this.PrgUpdateResources.Name = "PrgUpdateResources"; + this.PrgUpdateResources.Size = new System.Drawing.Size(363, 20); + this.PrgUpdateResources.Style = System.Windows.Forms.ProgressBarStyle.Continuous; + this.PrgUpdateResources.TabIndex = 14; + this.PrgUpdateResources.Visible = false; + // + // LblUpdateProgress + // + this.LblUpdateProgress.AutoEllipsis = true; + this.LblUpdateProgress.AutoSize = false; + this.LblUpdateProgress.Location = new System.Drawing.Point(235, 218); + this.LblUpdateProgress.Name = "LblUpdateProgress"; + this.LblUpdateProgress.Size = new System.Drawing.Size(363, 17); + this.LblUpdateProgress.TabIndex = 15; + this.LblUpdateProgress.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.LblUpdateProgress.Visible = false; + // // PageTools - // + // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.BtnGenerateToken); + this.Controls.Add(this.TxtRemoteToken); + this.Controls.Add(this.LblRemoteToken); + this.Controls.Add(this.TxtRemoteBranch); + this.Controls.Add(this.LblRemoteBranch); + this.Controls.Add(this.TxtRemoteRepository); + this.Controls.Add(this.LblRemoteRepository); + this.Controls.Add(this.CmbResourceSource); + this.Controls.Add(this.LblResourceSource); this.Controls.Add(this.TxtGcResRoot); this.Controls.Add(this.LblGcResRoot); this.Controls.Add(this.TxtProjectResRoot); this.Controls.Add(this.LblProjectResRoot); - this.Controls.Add(this.BtnUpdateBannerTitles); - this.Controls.Add(this.BtnUpdateActivity); this.Controls.Add(this.BtnUpdateAllResources); - this.Controls.Add(this.BtnUpdateResources); + this.Controls.Add(this.LblUpdateProgress); + this.Controls.Add(this.PrgUpdateResources); this.Name = "PageTools"; this.ResumeLayout(false); this.PerformLayout(); @@ -130,13 +217,21 @@ #endregion - private System.Windows.Forms.Button BtnUpdateResources; private System.Windows.Forms.Label LblProjectResRoot; private System.Windows.Forms.TextBox TxtProjectResRoot; private System.Windows.Forms.Label LblGcResRoot; private System.Windows.Forms.TextBox TxtGcResRoot; + private System.Windows.Forms.Label LblResourceSource; + private System.Windows.Forms.ComboBox CmbResourceSource; + private System.Windows.Forms.Label LblRemoteRepository; + private System.Windows.Forms.TextBox TxtRemoteRepository; + private System.Windows.Forms.Label LblRemoteBranch; + private System.Windows.Forms.TextBox TxtRemoteBranch; + private System.Windows.Forms.Label LblRemoteToken; + private System.Windows.Forms.TextBox TxtRemoteToken; + private System.Windows.Forms.Button BtnGenerateToken; private System.Windows.Forms.Button BtnUpdateAllResources; - private System.Windows.Forms.Button BtnUpdateActivity; - private System.Windows.Forms.Button BtnUpdateBannerTitles; + private System.Windows.Forms.ProgressBar PrgUpdateResources; + private System.Windows.Forms.Label LblUpdateProgress; } } diff --git a/Source/GrasscutterTools/Pages/PageTools.cs b/Source/GrasscutterTools/Pages/PageTools.cs index dbaaf8b..8cb8954 100644 --- a/Source/GrasscutterTools/Pages/PageTools.cs +++ b/Source/GrasscutterTools/Pages/PageTools.cs @@ -22,6 +22,8 @@ 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; @@ -29,6 +31,7 @@ using GrasscutterTools.Game; using GrasscutterTools.Game.Activity; using GrasscutterTools.Game.Data; using GrasscutterTools.Properties; +using GrasscutterTools.Utils; using Newtonsoft.Json; @@ -36,6 +39,22 @@ 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() @@ -47,140 +66,257 @@ namespace GrasscutterTools.Pages { 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 void BtnUpdateResources_Click(object sender, EventArgs e) + private const string RemoteResourceRepositoryDefault = "https://gitlab.com/Dimbreath/animegamedata2"; + private const string RemoteResourceBranchDefault = "main"; + + private void CmbResourceSource_SelectedIndexChanged(object sender, EventArgs e) { - var src = new OpenFileDialog - { - Title = "请选择当前原文件", - Multiselect = false, - }; - var dest = new OpenFileDialog - { - Title = "请选择包含新ID的文件", - Multiselect = false, - }; + UpdateResourceSourceControls(); + } - if (src.ShowDialog() == DialogResult.OK && dest.ShowDialog() == DialogResult.OK) - { - var srcLines = File.ReadAllLines(src.FileName); - var srcDic = new Dictionary(srcLines.Length); - foreach (var line in srcLines) - { - var sp = line.IndexOf(':'); - if (sp > 0) - { - var value = line.Substring(sp + 1).Trim(); - if (!value.StartsWith("[N/A]")) - srcDic[line.Substring(0, sp).Trim()] = line.Substring(sp + 1).Trim(); - } - } + private void UpdateResourceSourceControls() + { + var useRemote = CmbResourceSource.SelectedIndex == 1; + TxtGcResRoot.Enabled = !useRemote; + TxtRemoteRepository.Enabled = useRemote; + TxtRemoteBranch.Enabled = useRemote; + TxtRemoteToken.Enabled = useRemote; + BtnGenerateToken.Enabled = useRemote; + } - var destLines = File.ReadAllLines(dest.FileName); - using (var outStream = File.Create(dest.FileName)) - using (var outTxtStream = new StreamWriter(outStream)) - { - foreach (var line in destLines) - { - var sp = line.IndexOf(':'); - if (sp == -1) - { - outTxtStream.WriteLine(line); - } - else - { - var key = line.Substring(0, sp).Trim(); - if (!srcDic.TryGetValue(key, out var value)) - value = line.Substring(sp + 1).Trim(); - outTxtStream.WriteLine($"{key}:{value}"); - } - } - } - } + 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) || string.IsNullOrEmpty(TxtGcResRoot.Text)) + if (string.IsNullOrEmpty(TxtProjectResRoot.Text)) { - MessageBox.Show("请先填写资源目录路径!", Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show("请先填写项目 Resources 路径!", Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } - if (Directory.Exists(TxtProjectResRoot.Text) && Directory.Exists(TxtGcResRoot.Text)) + var useRemote = CmbResourceSource.SelectedIndex == 1; + if (Directory.Exists(TxtProjectResRoot.Text) && + (useRemote || Directory.Exists(TxtGcResRoot.Text))) { - Settings.Default.ResourcesDirPath = TxtGcResRoot.Text; - Settings.Default.ProjectResourcePath = TxtProjectResRoot.Text; + SaveResourceSettings(); return true; } else { - MessageBox.Show("请填写正确的Res路径!", Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show(useRemote ? "请填写正确的项目 Resources 路径!" : "请填写正确的 Res 路径!", + Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } } - private TextMapData TextMapData; - private GameResources GameResources; - - private void BtnUpdateAllResources_Click(object sender, EventArgs e) + 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 { - if (!CheckInputPaths()) return; - - TextMapData ??= new TextMapData(TxtGcResRoot.Text); - GameResources ??= new GameResources(TxtGcResRoot.Text, TextMapData); - - GameResources.ConvertResources(TxtProjectResRoot.Text); - MessageBox.Show("OK", Resources.Tips, MessageBoxButtons.OK); + 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 void BtnUpdateActivity_Click(object sender, EventArgs e) + private async Task RunUpdateAllResourcesAsync(bool useRemote, string localResourcePath, + string projectResourcesPath, string repositoryUrl, string branch, string token, + IProgress progress) { + RemoteResourceLoader remoteSource = null; try { - if (!CheckInputPaths()) return; + 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 目录"); + } - var json = File.ReadAllText( - Path.Combine(TxtGcResRoot.Text, "ExcelBinOutput", "NewActivityExcelConfigData.json"), + 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>(json); + var activityItems = JsonConvert.DeserializeObject>(activityJson); + if (activityItems == null) + throw new InvalidDataException("NewActivityExcelConfigData.json 内容无效。"); - TextMapData ??= new TextMapData(TxtGcResRoot.Text); - - // 确保使用中文UI文化加载GameData,防止ConvertResources留下的ru-ru状态 + ReportProgress(progress, 74, "加载活动名称资源"); var savedCulture = System.Threading.Thread.CurrentThread.CurrentUICulture; - System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-cn"); - GameData.LoadResources(); - System.Threading.Thread.CurrentThread.CurrentUICulture = savedCulture; + try + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-cn"); + GameData.LoadResources(); + } + finally + { + System.Threading.Thread.CurrentThread.CurrentUICulture = savedCulture; + } - UpdateActivityForLanguage(activityItems, "zh-cn"); - UpdateActivityForLanguage(activityItems, "zh-tw"); - UpdateActivityForLanguage(activityItems, "en-us"); - UpdateActivityForLanguage(activityItems, "ru-ru"); - MessageBox.Show("OK", Resources.Tips, MessageBoxButtons.OK); + 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, "所有资源更新完成"); } - catch (Exception ex) + finally { - MessageBox.Show(ex.ToString(), Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + remoteSource?.Dispose(); } } - private void UpdateActivityForLanguage(IReadOnlyCollection activityItems, string languageCode) + private void SetUpdateBusy(bool busy) { - TextMapData.LoadTextMapByLanguage(languageCode); + 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); + activityMap[item.ActivityId] = textMapData.GetText(item.NameTextMapHash); var buffer = new StringBuilder(); var processedIds = new HashSet(); @@ -228,53 +364,35 @@ namespace GrasscutterTools.Pages } } - var activityFilePath = Path.Combine(TxtProjectResRoot.Text, languageCode, "Activity.txt"); + var activityFilePath = Path.Combine(projectResourcesPath, languageCode, "Activity.txt"); File.WriteAllText(activityFilePath, buffer.ToString(), Encoding.UTF8); } - private void UpdateGachaResourceForLanguage(string languageCode) + private void UpdateGachaResourceForLanguage(string languageCode, string projectResourcesPath, + TextMapData textMapData) { - TextMapData.LoadTextMapByLanguage(languageCode); + textMapData.LoadTextMapByLanguage(languageCode); var titleBuffer = new StringBuilder(); const string titlePattern = "UI_GACHA_SHOW_PANEL_([^_]+?)_TITLE"; const string markPattern = "<[^>]+>"; - foreach (var kv in TextMapData.ManualTextMap) + foreach (var kv in textMapData.ManualTextMap) { var titleId = Regex.Match(kv.Value, titlePattern); if (!titleId.Success) continue; - var text = TextMapData.TextMap[kv.Key]; + var text = textMapData.TextMap[kv.Key]; titleBuffer.Append(titleId.Groups[1].Captures[0].Value) .Append(":") .AppendLine(Regex.Replace(text, markPattern, "")); } - var titleFilePath = Path.Combine(TxtProjectResRoot.Text, languageCode, "GachaBannerTitle.txt"); + var titleFilePath = Path.Combine(projectResourcesPath, languageCode, "GachaBannerTitle.txt"); File.WriteAllText(titleFilePath, titleBuffer.ToString(), Encoding.UTF8); } - private void BtnUpdateBannerTitles_Click(object sender, EventArgs e) - { - try - { - if (!CheckInputPaths()) return; - - TextMapData ??= new TextMapData(TxtGcResRoot.Text); - - UpdateGachaResourceForLanguage("zh-cn"); - UpdateGachaResourceForLanguage("zh-tw"); - UpdateGachaResourceForLanguage("en-us"); - UpdateGachaResourceForLanguage("ru-ru"); - MessageBox.Show("OK", Resources.Tips, MessageBoxButtons.OK); - } - catch (Exception ex) - { - MessageBox.Show(ex.ToString(), Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } } -} \ No newline at end of file +} diff --git a/Source/GrasscutterTools/Properties/Settings.Designer.cs b/Source/GrasscutterTools/Properties/Settings.Designer.cs index a3e8e46..04a8f8f 100644 --- a/Source/GrasscutterTools/Properties/Settings.Designer.cs +++ b/Source/GrasscutterTools/Properties/Settings.Designer.cs @@ -274,6 +274,54 @@ namespace GrasscutterTools.Properties { this["ProjectResourcePath"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string RemoteResourceRepository { + get { + return ((string)(this["RemoteResourceRepository"])); + } + set { + this["RemoteResourceRepository"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("main")] + public string RemoteResourceBranch { + get { + return ((string)(this["RemoteResourceBranch"])); + } + set { + this["RemoteResourceBranch"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string RemoteResourceToken { + get { + return ((string)(this["RemoteResourceToken"])); + } + set { + this["RemoteResourceToken"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool UseRemoteResourceRepository { + get { + return ((bool)(this["UseRemoteResourceRepository"])); + } + set { + this["UseRemoteResourceRepository"] = value; + } + } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] diff --git a/Source/GrasscutterTools/Properties/Settings.settings b/Source/GrasscutterTools/Properties/Settings.settings index 527a0c0..e8de41a 100644 --- a/Source/GrasscutterTools/Properties/Settings.settings +++ b/Source/GrasscutterTools/Properties/Settings.settings @@ -65,6 +65,18 @@ + + + + + main + + + + + + False + False @@ -81,4 +93,4 @@ 100 - \ No newline at end of file + diff --git a/Source/GrasscutterTools/Utils/RemoteResourceLoader.cs b/Source/GrasscutterTools/Utils/RemoteResourceLoader.cs new file mode 100644 index 0000000..e29e7ab --- /dev/null +++ b/Source/GrasscutterTools/Utils/RemoteResourceLoader.cs @@ -0,0 +1,320 @@ +/** + * 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. + **/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +using Newtonsoft.Json; + +namespace GrasscutterTools.Utils +{ + /// + /// Downloads only the game data files used by the DEBUG tools page. + /// The returned directory has the same layout as a local GC_Resources directory. + /// + internal sealed class RemoteResourceLoader : IDisposable + { + private static readonly string[] AllExcelFiles = + { + "AchievementExcelConfigData.json", + "AvatarExcelConfigData.json", + "CutsceneExcelConfigData.json", + "DungeonExcelConfigData.json", + "EquipAffixExcelConfigData.json", + "GadgetExcelConfigData.json", + "GatherExcelConfigData.json", + "HomeWorldBgmExcelConfigData.json", + "HomeWorldFurnitureExcelConfigData.json", + "MainQuestExcelConfigData.json", + "MaterialExcelConfigData.json", + "MonsterExcelConfigData.json", + "QuestExcelConfigData.json", + "ReliquaryExcelConfigData.json", + "ReliquarySetExcelConfigData.json", + "SceneExcelConfigData.json", + "SceneTagConfigData.json", + "WeaponExcelConfigData.json", + "WeatherExcelConfigData.json", + }; + + private static readonly string[] FallbackTextMapFiles = + { + "TextMapCHS.json", "TextMap_MediumCHS.json", + "TextMapCHT.json", "TextMap_MediumCHT.json", + "TextMapEN.json", "TextMap_MediumEN.json", + "TextMapRU_0.json", "TextMapRU_1.json", + "TextMap_MediumRU_0.json", "TextMap_MediumRU_1.json", + }; + + private readonly HttpClient _httpClient; + private readonly string _host; + private readonly string _projectPath; + private readonly string _branch; + private readonly string _tempDirectory; + private bool _disposed; + + private sealed class RepositoryTreeItem + { + public string path { get; set; } + public string type { get; set; } + } + + private RemoteResourceLoader(string repositoryUrl, string branch, string accessToken) + { + if (!TryParseRepository(repositoryUrl, out _host, out _projectPath)) + throw new ArgumentException("远程仓库必须是 GitLab 仓库地址,例如 https://gitlab.com/Dimbreath/animegamedata2。", nameof(repositoryUrl)); + + _branch = string.IsNullOrWhiteSpace(branch) ? "main" : branch.Trim(); + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromMinutes(5), + }; + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("GrasscutterTools/DebugResourceLoader"); + if (!string.IsNullOrWhiteSpace(accessToken)) + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("PRIVATE-TOKEN", accessToken.Trim()); + + _tempDirectory = Path.Combine(Path.GetTempPath(), "GrasscutterTools", "remote-resources", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path.Combine(_tempDirectory, "ExcelBinOutput")); + Directory.CreateDirectory(Path.Combine(_tempDirectory, "TextMap")); + } + + public string DirectoryPath => _tempDirectory; + + /// + /// Creates a temporary resource tree and downloads the files needed by one tools action. + /// + public static RemoteResourceLoader Download(string repositoryUrl, string branch, string accessToken, + bool includeAllExcelFiles, bool includeActivityFile, bool includeManualTextMap, + Action reportProgress = null) + { + return DownloadAsync(repositoryUrl, branch, accessToken, includeAllExcelFiles, + includeActivityFile, includeManualTextMap, reportProgress, CancellationToken.None) + .GetAwaiter().GetResult(); + } + + public static async Task DownloadAsync(string repositoryUrl, string branch, + string accessToken, bool includeAllExcelFiles, bool includeActivityFile, + bool includeManualTextMap, Action reportProgress, + CancellationToken cancellationToken) + { + var loader = new RemoteResourceLoader(repositoryUrl, branch, accessToken); + try + { + var requiredFiles = new List(); + if (includeManualTextMap) + requiredFiles.Add("ExcelBinOutput/ManualTextMapConfigData.json"); + if (includeActivityFile) + requiredFiles.Add("ExcelBinOutput/NewActivityExcelConfigData.json"); + + if (includeAllExcelFiles) + { + foreach (var file in AllExcelFiles) + requiredFiles.Add("ExcelBinOutput/" + file); + } + + await loader.DownloadFilesAsync(requiredFiles, required: true, 0, 35, + "ExcelBinOutput", reportProgress, cancellationToken); + await loader.DownloadTextMapsAsync(reportProgress, cancellationToken); + return loader; + } + catch + { + loader.Dispose(); + throw; + } + } + + private async Task DownloadFilesAsync(IReadOnlyCollection repositoryPaths, bool required, + int progressStart, int progressEnd, string description, Action reportProgress, + CancellationToken cancellationToken) + { + if (repositoryPaths.Count == 0) + return; + + var completed = 0; + Report(reportProgress, progressStart, + $"开始下载 {description}({repositoryPaths.Count} 个文件,并发下载)"); + var tasks = repositoryPaths.Select(async path => + { + await DownloadFileAsync(path, required, cancellationToken); + var current = Interlocked.Increment(ref completed); + var progress = progressStart + (progressEnd - progressStart) * current / repositoryPaths.Count; + Report(reportProgress, progress, + $"下载 {description} {current}/{repositoryPaths.Count}: {Path.GetFileName(path)}"); + }).ToArray(); + await Task.WhenAll(tasks); + } + + private async Task DownloadTextMapsAsync(Action reportProgress, + CancellationToken cancellationToken) + { + Report(reportProgress, 36, "查询远程 TextMap 文件列表"); + var paths = await FindTextMapPathsAsync(cancellationToken); + if (paths.Count == 0) + { + // Older mirrors do not expose the repository tree API. Try the conventional + // file names in that case; missing optional variants are harmless. + paths = FallbackTextMapFiles.Select(file => "TextMap/" + file).ToList(); + Report(reportProgress, 37, "远程文件列表不可用,使用标准 TextMap 文件名"); + } + + var textMapPaths = paths + .Where(IsSupportedTextMap) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + var downloaded = 0; + var completed = 0; + Report(reportProgress, 38, $"开始下载 TextMap({textMapPaths.Count} 个文件,并发下载)"); + var tasks = textMapPaths.Select(async path => + { + if (await DownloadFileAsync(path, required: false, cancellationToken)) + Interlocked.Increment(ref downloaded); + var current = Interlocked.Increment(ref completed); + var progress = 38 + 10 * current / Math.Max(1, textMapPaths.Count); + Report(reportProgress, progress, + $"下载 TextMap {current}/{textMapPaths.Count}: {Path.GetFileName(path)}"); + }).ToArray(); + await Task.WhenAll(tasks); + + if (downloaded == 0) + throw new FileNotFoundException("远程仓库中未找到 CHS、CHT、EN 或 RU TextMap 文件。"); + } + + private async Task> FindTextMapPathsAsync(CancellationToken cancellationToken) + { + var result = new List(); + try + { + for (var page = 1; page <= 20; page++) + { + var url = BuildApiUrl("repository/tree") + + "?path=TextMap&recursive=true&per_page=100&page=" + page + + "&ref=" + Uri.EscapeDataString(_branch); + using (var response = await _httpClient.GetAsync(url, cancellationToken)) + { + if (!response.IsSuccessStatusCode) break; + + var json = await response.Content.ReadAsStringAsync(); + var items = JsonConvert.DeserializeObject>(json); + if (items == null || items.Count == 0) break; + + result.AddRange(items.Where(item => item != null && string.Equals(item.type, "blob", StringComparison.OrdinalIgnoreCase)) + .Select(item => item.path) + .Where(path => !string.IsNullOrWhiteSpace(path))); + if (items.Count < 100) break; + } + } + } + catch (OperationCanceledException) + { + throw; + } + catch + { + // Fall back to the stable names below. The file download will report a useful + // error if the repository uses a layout that cannot be resolved. + } + + return result; + } + + private static bool IsSupportedTextMap(string path) + { + var name = Path.GetFileName(path); + return Regex.IsMatch(name, @"^TextMap.*(?:CHS|CHT|EN|RU)(?:_\d+)?\.json$", RegexOptions.IgnoreCase); + } + + private async Task DownloadFileAsync(string repositoryPath, bool required, + CancellationToken cancellationToken) + { + var rawUrl = BuildRawUrl(repositoryPath); + using (var response = await _httpClient.GetAsync(rawUrl, cancellationToken)) + { + if (!response.IsSuccessStatusCode) + { + if (required) + throw new HttpRequestException($"下载远程资源失败 ({(int)response.StatusCode}): {repositoryPath}"); + return false; + } + + var localPath = Path.GetFullPath(Path.Combine(_tempDirectory, + repositoryPath.Replace('/', Path.DirectorySeparatorChar))); + var tempRoot = Path.GetFullPath(_tempDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + if (!localPath.StartsWith(tempRoot, StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("远程仓库返回了无效的资源路径。"); + + Directory.CreateDirectory(Path.GetDirectoryName(localPath)); + File.WriteAllBytes(localPath, await response.Content.ReadAsByteArrayAsync()); + return true; + } + } + + private static void Report(Action reportProgress, int value, string message) + { + reportProgress?.Invoke(Math.Max(0, Math.Min(100, value)), message); + } + + private string BuildApiUrl(string endpoint) + { + return _host + "/api/v4/projects/" + Uri.EscapeDataString(_projectPath) + "/" + endpoint; + } + + private string BuildRawUrl(string repositoryPath) + { + var escapedBranch = string.Join("/", _branch.Split('/').Select(Uri.EscapeDataString)); + var escapedPath = string.Join("/", repositoryPath.Split('/').Select(Uri.EscapeDataString)); + return _host + "/" + _projectPath + "/-/raw/" + escapedBranch + "/" + escapedPath; + } + + private static bool TryParseRepository(string value, out string host, out string projectPath) + { + host = null; + projectPath = null; + if (!Uri.TryCreate(value?.Trim(), UriKind.Absolute, out var uri) || + (uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp)) + return false; + + var path = uri.AbsolutePath.Trim('/'); + var separator = path.IndexOf("/-/", StringComparison.Ordinal); + if (separator >= 0) + path = path.Substring(0, separator); + if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) + path = path.Substring(0, path.Length - 4); + + if (string.IsNullOrWhiteSpace(path)) return false; + host = uri.GetLeftPart(UriPartial.Authority).TrimEnd('/'); + projectPath = path; + return true; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _httpClient.Dispose(); + try + { + if (Directory.Exists(_tempDirectory)) + Directory.Delete(_tempDirectory, true); + } + catch + { + // Best effort cleanup. The OS will reclaim the directory eventually if a file + // is still held by a failed download. + } + } + } +}