diff --git a/Plain Craft Launcher 2/FormMain.xaml.cs b/Plain Craft Launcher 2/FormMain.xaml.cs
index 3826112f31..184445596f 100644
--- a/Plain Craft Launcher 2/FormMain.xaml.cs
+++ b/Plain Craft Launcher 2/FormMain.xaml.cs
@@ -7,7 +7,6 @@
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
-using System.Windows.Media.Effects;
using PCL.Core.App;
using PCL.Core.App.IoC;
using PCL.Core.App.Localization;
@@ -1402,6 +1401,11 @@ public enum PageType
///
CompDetail = 8,
+ ///
+ /// 帮助详情。这是一个副页面。
+ ///
+ HelpDetail = 9,
+
///
/// 游戏实时日志。这是一个副页面。
///
@@ -1496,6 +1500,10 @@ private string PageNameGet(PageStackData stack)
{
return Lang.Text("Main.Title.ResourceDownload", stack.additional.Value.CompProject.TranslatedName);
}
+ case PageType.HelpDetail:
+ {
+ return stack.helpPage?.Title ?? "";
+ }
case PageType.VersionSaves:
{
return Lang.Text("Main.Title.SaveManagement", ModBase.GetFolderNameFromPath(stack.additional.Value.SavePath));
@@ -1595,6 +1603,11 @@ public class PageStackData
string SavePath
)? additional;
+ ///
+ /// 帮助详情页实例。仅在 中使用。
+ ///
+ public PageHelpDetail? helpPage;
+
public PageType page;
public override bool Equals(object other)
@@ -1606,6 +1619,8 @@ public override bool Equals(object other)
var pageOther = (PageStackData)other;
if (page != pageOther.page)
return false;
+ if (helpPage is not null || pageOther.helpPage is not null)
+ return ReferenceEquals(helpPage, pageOther.helpPage);
if (additional is null) return pageOther.additional is null;
return pageOther.additional is not null && additional.Equals(pageOther.additional);
@@ -1899,6 +1914,13 @@ private void PageChangeActual(PageStackData stack, PageSubType subType)
PageChangeAnim(new MyPageLeft(), ModMain.frmDownloadCompDetail);
break;
}
+ case PageType.HelpDetail: // 帮助详情
+ {
+ if (stack.helpPage is null)
+ throw new InvalidOperationException("帮助详情页面未初始化");
+ PageChangeAnim(new MyPageLeft(), stack.helpPage);
+ break;
+ }
case PageType.VersionSaves: // 存档管理
{
if (ModMain.frmInstanceSavesLeft is null)
diff --git a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs
index 46f3f8e62c..768e4ec777 100644
--- a/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs
+++ b/Plain Craft Launcher 2/Modules/Event/CustomEvent.cs
@@ -3,6 +3,7 @@
using PCL.Core.App.Configuration;
using PCL.Core.App.Localization;
using PCL.Core.Utils.OS;
+using PCL.Network;
namespace PCL
{
@@ -94,7 +95,7 @@ public static string GetCustomVariable(string name, string defaultValue = "") =>
[EventType.WriteSetting] = _WriteSetting,
[EventType.ModifyVariable] = _WriteVariable,
[EventType.WriteVariable] = _WriteVariable,
- [EventType.OpenHelp] = (_, __) => ModBase.OpenWebsite("https://docs.pclc.cc/ce"),
+ [EventType.OpenHelp] = _OpenHelp,
};
///
@@ -318,17 +319,111 @@ private static void _WriteVariable(string arg, EventType type)
HintService.Hint(Lang.Text("Event.Variable.Written", args[0], args[1]), HintType.Success);
}
+ ///
+ /// 打开帮助或自定义主页页面
+ ///
+ private static void _OpenHelp(string arg, EventType type)
+ {
+ var args = SplitArgs(arg);
+ if (!Uri.TryCreate(args[0], UriKind.Absolute, out var uri) ||
+ (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
+ {
+ ModBase.OpenWebsite("https://docs.pclc.cc/ce");
+ return;
+ }
+
+ ModBase.RunInThread(() =>
+ {
+ try
+ {
+ var actualPaths = GetAbsoluteUrls(args[0], type);
+ var location = actualPaths[0];
+ var content = PageHelpDetail.LoadContent(location);
+ ModBase.RunInUiWait(() =>
+ {
+ var page = new PageHelpDetail(content);
+ ModMain.frmMain.PageChange(new FormMain.PageStackData
+ {
+ page = FormMain.PageType.HelpDetail,
+ helpPage = page
+ });
+ });
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(
+ ex,
+ Lang.Text("Event.Error.ExecutionFailed", type, arg),
+ ModBase.LogLevel.Msgbox,
+ userSummary: Lang.Text("Event.Error.ExecutionFailed", type, arg));
+ }
+ });
+ }
+
public static string[] GetAbsoluteUrls(string relativeUrl, EventType type)
{
- relativeUrl = relativeUrl.Replace('/', '\\').ToLower().TrimStart('\\');
+ if (type == EventType.OpenHelp &&
+ Uri.TryCreate(relativeUrl, UriKind.Absolute, out var remoteUri) &&
+ (remoteUri.Scheme == Uri.UriSchemeHttp || remoteUri.Scheme == Uri.UriSchemeHttps))
+ {
+ if (ModBase.RunInUi()) throw new Exception("能打开联网帮助页面的 MyListItem 必须手动设置 Title、Info 属性!");
+
+ // 获取文件名
+ string rawFileName;
+ try
+ {
+ rawFileName = Uri.UnescapeDataString(Path.GetFileName(remoteUri.AbsolutePath));
+ if (!Path.GetExtension(rawFileName).Equals(".json", StringComparison.OrdinalIgnoreCase))
+ throw new Exception("未指向 .json 后缀的文件");
+ if (rawFileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
+ throw new Exception("文件名包含非法字符");
+ }
+ catch (Exception e)
+ {
+ throw new Exception("联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!\n" +
+ "例如:\n" +
+ " - https://www.baidu.com/test.json(填写这个路径)\n" +
+ " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", e);
+ }
+
+ // 下载文件
+ var tempFolder = ModMain.RequestTaskTempFolder();
+ var jsonPath = Path.Combine(tempFolder, rawFileName);
+ var xamlPath = Path.ChangeExtension(jsonPath, ".xaml");
+ var xamlUri = new UriBuilder(remoteUri)
+ {
+ Path = Path.ChangeExtension(remoteUri.AbsolutePath, ".xaml").Replace('\\', '/')
+ }.Uri.AbsoluteUri;
+ ModBase.Log($"[Control] 转换网络帮助资源:{relativeUrl} -> {jsonPath}");
+ try
+ {
+ Task.WhenAll(
+ ModNet.NetDownloadByClient(remoteUri.AbsoluteUri, jsonPath),
+ ModNet.NetDownloadByClient(xamlUri, xamlPath)
+ ).GetAwaiter().GetResult();
+ }
+ catch (Exception e)
+ {
+ throw new Exception("下载指定的文件失败!\n" +
+ "注意,联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!\n" +
+ "例如:\n" +
+ " - https://www.baidu.com/test.json(填写这个路径)\n" +
+ " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", e);
+ }
+
+ relativeUrl = jsonPath;
+ }
+
+ // 确认路径
+ relativeUrl = relativeUrl.Replace('/', Path.DirectorySeparatorChar);
var pclDir = Path.Combine(Basics.ExecutableDirectory, "PCL");
- if (relativeUrl.Contains(":\\"))
+ if (Path.IsPathFullyQualified(relativeUrl)) // 绝对路径
{
ModBase.Log($"[Control] 自定义事件中由绝对路径 {type}: {relativeUrl}");
return [relativeUrl, pclDir];
}
- if (File.Exists(Path.Combine(pclDir, relativeUrl)))
+ if (File.Exists(Path.Combine(pclDir, relativeUrl))) // 相对 PCL 文件夹的路径
{
var fullPath = Path.Combine(pclDir, relativeUrl);
var resolved = Path.GetFullPath(fullPath);
@@ -337,7 +432,7 @@ public static string[] GetAbsoluteUrls(string relativeUrl, EventType type)
ModBase.Log($"[Control] 自定义事件中由相对 PCL 文件夹的路径 {type}: {fullPath}");
return [fullPath, pclDir];
}
- if (type is EventType.OpenFile or EventType.ExecuteCommand)
+ if (type is EventType.OpenFile or EventType.ExecuteCommand) // 直接使用原有路径启动程序
{
ModBase.Log($"[Control] 自定义事件中直接 {type}: {relativeUrl}");
return [relativeUrl, pclDir];
diff --git a/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml b/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml
new file mode 100644
index 0000000000..a33254ad4a
--- /dev/null
+++ b/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml.cs b/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml.cs
new file mode 100644
index 0000000000..d7f01f2c44
--- /dev/null
+++ b/Plain Craft Launcher 2/Pages/PageHelpDetail.xaml.cs
@@ -0,0 +1,86 @@
+using System.IO;
+using System.Security;
+using System.Windows;
+using PCL.Core.App.Localization;
+
+namespace PCL;
+
+public partial class PageHelpDetail : IRefreshable
+{
+ internal sealed record HelpContent(string SourcePath, string Title, string Xaml);
+
+ private HelpContent _content = null!;
+
+ public string Title => _content.Title;
+
+ internal PageHelpDetail(HelpContent content)
+ {
+ InitializeComponent();
+ Loaded += (_, _) => PanBack.ScrollToHome();
+ ApplyContent(content);
+ }
+
+ ///
+ /// 从帮助 JSON 及同名 XAML 文件读取详情页数据。失败会抛出异常。
+ ///
+ internal static HelpContent LoadContent(string jsonPath)
+ {
+ if (!File.Exists(jsonPath))
+ throw new FileNotFoundException("未找到帮助 JSON 文件", jsonPath);
+
+ var json = ModMain.ArgumentReplace(File.ReadAllText(jsonPath), SecurityElement.Escape);
+ using var document = JsonDocument.Parse(json);
+ if (!document.RootElement.TryGetProperty("Title", out var titleElement) ||
+ titleElement.ValueKind != JsonValueKind.String ||
+ string.IsNullOrWhiteSpace(titleElement.GetString()))
+ throw new ArgumentException("帮助 JSON 中未找到有效的 Title 项", nameof(jsonPath));
+
+ var xamlPath = Path.ChangeExtension(jsonPath, ".xaml");
+ if (!File.Exists(xamlPath))
+ throw new FileNotFoundException("未找到帮助 JSON 对应的 XAML 文件", xamlPath);
+
+ var xaml = File.ReadAllText(xamlPath);
+ if (string.IsNullOrWhiteSpace(xaml))
+ throw new InvalidDataException("帮助 XAML 文件为空");
+
+ return new HelpContent(jsonPath, titleElement.GetString()!, xaml);
+ }
+
+ private void ApplyContent(HelpContent content)
+ {
+ // 修改时应同时修改 PageLaunchRight.LoadContent。
+ var xaml = ModMain.ArgumentReplace(content.Xaml);
+ while (xaml.Contains("xmlns"))
+ xaml = xaml.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", "");
+ xaml =
+ $"{xaml}";
+
+ var element = (UIElement)ModBase.GetObjectFromXML(xaml, out var sanitizeResult);
+ foreach (var unsupported in sanitizeResult.UnsupportedTypesFound)
+ HintService.Hint(Lang.Text("Event.Sanitize.UnsupportedTypeHint", unsupported), HintType.Error);
+ foreach (var unknown in sanitizeResult.UnrecognizedTypes)
+ HintService.Hint(Lang.Text("Event.Sanitize.UnknownTypeHint", unknown), HintType.Error);
+
+ _content = content;
+ PanCustom.Children.Clear();
+ PanCustom.Children.Add(element);
+ if (ModMain.frmMain?.pageCurrent.page == FormMain.PageType.HelpDetail)
+ ModMain.frmMain.PageNameRefresh();
+ }
+
+ public void Refresh()
+ {
+ try
+ {
+ ApplyContent(LoadContent(_content.SourcePath));
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(
+ ex,
+ "刷新帮助详情页失败",
+ ModBase.LogLevel.Msgbox,
+ userSummary: Lang.Text("Event.Error.ExecutionFailed", EventType.OpenHelp, _content.SourcePath));
+ }
+ }
+}