Files
MRCC/launcher/PCL-CE/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.cs
T
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

899 lines
34 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.IO;
using System.Globalization;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using PCL.Core.App;
using PCL.Core.Logging;
using PCL.Core.UI;
using PCL.Network;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageLaunchRight : IRefreshable
{
public PageLaunchRight()
{
InitializeComponent();
onlineLoader = new ModLoader.LoaderTask<string, int>(
Lang.Text("Launch.Homepage.Task.Download"),
OnlineLoaderSub)
{
reloadTimeout = 10 * 60 * 1000
};
Loaded += (_, _) => Init();
Loaded += (_, _) => Refresh();
Unloaded += (_, _) => _DisposeHomepageLiveWatcher();
}
private void Init()
{
PanBack.ScrollToHome();
PanScroll = PanBack; // 不知道为啥不能在 XAML 设置
PanLog.Visibility = ModBase.modeDebug ? Visibility.Visible : Visibility.Collapsed;
// 社区版提示
PanHint.Visibility = States.Hint.CEMessage
? Visibility.Visible
: Visibility.Collapsed;
LabHint1.Text = Lang.Text("Launch.Right.CommunityHint.Message");
LabHint2.Text = Lang.Text("Launch.Right.CommunityHint.HidePrompt");
_EnsureHomepageLiveWatcher();
}
// 暂时关闭快照版提示
private void BtnHintClose_Click(object sender, EventArgs e)
{
var input = ModMain.MyMsgBoxInput(Lang.Text("Launch.Right.CommunityHint.InputTitle"));
if (string.IsNullOrWhiteSpace(input))
return;
input = new string(input.Where(char.IsAsciiLetter).ToArray()).ToLower();
if (input.Contains("pclcommunity"))
{
ModAnimation.AniDispose(PanHint, true);
States.Hint.CEMessage = false;
}
else
{
HintService.Hint(Lang.Text("Launch.Right.CommunityHint.WrongInput"));
}
}
#region 主页
/// <summary>
/// 刷新主页。
/// </summary>
private void Refresh()
{
ModBase.RunInNewThread(() =>
{
try
{
lock (refreshLock)
{
RefreshReal();
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
"加载 PCL 主页自定义信息失败",
ModBase.modeDebug
? ModBase.LogLevel.Msgbox
: ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Error.OperationFailed"));
}
}, $"刷新主页 #{ModBase.GetUuid()}");
}
private void RefreshReal()
{
var content = "";
string url = null;
var uiCustomType = (int)Config.Preference.Homepage.Type;
if (uiCustomType == 1)
{
// 本地文件
LogWrapper.Info("[Page] 主页自定义数据来源:本地文件");
content = ModBase.ReadFile(Path.Combine(ModBase.exePath, "PCL", "Custom.xaml"));
}
else if (uiCustomType == 2)
{
// 网络文件
url = (string)Config.Preference.Homepage.CustomUrl;
content = LoadFromNetwork(url);
}
else if (uiCustomType == 3)
{
// 预设主页
var preset = (int)Config.Preference.Homepage.SelectedPreset;
switch (preset)
{
case 0:
LogWrapper.Info("[Page] 主页预设:你知道吗");
var hintText = GetRandomHint();
content = $@"
<local:MyCard Title=""{{DynamicResource Launch.Status.Trivia}}"" Margin=""0,0,0,15"">
<TextBlock Margin=""25,38,23,15"" FontSize=""13.5"" IsHitTestVisible=""False"" Text=""{hintText}"" TextWrapping=""Wrap"" Foreground=""{{DynamicResource ColorBrush1}}"" />
<local:MyIconButton Height=""22"" Width=""22"" Margin=""9"" VerticalAlignment=""Top"" HorizontalAlignment=""Right""
EventType=""刷新主页"" EventData=""/""
SvgIcon=""lucide/refresh-cw"" />
</local:MyCard>";
break;
case 1:
LogWrapper.Info("[Page] 主页预设:回声洞 已被移除");
ModMain.MyMsgBox(Lang.Text("Launch.Homepage.Preset.EchoCave.Removed"));
return;
case 2:
LogWrapper.Info("[Page] 主页预设:Minecraft 新闻");
url = "https://news.bugjump.net";
content = LoadFromNetwork(url);
break;
// case 3:
// LogWrapper.Info("[Page] 主页预设:简单主页");
// url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/MFn233/Custom.xaml";
// content = LoadFromNetwork(url);
// break;
case 3:
LogWrapper.Info("[Page] 主页预设:每日整合包推荐");
url = "https://pclsub.sodamc.com/";
content = LoadFromNetwork(url);
break;
case 4:
LogWrapper.Info("[Page] 主页预设:Minecraft 皮肤推荐");
url = "https://forgepixel.com/pcl_sub_file";
content = LoadFromNetwork(url);
break;
case 5:
LogWrapper.Info("[Page] 主页预设:OpenBMCLAPI 仪表盘 Lite");
url = "https://pcl-bmcl.milu.ink/";
content = LoadFromNetwork(url);
break;
// case 7:
// LogWrapper.Info("[Page] 主页预设:主页市场");
// url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/JingHai-Lingyun/Custom.xaml";
// content = LoadFromNetwork(url);
// break;
// case 8:
// LogWrapper.Info("[Page] 主页预设:更新日志");
// url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/Joker2184/UpdateHomepage.xaml";
// content = LoadFromNetwork(url);
// break;
case 6:
LogWrapper.Info("[Page] 主页预设:PCL 新功能说明书");
url = "https://raw.gitcode.com/WForst-Breeze/WhatsNewPCL/raw/main/Custom.xaml";
content = LoadFromNetwork(url);
break;
// case 10:
// LogWrapper.Info("[Page] 主页预设:OpenMCIM Dashboard");
// url = "https://files.mcimirror.top/PCL";
// content = LoadFromNetwork(url);
// break;
case 7:
LogWrapper.Info("[Page] 主页预设:杂志主页");
url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/Ext1nguisher/Custom.xaml";
content = LoadFromNetwork(url);
break;
case 8:
LogWrapper.Info("[Page] 主页预设:PCL GitHub 仪表盘");
url = "https://ddf.pcl-community.org/Custom.xaml";
content = LoadFromNetwork(url);
break;
case 9:
LogWrapper.Info("[Page] 主页预设:Minecraft 更新摘要");
url = "https://raw.gitcode.com/ENC_Euphony/PCL-AI-Summary-HomePage/raw/master/Custom.xaml";
content = LoadFromNetwork(url);
break;
case 10:
LogWrapper.Info("[Page] 主页预设:今日新闻热点");
url = "https://pcl.wyc-w.top/index.xaml";
content = LoadFromNetwork(url);
break;
case 11:
LogWrapper.Info("[Page] 主页预设:Minecraft 芝士站");
url = "https://www.xxag.top/mkss";
content = LoadFromNetwork(url);
break;
case 12:
LogWrapper.Info("[Page] 主页预设:整合包推荐引擎");
url = "https://qawsedrftgyhujiko.fun/pcl2/Custom.xaml";
content = LoadFromNetwork(url);
break;
case 13:
LogWrapper.Info("[Page] 主页预设:Bangumi 番剧主页");
url = "https://bangumi.p.kaphia.qzz.io";
content = LoadFromNetwork(url);
break;
case 14:
LogWrapper.Info("[Page] 主页预设:PCL CE 公告栏");
url = "https://s3.pysio.online/pcl2-ce/apiv2/pages/announce.xaml";
content = LoadFromNetwork(url);
break;
case 15:
LogWrapper.Info("[Page] 主页预设:Minecraft 信息流");
Dispatcher.Invoke(() =>
{
if (ModMain.frmHomepageNews is null)
ModMain.frmHomepageNews = new PageHomepageNewsView();
PanCustom.Children.Clear();
PanCustom.Children.Add(ModMain.frmHomepageNews);
});
return;
}
}
ModBase.RunInUi(() => LoadContent(content));
}
/// <summary>
/// 根据 URL 加载网络内容,优先使用缓存
/// </summary>
private string LoadFromNetwork(string url)
{
if (string.IsNullOrWhiteSpace(url)) return "";
var cachePath = Path.Combine(ModBase.pathTemp, "Cache", "Custom.xaml");
var cachedUrl = (string)States.UI.SavedHomepageUrl;
if (url == cachedUrl && File.Exists(cachePath))
{
LogWrapper.Info("[Page] 主页自定义数据来源:联网缓存文件");
// 后台更新缓存
onlineLoader.Start(url);
return ModBase.ReadFile(cachePath);
}
LogWrapper.Info("[Page] 主页自定义数据来源:联网全新下载");
HintWrapper.Show(Lang.Text("Launch.Homepage.Loading"));
ModBase.RunInUiWait(() => LoadContent("")); // 先清空页面
States.UI.SavedHomepageVersion = "";
onlineLoader.Start(url); // 下载完成后将会再次触发更新
return "";
}
private readonly object refreshLock = new();
public static string GetRandomHint(bool enableLengthLimit = false, bool raw = false)
{
string[]? lines = null;
// 外部文件
var externalPath = Path.Combine(ModBase.exePath, "PCL", "hints.txt");
if (File.Exists(externalPath))
{
try
{
lines = File.ReadAllLines(externalPath)
.Where(l => !string.IsNullOrWhiteSpace(l))
.Select(l => l.Trim())
.ToArray();
}
catch
{
ModBase.Log(
Lang.Text("Launch.Homepage.Error.ExternalFile", externalPath),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Homepage.Error.ExternalFile", externalPath));
}
}
// 嵌入式资源
if (lines is null || lines.Length == 0)
{
var langCode = LocalizationService.CurrentLanguage.Code;
lines = _LoadEmbeddedHints(langCode)
?? _LoadEmbeddedHints(LocalizationService.DefaultLanguageCode);
}
// 长度限制
if (enableLengthLimit)
{
var shortLines = lines.Where(l => l.Length < 50).ToArray();
if (shortLines.Length > 0) lines = shortLines;
}
// 随机返回
var hint = lines[Random.Shared.Next(lines.Length)];
return raw ? hint : hint.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
}
private static string[]? _LoadEmbeddedHints(string langCode)
{
try
{
var uri = new Uri($"pack://application:,,,/Plain Craft Launcher 2;component/Resources/hints/{langCode}.txt", UriKind.Absolute);
using var stream = Application.GetResourceStream(uri)?.Stream;
if (stream is null) return null;
using var reader = new StreamReader(stream);
return reader.ReadToEnd()
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Where(l => !string.IsNullOrWhiteSpace(l))
.Select(l => l.Trim())
.ToArray();
}
catch
{
return null;
}
}
// 联网获取主页文件
private readonly ModLoader.LoaderTask<string, int> onlineLoader;
private void OnlineLoaderSub(ModLoader.LoaderTask<string, int> task)
{
var address = task.input; // #3721 中连续触发两次导致内容变化
try
{
// 获取版本校验地址
string versionAddress;
if (address.Contains(".xaml"))
{
versionAddress = address.Replace(".xaml", ".xaml.ini");
}
else
{
versionAddress = address.BeforeFirst("?");
if (!versionAddress.EndsWith("/"))
versionAddress += "/";
versionAddress += "version";
if (address.Contains("?"))
versionAddress += "?" + address.AfterFirst("?");
}
// 校验版本
var version = "";
var needDownload = true;
try
{
version = Requester.FetchString(versionAddress);
if (version.Length > 1000)
throw new Exception($"获取的主页版本过长({version.Length} 字符)");
var currentVersion = States.UI.SavedHomepageVersion;
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(currentVersion) &&
(version ?? "") == (currentVersion ?? ""))
{
ModBase.Log($"[Page] 当前缓存的主页已为最新,当前版本:{version},检查源:{versionAddress}");
needDownload = false;
}
else
{
ModBase.Log($"[Page] 需要下载联网主页,当前版本:{version},检查源:{versionAddress}");
}
}
catch (Exception exx)
{
ModBase.Log(exx, "联网获取主页版本失败", ModBase.LogLevel.Developer);
ModBase.Log($"[Page] 无法检查联网主页版本,将直接下载,检查源:{versionAddress}");
}
// 实际下载
if (needDownload)
{
var fileContent = Requester.FetchString(address);
ModBase.Log($"[Page] 已联网下载主页,内容长度:{fileContent.Length},来源:{address}");
States.UI.SavedHomepageUrl = address;
States.UI.SavedHomepageVersion = version;
ModBase.WriteFile(ModBase.pathTemp + @"Cache\Custom.xaml", fileContent);
}
// 要求刷新
ModBase.RunInUi(Refresh); // 不直接调用 Refresh,以防止死循环(#6245
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Homepage.Error.Download", address),
ModBase.modeDebug
? ModBase.LogLevel.Msgbox
: ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Homepage.Error.Download", address));
}
}
/// <summary>
/// 立即强制刷新主页。
/// 必须在 UI 线程调用。
/// </summary>
public void ForceRefresh()
{
ModBase.Log("[Page] 要求强制刷新主页");
ClearCache();
// 实际的刷新
if (ModMain.frmMain.pageCurrent.page == FormMain.PageType.Launch)
{
PanBack.ScrollToHome();
Refresh();
}
else
{
ModMain.frmMain.PageChange(FormMain.PageType.Launch);
}
}
void IRefreshable.Refresh()
{
ForceRefresh();
}
/// <summary>
/// 清空主页缓存信息。
/// </summary>
private void ClearCache()
{
loadedContentHash = -1;
onlineLoader.input = "";
States.UI.SavedHomepageUrl = "";
States.UI.SavedHomepageVersion = "";
ModBase.Log("[Page] 已清空主页缓存");
}
/// <summary>
/// 从文本内容中加载主页。
/// 必须在 UI 线程调用。
/// </summary>
private void LoadContent(string content)
{
lock (loadContentLock)
{
// 如果加载目标内容一致则不加载
var hash = content.GetHashCode();
if (hash == loadedContentHash)
{
_ApplyHomepageLivePatchesFromFile();
return;
}
loadedContentHash = hash;
// 实际加载内容
PanCustom.Children.Clear();
if (string.IsNullOrWhiteSpace(content))
{
ModBase.Log("[Page] 实例化:清空主页 UI,来源为空");
return;
}
var loadStartTime = DateTime.Now;
try
{
content = ModMain.ArgumentReplace(content);
while (content.Contains("xmlns"))
content = content.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", "");
content =
$"<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\">{content}</StackPanel>";
ModBase.Log($"[Page] 实例化:加载主页 UI 开始,最终内容长度:{content.Count()}");
PanCustom.Children.Add((UIElement)ModBase.GetObjectFromXML(content, out var sanitizeResult));
_ShowSanitizeHints(sanitizeResult);
_ApplyHomepageLivePatchesFromFile();
}
catch (Exception ex)
{
if (ModBase.modeDebug)
{
ModBase.Log(ex, $"加载失败的主页内容:\r\n{content}");
if (ModMain.MyMsgBox(
ex is UnauthorizedAccessException
? ex.Message
: Lang.Text("Launch.Homepage.LoadFailed.Message", ex),
Lang.Text("Launch.Homepage.LoadFailed.Title"),
Lang.Text("Launch.Homepage.LoadFailed.Retry"),
Lang.Text("Common.Action.Cancel")) ==
1) goto Refresh; // 防止 SyncLock 死锁
}
else
{
ModBase.Log(
ex,
Lang.Text("Launch.Homepage.LoadFailed.Title"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Homepage.LoadFailed.Title"));
}
return;
}
var loadCostTime = (DateTime.Now - loadStartTime).Milliseconds;
ModBase.Log($"[Page] 实例化:加载主页 UI 完成,耗时 {loadCostTime}ms");
if (loadCostTime > 3000)
HintService.Hint(Lang.Text("Launch.Homepage.SlowWarning", Lang.Number(Math.Round(loadCostTime / 1000d, 1), "N1")));
}
return;
Refresh: ;
ForceRefresh();
}
private int loadedContentHash = -1;
private readonly object loadContentLock = new();
private static void _ShowSanitizeHints(XamlEventSanitizer.SanitizeResult result)
{
foreach (var unsupported in result.UnsupportedTypesFound)
HintService.Hint(Lang.Text("Event.Sanitize.UnsupportedTypeHint", unsupported), HintType.Error);
foreach (var unknown in result.UnrecognizedTypes)
HintService.Hint(Lang.Text("Event.Sanitize.UnknownTypeHint", unknown), HintType.Error);
}
private const string homepageLivePatchFileName = "CustomLive.json";
private const string homepageLiveSupportFileName = "CustomLive.supported.json";
// Keep the reflection patch surface explicit because patch files are written by external tools.
private static readonly Dictionary<string, string> _homepageLiveAllowedProperties = new(StringComparer.OrdinalIgnoreCase)
{
["text"] = "Text",
["title"] = "Title",
["info"] = "Info",
["tooltip"] = "ToolTip",
["visibility"] = "Visibility",
["isEnabled"] = "IsEnabled",
["opacity"] = "Opacity"
};
private FileSystemWatcher? _homepageLiveWatcher;
private DispatcherTimer? _homepageLivePatchTimer;
private void _EnsureHomepageLiveWatcher()
{
if (_homepageLiveWatcher != null) return;
if ((int)Config.Preference.Homepage.Type != 1) return;
try
{
var directory = _GetHomepageLiveDirectory();
Directory.CreateDirectory(directory);
_WriteHomepageLiveSupportMarker(directory);
_homepageLiveWatcher = new FileSystemWatcher(directory, homepageLivePatchFileName)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName
};
_homepageLiveWatcher.Changed += (_, _) => _QueueHomepageLivePatchApply();
_homepageLiveWatcher.Created += (_, _) => _QueueHomepageLivePatchApply();
_homepageLiveWatcher.Renamed += (_, _) => _QueueHomepageLivePatchApply();
_homepageLiveWatcher.EnableRaisingEvents = true;
_QueueHomepageLivePatchApply();
}
catch (Exception ex)
{
ModBase.Log(ex, "[Page] Failed to start custom homepage live patch watcher", ModBase.LogLevel.Developer);
}
}
private void _DisposeHomepageLiveWatcher()
{
try
{
_homepageLiveWatcher?.Dispose();
}
catch (Exception ex)
{
ModBase.Log(ex, "[Page] Failed to dispose custom homepage live patch watcher", ModBase.LogLevel.Developer);
}
_homepageLiveWatcher = null;
try
{
if (_homepageLivePatchTimer != null)
{
_homepageLivePatchTimer.Stop();
_homepageLivePatchTimer.Tick -= _HomepageLivePatchTimerTick;
_homepageLivePatchTimer = null;
}
}
catch (Exception ex)
{
ModBase.Log(ex, "[Page] Failed to dispose custom homepage live patch debounce timer", ModBase.LogLevel.Developer);
}
_DeleteHomepageLiveSupportMarker();
}
private void _QueueHomepageLivePatchApply()
{
ModBase.RunInUi(() =>
{
_homepageLivePatchTimer ??= new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(120)
};
_homepageLivePatchTimer.Tick -= _HomepageLivePatchTimerTick;
_homepageLivePatchTimer.Tick += _HomepageLivePatchTimerTick;
_homepageLivePatchTimer.Stop();
_homepageLivePatchTimer.Start();
});
}
private void _HomepageLivePatchTimerTick(object? sender, EventArgs e)
{
_homepageLivePatchTimer?.Stop();
_ApplyHomepageLivePatchesFromFile();
}
private void _ApplyHomepageLivePatchesFromFile()
{
if (PanCustom.Children.Count == 0) return;
if ((int)Config.Preference.Homepage.Type != 1) return;
var file = Path.Combine(_GetHomepageLiveDirectory(), homepageLivePatchFileName);
if (!File.Exists(file)) return;
try
{
var token = JsonNode.Parse(_ReadHomepageLivePatchFile(file),
new JsonNodeOptions { PropertyNameCaseInsensitive = true },
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true });
foreach (var patch in _EnumerateHomepageLivePatches(token))
_ApplyHomepageLivePatch(patch);
}
catch (Exception ex)
{
ModBase.Log(ex, "[Page] Failed to apply custom homepage live patches", ModBase.LogLevel.Developer);
}
}
private static string _ReadHomepageLivePatchFile(string file)
{
Exception? lastException = null;
for (var i = 0; i < 3; i++)
{
try
{
using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
catch (Exception ex)
{
lastException = ex;
Thread.Sleep(50);
}
}
throw lastException ?? new IOException("Unable to read custom homepage live patch file.");
}
private static string _GetHomepageLiveDirectory()
{
return Path.Combine(ModBase.exePath, "PCL");
}
private static void _WriteHomepageLiveSupportMarker(string directory)
{
try
{
var marker = new JsonObject(new JsonNodeOptions { PropertyNameCaseInsensitive = true })
{
["processId"] = Environment.ProcessId,
["processPath"] = Environment.ProcessPath ?? "",
["patchFile"] = homepageLivePatchFileName,
["startedAt"] = DateTime.Now.ToString("O", CultureInfo.InvariantCulture)
};
File.WriteAllText(Path.Combine(directory, homepageLiveSupportFileName), marker.ToJsonString());
}
catch (Exception ex)
{
ModBase.Log(ex, "[Page] Failed to write custom homepage live patch support marker", ModBase.LogLevel.Developer);
}
}
private static void _DeleteHomepageLiveSupportMarker()
{
try
{
var file = Path.Combine(_GetHomepageLiveDirectory(), homepageLiveSupportFileName);
if (!File.Exists(file)) return;
var marker = (JsonObject)JsonNode.Parse(_ReadHomepageLivePatchFile(file),
new JsonNodeOptions { PropertyNameCaseInsensitive = true },
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true })!;
if (marker["processId"]?.GetValue<int>() == Environment.ProcessId)
File.Delete(file);
}
catch (Exception ex)
{
ModBase.Log(ex, "[Page] Failed to delete custom homepage live patch support marker", ModBase.LogLevel.Developer);
}
}
private static IEnumerable<JsonObject> _EnumerateHomepageLivePatches(JsonNode token)
{
if (token is JsonObject obj)
{
if (obj["patches"] is JsonArray patches)
{
foreach (var patch in patches.OfType<JsonObject>())
yield return patch;
yield break;
}
if (_TryGetString(obj, "target", "tag", "name") != null)
{
yield return obj;
yield break;
}
foreach (var property in obj)
{
if (property.Value is not JsonObject patch) continue;
patch = (JsonObject)JsonNode.Parse(patch.ToJsonString(),
new JsonNodeOptions { PropertyNameCaseInsensitive = true },
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true })!;
patch["target"] ??= property.Key;
yield return patch;
}
}
else if (token is JsonArray array)
{
foreach (var patch in array.OfType<JsonObject>())
yield return patch;
}
}
private void _ApplyHomepageLivePatch(JsonObject patch)
{
var target = _TryGetString(patch, "target", "tag", "name");
if (string.IsNullOrWhiteSpace(target)) return;
foreach (var element in _FindElementsByTag(PanCustom, target))
_ApplyHomepageLivePatchToElement(element, patch);
}
private void _ApplyHomepageLivePatchToElement(FrameworkElement element, JsonObject patch)
{
_SetPropertyIfPresent(element, patch, "text", "Text");
_SetPropertyIfPresent(element, patch, "title", "Title");
_SetPropertyIfPresent(element, patch, "info", "Info");
_SetPropertyIfPresent(element, patch, "tooltip", "ToolTip");
_SetPropertyIfPresent(element, patch, "toolTip", "ToolTip");
_SetPropertyIfPresent(element, patch, "visibility", "Visibility");
_SetPropertyIfPresent(element, patch, "isEnabled", "IsEnabled");
_SetPropertyIfPresent(element, patch, "opacity", "Opacity");
if (patch["properties"] is JsonObject properties)
{
foreach (var property in properties)
_TrySetElementProperty(element, property.Key, property.Value?.ToString() ?? "");
}
var childrenXaml = _TryGetString(patch, "childrenXaml", "ChildrenXaml");
if (!string.IsNullOrEmpty(childrenXaml) && element is Panel panel)
_ReplacePanelChildren(panel, childrenXaml);
}
private static void _SetPropertyIfPresent(FrameworkElement element, JsonObject patch, string jsonName, string propertyName)
{
if (patch.TryGetPropertyValue(jsonName, out var value))
_TrySetElementProperty(element, propertyName, value?.ToString() ?? "");
}
private static bool _TrySetElementProperty(FrameworkElement element, string propertyName, string value)
{
if (!_homepageLiveAllowedProperties.TryGetValue(propertyName, out var allowedPropertyName))
{
ModBase.Log($"[Page] Skipped unsupported live patch property {propertyName}", ModBase.LogLevel.Developer);
return false;
}
propertyName = allowedPropertyName;
var property = element.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
if (property == null || !property.CanWrite) return false;
try
{
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var trimmedValue = value.Trim();
object convertedValue;
if (propertyType == typeof(string))
convertedValue = value;
else if (propertyType == typeof(object))
convertedValue = value;
else if (propertyType == typeof(bool) && bool.TryParse(trimmedValue, out var boolValue))
convertedValue = boolValue;
else if (propertyType == typeof(int) && int.TryParse(trimmedValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
convertedValue = intValue;
else if (propertyType == typeof(double) && double.TryParse(trimmedValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue))
convertedValue = doubleValue;
else if (propertyType == typeof(Visibility))
{
if (!Enum.TryParse(trimmedValue, true, out Visibility visibilityValue))
return false;
convertedValue = visibilityValue;
}
else if (propertyType.IsEnum && Enum.TryParse(propertyType, trimmedValue, true, out var enumValue))
convertedValue = enumValue;
else
return false;
property.SetValue(element, convertedValue);
return true;
}
catch (Exception ex)
{
ModBase.Log(ex, $"[Page] Failed to set live patch property {propertyName}", ModBase.LogLevel.Developer);
return false;
}
}
private static void _ReplacePanelChildren(Panel panel, string childrenXaml)
{
var content = ModMain.ArgumentReplace(childrenXaml);
while (content.Contains("xmlns"))
content = content.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", "");
var wrapped =
$"<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\">{content}</StackPanel>";
if (ModBase.GetObjectFromXML(wrapped) is not Panel parsedPanel) return;
var children = parsedPanel.Children.OfType<UIElement>().ToList();
parsedPanel.Children.Clear();
panel.Children.Clear();
foreach (var child in children)
panel.Children.Add(child);
}
private static IEnumerable<FrameworkElement> _FindElementsByTag(DependencyObject root, string tag)
{
if (root is FrameworkElement element &&
string.Equals(element.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase))
yield return element;
int count;
try
{
count = VisualTreeHelper.GetChildrenCount(root);
}
catch
{
yield break;
}
for (var i = 0; i < count; i++)
{
foreach (var child in _FindElementsByTag(VisualTreeHelper.GetChild(root, i), tag))
yield return child;
}
}
private static string? _TryGetString(JsonObject obj, params string[] names)
{
foreach (var name in names)
{
if (obj.TryGetPropertyValue(name, out var value))
return value?.ToString();
}
return null;
}
#endregion
}