Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion Plain Craft Launcher 2/FormMain.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1402,6 +1401,11 @@ public enum PageType
/// </summary>
CompDetail = 8,

/// <summary>
/// 帮助详情。这是一个副页面。
/// </summary>
HelpDetail = 9,

/// <summary>
/// 游戏实时日志。这是一个副页面。
/// </summary>
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -1595,6 +1603,11 @@ public class PageStackData
string SavePath
)? additional;

/// <summary>
/// 帮助详情页实例。仅在 <see cref="PageType.HelpDetail"/> 中使用。
/// </summary>
public PageHelpDetail? helpPage;

public PageType page;

public override bool Equals(object other)
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
105 changes: 100 additions & 5 deletions Plain Craft Launcher 2/Modules/Event/CustomEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using PCL.Core.App.Configuration;
using PCL.Core.App.Localization;
using PCL.Core.Utils.OS;
using PCL.Network;

namespace PCL
{
Expand Down Expand Up @@ -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,
};

/// <summary>
Expand Down Expand Up @@ -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);
}

/// <summary>
/// 打开帮助或自定义主页页面
/// </summary>
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);
Expand All @@ -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];
Expand Down
24 changes: 24 additions & 0 deletions Plain Craft Launcher 2/Pages/PageHelpDetail.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<local:MyPageRight x:Class="PCL.PageHelpDetail"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL"
PanScroll="{Binding ElementName=PanBack}">
<local:MyScrollViewer x:Name="PanBack"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel x:Name="PanCustom" Margin="25,25,25,10">
<StackPanel.Resources>
<Style TargetType="TextBlock" BasedOn="{StaticResource BasedOnTextBlock}">
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style TargetType="local:MyCard">
<Setter Property="Margin" Value="0,0,0,15" />
</Style>
<Style TargetType="Image">
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality" />
<Setter Property="HorizontalAlignment" Value="Center" />
</Style>
</StackPanel.Resources>
</StackPanel>
</local:MyScrollViewer>
</local:MyPageRight>
86 changes: 86 additions & 0 deletions Plain Craft Launcher 2/Pages/PageHelpDetail.xaml.cs
Original file line number Diff line number Diff line change
@@ -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);
}

/// <summary>
/// 从帮助 JSON 及同名 XAML 文件读取详情页数据。失败会抛出异常。
/// </summary>
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);
Comment on lines +31 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape replacement values as JSON, not XML

When the help JSON contains a launcher placeholder such as {path} or {version_path}, ArgumentReplace passes its Windows value through SecurityElement.Escape, which does not escape backslashes for JSON. The resulting value contains sequences such as C:\PCL/\P, so JsonDocument.Parse treats them as invalid JSON escapes and the custom help page never opens. Use JSON string escaping (or parse/replace the relevant JSON value) instead of XML escaping.

Useful? React with 👍 / 👎.

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 =
$"<StackPanel xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:sys=\"clr-namespace:System;assembly=System.Runtime\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:local=\"clr-namespace:PCL;assembly=Plain Craft Launcher 2\">{xaml}</StackPanel>";

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));
}
}
}
Loading